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 输入数据。

以下是我要实现的目标的简要说明:

我不确定的是如何将绑定数据更改为 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 之外),但它应该让您走上正确的道路。