d3.js: 更改selection.each()中绑定的(drill-down)个数据
d3.js: Change (drill-down) data bound in selection.each()
我正在 d3.js(版本 5)中制作组织结构图。我希望它尽可能动态,所以当有变化时,我可以轻松更新输入文件,它会 re-render。我在下面和 this CodePen 中有一个简化的 reprex,其中我有 hard-coded 输入数据。
以下是我要实现的目标的简要说明:
const tree
是表示可视化分层部分的对象数组:
- 一个
ROOT
,
- 3 位经理,以及
- 6 个项目
const staff
是代表员工的对象数组。
searchObj
和 findIndicesOfMatches
在 .map()
中工作,将项目中员工的姓名替换为代表他们的对象(以及我将在继续开发时使用的属性)此组织结构图)
- 我确定树布局并渲染树。父节点根据需要展开以覆盖其子节点。
- 最后一步,也就是我被卡住的地方,是我想遍历叶节点,附加一个
g
,并根据 [=] 渲染额外的 rect
s 23=] 属性。我目前正在尝试在节点上使用 .each()
,检查以确保它们是叶子 (if(!d.children)
),附加一个 g
,并在项目中建立员工代表。
我不确定的是如何将绑定数据更改为 staff
属性。到目前为止,我没有尝试过。目前,我在 g
中得到一个 rect
,即使对于没有员工的项目也是如此。
它的样子:
它应该是什么样子:
有什么想法吗?
index.html
:
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="./org-chart.js"></script>
<style>
body {
font-family: 'Helvetica';
color: #666;
font: 12px sans-serif;
}
</style>
</head>
<body>
<div id="viz"></div>
</body>
</html>
org-chart.js
:
const tree = [
{
id: 'ROOT',
parent: null
},
{
id: 'Manager 1',
parent: 'ROOT'
},
{
id: 'Manager 2',
parent: 'ROOT'
},
{
id: 'Manager 3',
parent: 'ROOT'
},
{
id: 'Project 1',
parent: 'Manager 1',
staff: ['Staff 1']
},
{
id: 'Project 2',
parent: 'Manager 1',
staff: ['Staff 1', 'Staff 2']
},
{
id: 'Project 3',
parent: 'Manager 2',
staff: ['Staff 2', 'Staff 3', 'Staff 4']
},
{
id: 'Project 4',
parent: 'Manager 2',
staff: ['Staff 2', 'Staff 3', 'Staff 5']
},
{
id: 'Project 5',
parent: 'Manager 2',
staff: []
},
{
id: 'Project 6',
parent: 'Manager 3',
staff: ['Staff 4', 'Staff 5']
}
];
const staff = [
{ name: 'Staff 1', office: 'Office 1' },
{ name: 'Staff 2', office: 'Office 2' },
{ name: 'Staff 3', office: 'Office 3' },
{ name: 'Staff 4', office: 'Office 4' },
{ name: 'Staff 5', office: 'Office 5' }
];
function searchObj(obj, query) {
for (var key in obj) {
var value = obj[key];
if (typeof value === 'object') {
searchObj(value, query);
}
if (value === query) {
return true;
}
}
return false;
}
function findIndicesOfMatches(arrayOfObjects, query) {
booleanArray = arrayOfObjects.map(el => {
return searchObj(el, query);
});
const reducer = (accumulator, currentValue, index) => {
return currentValue ? accumulator.concat(index) : accumulator;
};
return booleanArray.reduce(reducer, []);
}
// Join tree and staff data
const joinedData = tree.map(el => {
if ('staff' in el) {
newStaffArray = el.staff.map(s => {
const staffIndex = findIndicesOfMatches(staff, s);
return staff[staffIndex];
});
return { ...el, staff: newStaffArray };
} else {
return el;
}
});
console.log('joinedData');
console.log(joinedData);
// Sizing variables
const margin = { top: 50, right: 50, bottom: 90, left: 90 },
width = 1000,
height = 200,
node_height = 25,
leaf_node_width = 100;
// Draw function
drawOrgChart = data => {
const svg = d3
.select('#viz')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.bottom + margin.top);
const stratify = d3
.stratify()
.parentId(d => d.parent)
.id(d => d.id);
const tree = d3
.tree()
.size([width, height])
.separation(d => leaf_node_width * 0.5);
const dataStratified = stratify(data);
var nodes = d3.hierarchy(dataStratified);
root_node = tree(nodes);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
var nodes = g
.selectAll('.node')
.data(nodes.descendants(), d => d.id)
.enter()
.append('g')
.attr('class', function(d) {
return 'node' + (d.children ? ' node--internal' : ' node--leaf');
})
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
var rect_colors = ['grey', 'blue', 'green', 'maroon'];
nodes
.append('rect')
.attr('height', node_height)
.attr('width', d => {
const extent = d3.extent(d.leaves().map(d => d.x));
return extent[1] - extent[0] + leaf_node_width;
})
.attr('fill', '#ffffff')
.attr('stroke', d => {
return rect_colors[d.depth];
})
.attr('transform', d => {
const first_leaf_x = d.leaves()[0].x;
return `translate(${-(d.x - first_leaf_x + leaf_node_width / 2)},0)`;
})
.attr('rx', 5)
.attr('ry', 5);
nodes
.append('text')
.attr('dy', '.35em')
.attr('x', d => 0)
.attr('y', node_height / 2)
.style('text-anchor', 'middle')
.text(function(d) {
return d.data.data.id;
});
// This is the bit I can't figure out:
// I'd like to append additional elements to
// the leaf nodes based on the 'staff' property
console.log(nodes.data());
nodes.each(function(d, j) {
if (!d.children) {
const staff = d.data.data.staff;
console.log(staff);
d3.select(this)
.append('g')
.selectAll('rect')
.data([staff])
.enter()
.append('rect')
.attr('x', 0)
.attr('y', (p, i) => 30 * (i + 1))
.attr('height', node_height)
.attr('width', leaf_node_width)
.attr('transform', `translate(-${leaf_node_width / 2},0)`)
.attr('stroke', 'red')
.attr('fill', '#efefef80');
}
});
};
document.addEventListener('DOMContentLoaded', function() {
drawOrgChart(joinedData);
});
您需要将 .data([staff])
替换为 .data(staff)
。 staff
已经是一个数组。如果你使用 [staff]
它绑定到一个 one 元素的数组,它本身就是一个数组。这就是为什么你只看到员工的一片叶子。
d3.select(this)
.append('g')
.selectAll('rect')
// use staff instead of [staff]
.data(staff)
....
查看此修改CodePen
矩形的大小仍然存在问题(最后一个在 svg 之外),但它应该让您走上正确的道路。
我正在 d3.js(版本 5)中制作组织结构图。我希望它尽可能动态,所以当有变化时,我可以轻松更新输入文件,它会 re-render。我在下面和 this CodePen 中有一个简化的 reprex,其中我有 hard-coded 输入数据。
以下是我要实现的目标的简要说明:
const tree
是表示可视化分层部分的对象数组:- 一个
ROOT
, - 3 位经理,以及
- 6 个项目
- 一个
const staff
是代表员工的对象数组。searchObj
和findIndicesOfMatches
在.map()
中工作,将项目中员工的姓名替换为代表他们的对象(以及我将在继续开发时使用的属性)此组织结构图)- 我确定树布局并渲染树。父节点根据需要展开以覆盖其子节点。
- 最后一步,也就是我被卡住的地方,是我想遍历叶节点,附加一个
g
,并根据 [=] 渲染额外的rect
s 23=] 属性。我目前正在尝试在节点上使用.each()
,检查以确保它们是叶子 (if(!d.children)
),附加一个g
,并在项目中建立员工代表。
我不确定的是如何将绑定数据更改为 staff
属性。到目前为止,我没有尝试过。目前,我在 g
中得到一个 rect
,即使对于没有员工的项目也是如此。
它的样子:
它应该是什么样子:
有什么想法吗?
index.html
:
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="./org-chart.js"></script>
<style>
body {
font-family: 'Helvetica';
color: #666;
font: 12px sans-serif;
}
</style>
</head>
<body>
<div id="viz"></div>
</body>
</html>
org-chart.js
:
const tree = [
{
id: 'ROOT',
parent: null
},
{
id: 'Manager 1',
parent: 'ROOT'
},
{
id: 'Manager 2',
parent: 'ROOT'
},
{
id: 'Manager 3',
parent: 'ROOT'
},
{
id: 'Project 1',
parent: 'Manager 1',
staff: ['Staff 1']
},
{
id: 'Project 2',
parent: 'Manager 1',
staff: ['Staff 1', 'Staff 2']
},
{
id: 'Project 3',
parent: 'Manager 2',
staff: ['Staff 2', 'Staff 3', 'Staff 4']
},
{
id: 'Project 4',
parent: 'Manager 2',
staff: ['Staff 2', 'Staff 3', 'Staff 5']
},
{
id: 'Project 5',
parent: 'Manager 2',
staff: []
},
{
id: 'Project 6',
parent: 'Manager 3',
staff: ['Staff 4', 'Staff 5']
}
];
const staff = [
{ name: 'Staff 1', office: 'Office 1' },
{ name: 'Staff 2', office: 'Office 2' },
{ name: 'Staff 3', office: 'Office 3' },
{ name: 'Staff 4', office: 'Office 4' },
{ name: 'Staff 5', office: 'Office 5' }
];
function searchObj(obj, query) {
for (var key in obj) {
var value = obj[key];
if (typeof value === 'object') {
searchObj(value, query);
}
if (value === query) {
return true;
}
}
return false;
}
function findIndicesOfMatches(arrayOfObjects, query) {
booleanArray = arrayOfObjects.map(el => {
return searchObj(el, query);
});
const reducer = (accumulator, currentValue, index) => {
return currentValue ? accumulator.concat(index) : accumulator;
};
return booleanArray.reduce(reducer, []);
}
// Join tree and staff data
const joinedData = tree.map(el => {
if ('staff' in el) {
newStaffArray = el.staff.map(s => {
const staffIndex = findIndicesOfMatches(staff, s);
return staff[staffIndex];
});
return { ...el, staff: newStaffArray };
} else {
return el;
}
});
console.log('joinedData');
console.log(joinedData);
// Sizing variables
const margin = { top: 50, right: 50, bottom: 90, left: 90 },
width = 1000,
height = 200,
node_height = 25,
leaf_node_width = 100;
// Draw function
drawOrgChart = data => {
const svg = d3
.select('#viz')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.bottom + margin.top);
const stratify = d3
.stratify()
.parentId(d => d.parent)
.id(d => d.id);
const tree = d3
.tree()
.size([width, height])
.separation(d => leaf_node_width * 0.5);
const dataStratified = stratify(data);
var nodes = d3.hierarchy(dataStratified);
root_node = tree(nodes);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
var nodes = g
.selectAll('.node')
.data(nodes.descendants(), d => d.id)
.enter()
.append('g')
.attr('class', function(d) {
return 'node' + (d.children ? ' node--internal' : ' node--leaf');
})
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
var rect_colors = ['grey', 'blue', 'green', 'maroon'];
nodes
.append('rect')
.attr('height', node_height)
.attr('width', d => {
const extent = d3.extent(d.leaves().map(d => d.x));
return extent[1] - extent[0] + leaf_node_width;
})
.attr('fill', '#ffffff')
.attr('stroke', d => {
return rect_colors[d.depth];
})
.attr('transform', d => {
const first_leaf_x = d.leaves()[0].x;
return `translate(${-(d.x - first_leaf_x + leaf_node_width / 2)},0)`;
})
.attr('rx', 5)
.attr('ry', 5);
nodes
.append('text')
.attr('dy', '.35em')
.attr('x', d => 0)
.attr('y', node_height / 2)
.style('text-anchor', 'middle')
.text(function(d) {
return d.data.data.id;
});
// This is the bit I can't figure out:
// I'd like to append additional elements to
// the leaf nodes based on the 'staff' property
console.log(nodes.data());
nodes.each(function(d, j) {
if (!d.children) {
const staff = d.data.data.staff;
console.log(staff);
d3.select(this)
.append('g')
.selectAll('rect')
.data([staff])
.enter()
.append('rect')
.attr('x', 0)
.attr('y', (p, i) => 30 * (i + 1))
.attr('height', node_height)
.attr('width', leaf_node_width)
.attr('transform', `translate(-${leaf_node_width / 2},0)`)
.attr('stroke', 'red')
.attr('fill', '#efefef80');
}
});
};
document.addEventListener('DOMContentLoaded', function() {
drawOrgChart(joinedData);
});
您需要将 .data([staff])
替换为 .data(staff)
。 staff
已经是一个数组。如果你使用 [staff]
它绑定到一个 one 元素的数组,它本身就是一个数组。这就是为什么你只看到员工的一片叶子。
d3.select(this)
.append('g')
.selectAll('rect')
// use staff instead of [staff]
.data(staff)
....
查看此修改CodePen
矩形的大小仍然存在问题(最后一个在 svg 之外),但它应该让您走上正确的道路。