#javascript #jquery #d3.js
Вопрос:
Я нахожу, что количество дочерних узлов в d3.js дерево должно быть ограничено, чтобы предотвратить плотную кластеризацию многих узлов.
Крайний пример:
Я пытался сделать это, группируя дочерние узлы, где это возможно, но доходит до того, что это уже невозможно.
Было бы полезно иметь макет, в котором можно задать количество отображаемых дочерних узлов, а также щелкнуть узел (или что-либо еще) в конце вертикального списка узлов, чтобы отобразить следующую партию дочерних узлов, что также скроет верхнюю группу дочерних узлов.
Другой расширяющийся узел затем отобразится в верхней части этого пакета, чтобы отобразить предыдущую партию узлов и т. Д., При этом количество отображаемых дочерних узлов всегда ограничено заданным значением.
Это делается для отображения/скрытия узлов, уже содержащихся в данных JSON. Как этого можно было бы достичь?
См. скрипку.
// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 90, bottom: 30, left: 90},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
.attr("width", width margin.right margin.left)
.attr("height", height margin.top margin.bottom)
.append("g")
.attr("transform", "translate("
margin.left "," margin.top ")");
var i = 0,
duration = 750,
root;
// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);
// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) { return d.children; });
root.x0 = height / 2;
root.y0 = 0;
// Collapse after the second level
root.children.forEach(collapse);
update(root);
// Collapse the node and all it's children
function collapse(d) {
if(d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
}
function update(source) {
const colourScale = d3.scaleOrdinal()
.domain( [ "Parent", "Child 1", "Child 2" ] )
.range( [ "#abacab", "#b67a4e", "#5a6fbb" ] );
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function(d){ d.y = d.depth * 180});
// ****************** Nodes section ***************************
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function(d) {return d.id || (d.id = i); });
// Enter any new modes at the parent's previous position.
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function(d) {
return "translate(" source.y0 "," source.x0 ")";
})
.on('click', click);
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style("fill", function(d) {
return colourScale(findParent(d))
});
// Add labels for the nodes
nodeEnter.append('text')
.attr("dy", ".35em")
.attr("x", function(d) {
return d.children || d._children ? -13 : 13;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) { return d.data.name; });
// UPDATE
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" d.y "," d.x ")";
});
// Update the node attributes and style
nodeUpdate.select('circle.node')
.attr('r', 6)
.style("fill", function(d) {
return colourScale(findParent(d));
});
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" source.y "," source.x ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// ****************** links section ***************************
// Update the links...
var link = svg.selectAll('path.link')
.data(links, function(d) { return d.id; });
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr("stroke-width", function(d) {
return 1;
})
.attr('d', function(d){
var o = {
x: source.x0,
y: source.y0
}
return diagonal(o, o)
})
.attr("opacity", "0.3")
.style("stroke", function(d) {
//return colourScale(findParentLinks(d));
});
// UPDATE
var linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function(d){ return diagonal(d, d.parent) });
// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function(d) {
var o = {x: source.x, y: source.y}
return diagonal(o, o)
})
.remove();
// Store the old positions for transition.
nodes.forEach(function(d){
d.x0 = d.x;
d.y0 = d.y;
});
// Creates a curved (diagonal) path from parent to the child nodes
function diagonal(s, d) {
path = `M ${s.y} ${s.x}
C ${(s.y d.y) / 2} ${s.x},
${(s.y d.y) / 2} ${d.x},
${d.y} ${d.x}`
return path
}
function findParent(datum) {
if (datum.depth < 2) {
return datum.data.name
} else {
return findParent(datum.parent)
}
}
function findParentLinks(datum) {
if (datum.target.depth < 2) {
return datum.target.name
} else {
return findParent(datum.target.parent)
}
}
// Toggle children on click.
function click(event, d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
Комментарии:
1. Заполнители внешних узлов будут похожи на кнопки прокрутки, верно? Щелчок по ним покажет новые узлы на той же глубине, скрывая при этом другие? Вот как я читаю «количество отображаемых дочерних узлов всегда ограничено заданным значением» и «узел (или что-то еще) в конце вертикального списка узлов можно щелкнуть, чтобы отобразить следующую партию дочерних узлов, что также скроет верхнюю группу дочерних узлов».
2. @AndrewReid В некотором смысле, да, как кнопки прокрутки. Однако, в отличие от кнопок прокрутки, узлы (или что-то еще…) в конце списка вертикальных узлов будут отображать следующую партию узлов в JSON, которые не видны из-за ограничения, установленного в сценарии для отображения числа.
3. Дочерние узлы, подлежащие ограничению, относятся к двум типам d. (с использованием d3.js 3.5.11 на данном этапе, пока я не смогу завершить изменения для работы с v7) в моем контексте. Два типа d., которые будут ограничены, будут самыми густонаселенными, следовательно, необходимо ограничить. Они есть в моем приложении, d.type ==»learning_event» и d.type = = «оценка». Однако для целей этого вопроса используйте все, что хотите, но ограничивайте, в идеале, d.типы в конечных узлах.
4. Кроме того , да, кнопки расширения работают только на уровне дочерних узлов, в которых они находятся.
5. Кстати, я ожидаю, что решение этой проблемы очень поможет нам d3.js пользователи, у которых возникают аналогичные проблемы с большим количеством дочерних узлов в дереве и ограниченным пространством на мониторе, чтобы показать их с помощью прокрутки, панорамирования или масштабирования.
Ответ №1:
var treeData = {
"name": "Top Level",
"children": [{
"name": "Child 1",
"children": [{
"name": "C1"
},
{
"name": "C2"
},
{
"name": "C3"
},
{
"name": "C4"
},
{
"name": "C5"
},
{
"name": "C6"
},
{
"name": "C7"
},
{
"name": "C8"
},
{
"name": "C9"
},
{
"name": "C10"
},
{
"name": "C11"
},
{
"name": "C12"
},
{
"name": "C13"
},
{
"name": "C14"
},
{
"name": "C15"
},
{
"name": "C16"
},
{
"name": "C17"
},
{
"name": "C18"
},
{
"name": "C19"
},
{
"name": "C20"
},
{
"name": "C21"
},
]
},
{
"name": "Child 2",
"children": [{
"name": "Son of Child 2"
},
{
"name": "Daughter of Child 2"
}
]
}
]
};
// Set the dimensions and margins of the diagram
var margin = {
top: 20,
right: 90,
bottom: 30,
left: 90
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
.attr("width", width margin.right margin.left)
.attr("height", height margin.top margin.bottom)
.append("g")
.attr("transform", "translate("
margin.left "," margin.top ")");
var i = 0,
duration = 750,
root;
// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);
// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) {
return d.children;
});
root.x0 = height / 2;
root.y0 = 0;
function pageNodes(d, maxNode) {
if (d.children) {
d.children.forEach(c => pageNodes(c, maxNode));
if (d.children.length > maxNode) {
d.pages = {}
const count = maxNode - 2;
const l = Math.ceil(d.children.length / count);
for (let i = 0; i < l; i ) {
let startRange = i * count;
let endRange = i * count count;
d.pages[i] = d.children.slice(startRange, endRange);
d.pages[i].unshift({
...d.pages[i][0],
data: {
name: "..."
},
page: i == 0 ? l - 1 : i - 1
})
// console.log(i, d.pages[i]);
d.pages[i].push({
...d.pages[i][0],
data: {
name: "..."
},
page: i != (l - 1) ? i 1 : 0,
});
}
d.children = d.pages[0];
}
}
}
root.children.forEach(c => pageNodes(c, 10));
// Collapse after the second level
root.children.forEach(collapse);
update(root);
// Collapse the node and all it's children
function collapse(d) {
if (d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
}
function update(source) {
const colourScale = d3.scaleOrdinal()
.domain(["Parent", "Child 1", "Child 2"])
.range(["#abacab", "#b67a4e", "#5a6fbb"]);
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function(d) {
d.y = d.depth * 180
});
// ****************** Nodes section ***************************
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function(d) {
return d.id || (d.id = i);
});
// Enter any new modes at the parent's previous position.
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function(d) {
return "translate(" source.y0 "," source.x0 ")";
})
.on('click', click);
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style("fill", function(d) {
return colourScale(findParent(d))
});
// Add labels for the nodes
nodeEnter.append('text')
.attr("dy", ".35em")
.attr("x", function(d) {
return d.children || d._children ? -13 : 13;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) {
return d.data.name;
});
// UPDATE
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" d.y "," d.x ")";
});
// Update the node attributes and style
nodeUpdate.select('circle.node')
.attr('r', 6)
.style("fill", function(d) {
return colourScale(findParent(d));
});
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" source.y "," source.x ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// ****************** links section ***************************
// Update the links...
var link = svg.selectAll('path.link')
.data(links, function(d) {
return d.id;
});
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr("stroke-width", function(d) {
return 1;
})
.attr('d', function(d) {
var o = {
x: source.x0,
y: source.y0
}
return diagonal(o, o)
})
.attr("opacity", "0.3")
.style("stroke", function(d) {
//return colourScale(findParentLinks(d));
});
// UPDATE
var linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function(d) {
return diagonal(d, d.parent)
});
// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function(d) {
var o = {
x: source.x,
y: source.y
}
return diagonal(o, o)
})
.remove();
// Store the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
// Creates a curved (diagonal) path from parent to the child nodes
function diagonal(s, d) {
path = `M ${s.y} ${s.x}
C ${(s.y d.y) / 2} ${s.x},
${(s.y d.y) / 2} ${d.x},
${d.y} ${d.x}`
return path
}
function findParent(datum) {
if (datum.depth < 2) {
return datum.data.name
} else {
return findParent(datum.parent)
}
}
function findParentLinks(datum) {
if (datum.target.depth < 2) {
return datum.target.name
} else {
return findParent(datum.target.parent)
}
}
// Toggle children on click.
function click(event, d) {
if (d.hasOwnProperty('page')) {
d.parent.children = d.parent.pages[d.page];
} else if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
Вот рабочий пример
Комментарии:
1. Выглядит довольно неплохо. Спасибо. Единственная проблема, которую я вижу на этом этапе, заключается в том, что, когда вы переходите к последней партии, этот уровень дочерних узлов полностью исчезает. Щелчок по последнему узлу расширения должен показать первый пакет и так далее.
2. Я раздвоил вашу скрипку, чтобы лучше показать различные именованные узлы детей jsfiddle.net/o374ny18/1
3. У меня возникли некоторые проблемы с вашим решением с использованием более сложных данных. Если я открою новый вопрос, вы посмотрите на него?
4. @IlludiumPu36 да, конечно, поделитесь ссылкой на новый вопрос.
5. Я заставил его работать в jsfiddle.net/9g5z1pen Есть ли что-нибудь в функции pageNodes, что в d3.js 3.5 (или что-то еще), что приведет к тому, что первый и последний дочерние узлы будут иметь имя «LE 1″, а не»…».?