Если цель:
состоит в том, чтобы объединить симуляцию силы d3, элементы g и полигоны Вороного, чтобы упростить триггерные события на узлах, такие как перетаскивание, наведение курсора мыши, всплывающие подсказки и т. д. с графиком, который может динамически обновляться.
Я собираюсь немного отступить от специфики вашего кода и попытаться добраться до цели. Я буду использовать два основных источника (один, на который вы ссылаетесь) в этой попытке добраться туда (и я могу ошибаться при этом).
Первый источник: пример перетаскивания блочного круга Майка Бостока.
Второй источник: пример графика, ориентированного на Силу, Майка Бостока.
Я надеюсь, что этот подход, по крайней мере, поможет достичь вашей цели (я воспользовался им частично, так как у меня возникли трудности с вашим фрагментом). Это должно быть полезно в качестве минимального примера и доказательства концепции.
Как и вы, я буду использовать пример с перетаскиванием круга в качестве основы, а затем попытаюсь включить пример с направленной силой.
Ключевые части графа направленной силы, которые необходимо импортировать, определяют симуляцию:
var simulation = d3.forceSimulation()
Назначение узлов:
simulation
.nodes(circle)
.on("tick", ticked);
( .nodes(graph.nodes)
в оригинале )
Инструктирование, что делать в тик:
force.nodes(circles)
.on('tick',ticked);
Отмеченная функция:
function ticked() {
circle.selectAll('circle')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
( нам не нужна часть ссылки, и мы хотим обновить круги (а не переменную с именем node )
И части, которые попадают в события перетаскивания.
Если мы импортируем все это в сниппет (объединив события перетаскивания, добавив отмеченную функцию, мы получим:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 32;
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody())
var circles = d3.range(20).map(function() {
return {
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[-1, -1], [width + 1, height + 1]]);
var circle = svg.selectAll("g")
.data(circles)
.enter().append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("id", function(d, i) { return "cell-" + i; });
circle.append("clipPath")
.attr("id", function(d, i) { return "clip-" + i; })
.append("use")
.attr("xlink:href", function(d, i) { return "#cell-" + i; });
circle.append("circle")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d, i) { return color(i); });
simulation
.nodes(circles)
.on("tick", ticked);
function ticked() {
circle.selectAll('circle')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d, i) {
d3.select(this).classed("active", false);
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function renderCell(d) {
return d == null ? null : "M" + d.join("L") + "Z";
}
path {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active circle {
stroke: #000;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="600" height="400"></svg>
Очевидная проблема заключается в том, что ячейки не обновляются, если нет перетаскивания. Чтобы решить эту проблему, нам просто нужно взять строку, которая обновляет ячейки при перетаскивании, и поместить ее в отмеченную функцию, чтобы она обновлялась по тиканию:
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 32;
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody())
var circles = d3.range(20).map(function() {
return {
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[-1, -1], [width + 1, height + 1]]);
var circle = svg.selectAll("g")
.data(circles)
.enter().append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("id", function(d, i) { return "cell-" + i; });
circle.append("clipPath")
.attr("id", function(d, i) { return "clip-" + i; })
.append("use")
.attr("xlink:href", function(d, i) { return "#cell-" + i; });
circle.append("circle")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d, i) { return color(i); });
circle.append("text")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("dy", '0.35em')
.attr("text-anchor", function(d) { return 'middle'; })
.attr("opacity", 0.6)
.style("font-size", "1.8em")
.style("font-family", "Sans-Serif")
.text(function(d, i) { return i; })
simulation
.nodes(circles)
.on("tick", ticked);
function ticked() {
circle.selectAll('circle')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
circle.selectAll('text')
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
}
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d, i) {
d3.select(this).classed("active", false);
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function renderCell(d) {
return d == null ? null : "M" + d.join("L") + "Z";
}
path {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active circle {
stroke: #000;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="600" height="400"></svg>
update: обновление узлов:
По крайней мере, для меня добавление и удаление узлов усложнилось. Основная проблема заключалась в том, что приведенный выше код переупорядочивал группы svg с помощью d3.selection.raise() в событиях перетаскивания, что могло испортить мой порядок пути клипа, если использовать только приращение элемента данных. Точно так же удаление элементов из середины массива может вызвать проблемы с сопряжением между ячейками, группами и кругами. Это сопряжение было основной проблемой — наряду с обеспечением того, чтобы все добавленные узлы находились в правильном родительском элементе и в правильном порядке.
Чтобы решить проблемы с сопряжением, я использовал новое свойство в данных для использования в качестве идентификатора, а не приращения. Во-вторых, я делаю пару конкретных манипуляций с ячейками при добавлении: убеждаюсь, что они находятся в правильном родительском элементе и что ячейка отображается над кругом в DOM (используя d3.selection.lower()).
Примечание. Мне не удалось удалить круг и сохранить работу вороного с типичным циклом обновления, поэтому я просто воссоздал его для каждого удаления, и поскольку, насколько мне известно, вороной пересчитывается каждый тик, это не должно быть проблемой.
Результат (нажмите, чтобы удалить/добавить, нажмите кнопку, чтобы переключить удаление/добавление):
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
radius = 32;
var n = 0;
var circles = d3.range(15).map(function() {
return {
n: n++,
x: Math.round(Math.random() * (width - radius * 2) + radius),
y: Math.round(Math.random() * (height - radius * 2) + radius)
};
});
// control add/remove
var addNew = false;
d3.select('#control').append('input')
.attr('type','button')
.attr('value', addNew ? "Add" : "Remove")
.on('click', function(d) {
addNew = !addNew;
d3.select(this).attr('value', addNew ? "Add" : "Remove")
d3.selectAll('g').on('click', (addNew) ? add : remove);
});
var color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.extent([[-1, -1], [width + 1, height + 1]]);
var circle = svg.selectAll("g")
.data(circles)
.enter().append("g")
.attr('id',function(d) { return 'g-'+d.n })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on('click', (addNew) ? add : remove);
var cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("class","cell")
.attr("id", function(d) { return "cell-" + d.data.n; });
circle.append("clipPath")
.attr("id", function(d) { return "clip-" + d.n; })
.append("use")
.attr("xlink:href", function(d) { return "#cell-" + d.n; });
circle.append("circle")
.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d) { return color(d.n); });
circle.append("text")
.attr("clip-path", function(d, i) { return "url(#clip-" + i + ")"; })
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("dy", '0.35em')
.attr("text-anchor", function(d) { return 'middle'; })
.attr("opacity", 0.6)
.style("font-size", "1.8em")
.style("font-family", "Sans-Serif")
.text(function(d) { return d.n; })
var simulation = d3.forceSimulation()
.nodes(circles)
.force('charge', d3.forceManyBody());
simulation.nodes(circles)
.on('tick',ticked);
function ticked() {
circle.selectAll('circle')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
circle.selectAll('text')
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d3.select(this).select("circle").attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
cell = cell.data(voronoi.polygons(circles)).attr("d", renderCell);
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function remove () {
d3.select(this).raise();
var id = d3.select(this).attr('id').split('-')[1];
id = +id;
// Get the clicked item:
var index = circles.map(function(d) {
return d.n;
}).indexOf(id);
circles.splice(index,1);
// Update circle data:
var circle = svg.selectAll("g")
.data(circles);
circle.exit().remove();
circle.selectAll("clipPath").exit().remove();
circle.selectAll("circle").exit().remove();
circle.selectAll("text").exit().remove();
//// Update voronoi:
d3.selectAll('.cell').remove();
cell = circle.append("path")
.data(voronoi.polygons(circles))
.attr("d", renderCell)
.attr("class","cell")
.attr("id", function(d) { return "cell-" + d.data.n; });
simulation.nodes(circles)
.on('tick',ticked);
}
function add() {
// Add circle to circles:
var coord = d3.mouse(this);
var newIndex = d3.max(circles, function(d) { return d.n; }) + 1;
circles.push({x: coord[0], y: coord[1], n: newIndex });
// Enter and Append:
circle = svg.selectAll("g").data(circles).enter()
var newCircle = circle.append("g")
.attr('id',function(d) { return 'g-'+d.n })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on('click',add)
cell = circle.selectAll("path")
.data(voronoi.polygons(circles)).enter();
cell.select('#g-'+newIndex).append('path')
.attr("d", renderCell)
.attr("class","cell")
.attr("id", function(d) { return "cell-" + d.data.n; });
newCircle.data(circles).enter();
newCircle.append("clipPath")
.attr("id", function(d) { return "clip-" + d.n; })
.append("use")
.attr("xlink:href", function(d) { return "#cell-" + d.n; });
newCircle.append("circle")
.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", radius)
.style("fill", function(d) { return color(d.n); });
newCircle.append("text")
.attr("clip-path", function(d) { return "url(#clip-" + d.n + ")"; })
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("dy", '0.35em')
.attr("text-anchor", function(d) { return 'middle'; })
.attr("opacity", 0.6)
.style("font-size", "1.8em")
.style("font-family", "Sans-Serif")
.text(function(d) { return d.n; })
cell = d3.selectAll('.cell');
d3.select("#cell-"+newIndex).lower(); // ensure the path is above the circle in svg.
simulation.nodes(circles)
.on('tick',ticked);
}
function renderCell(d) {
return d == null ? null : "M" + d.join("L") + "Z";
}
.cell {
pointer-events: all;
fill: none;
stroke: #666;
stroke-opacity: 0.2;
}
.active circle {
stroke: #000;
stroke-width: 2px;
}
svg {
background: #eeeeee;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="control"> </div>
<svg width="960" height="500"></svg>
Что касается конкретных частей вашего вопроса, я обнаружил, что проблемы с перетаскиванием и отсечением пути в первых двух пунктах вашего вопроса были в значительной степени проблемой объединения путей отсечения, ячеек и кругов, а также поиска правильного способа добавления новые элементы диаграммы, которые, надеюсь, я продемонстрировал выше.
Я надеюсь, что этот последний фрагмент ближе к конкретным проблемам, с которыми вы столкнулись, и я надеюсь, что приведенный выше код ясен, но, вероятно, он перешел от ясного и лаконичного Bostockian к какому-то другому более низкому стандарту.
person
Andrew Reid
schedule
25.02.2017