Chris Farmer
Chris Farmer

Reputation: 25386

How can I append an element after select in d3?

I have a simple data structure that I want to use to render svg with d3.

const data = [
  {
    project: "One",
    stages: [
      { name: "foo", items: [1, 2, 3] },
      { name: "bar", items: [2, 3] }
    ]
  },
  {
    project: "Two",
    stages: [
      { name: "bar", items: [2, 3] },
      { name: "baz", items: [3, 4] }
    ]
  }
];

I want to render this as a couple nested <g> elements -- one for the project and one for the stage. Within each stage group, I want to have one <rect> representing all the items in the stage. Finally, one more rect per item. The final structure should look like:

<svg>
  <g class="project" data-project="One">
    <g class="stage" data-stage="foo">
      <rect class="range" />       <-- this doesn't get added -- why?
      <rect class="item" data-item="1" />
      <rect class="item" data-item="2" />
      <rect class="item" data-item="3" />
    </g>
    <g class="stage" data-stage="bar">
      <rect class="range" />       <-- this doesn't get added -- why?
      <rect class="item" data-item="2" />
      <rect class="item" data-item="3" />
    </g>
  </g>
  <g class="project" data-project="Two">
    <g class="stage" data-stage="bar">
      <rect class="range" />       <-- this doesn't get added -- why?
      <rect class="item" data-item="2" />
      <rect class="item" data-item="3" />
    </g>
    <g class="stage" data-stage="baz">
      <rect class="range" />       <-- this doesn't get added -- why?
      <rect class="item" data-item="3" />
      <rect class="item" data-item="4" />
    </g>
  </g>
</svg>

I tried to model this like so:

// Project group elements
const projects = d3.select(svg.current).selectAll("g.project").data(data);
const projectsEnter = projects
  .enter()
  .append("g")
  .classed("project", true)
  .attr("data-project", (d) => d.project);

// Stage groups within each project
const stages = projects
  .merge(projectsEnter)
  .selectAll("g.stage")
  .data((d) => d.stages);
const stagesEnter = stages
  .enter()
  .append("g")
  .classed("stage", true)
  .attr("data-stage", (d) => d.name);

// "range" representing the whole stage
// !!!!!
// These rect.range elements don't get added
// !!!!!
const range = stages.merge(stagesEnter).select("rect.range");
const rangeEnter = range.enter().append("rect").classed("range", true);

// Items within each stage
const items = stages
  .merge(stagesEnter)
  .selectAll("rect.item")
  .data((d) => d.items);
const itemsEnter = items.enter().append("rect").classed("item", true);
itemsEnter.merge(items).attr("data-item", (d) => d);

This mostly works, except the rect.range elements don't get added. I seem to be incorrectly understanding what select does here:

const range = stages.merge(stagesEnter).select("rect.range");
const rangeEnter = range.enter().append("rect").classed("range", true);

What's the correct way to get a single rect added to each g.stage here?

Here's a codesandbox to demo my issue: https://codesandbox.io/s/sweet-tdd-8tdoh?file=/src/App.js

Upvotes: 0

Views: 64

Answers (1)

Dan
Dan

Reputation: 1571

The code is simpler if you use join rather than enter/append/merge:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
    <div id="chart"></div>

    <script>
      const width = 500;
      const height = 500;

      const svg = d3.select('#chart')
        .append('svg')
          .attr('width', width)
          .attr('height', height);

      const data = [
        {
          project: "One",
          stages: [
            { name: "foo", items: [1, 2, 3] },
            { name: "bar", items: [2, 3] }
          ]
        },
        {
          project: "Two",
          stages: [
            { name: "bar", items: [2, 3] },
            { name: "baz", items: [3, 4] }
          ]
        }
      ];

      const projects = svg.selectAll('.project')
        .data(data)
        .join('g')
          .attr('class', 'project')
          .attr('data-project', d => d.project);

      const stages = projects.selectAll('.stage')
        .data(d => d.stages)
        .join('g')
          .attr('class', 'stage')
          .attr('data-stage', d => d.name);

      const range = stages.append('rect')
          .attr('class', 'range');

      const items = stages.selectAll('.item')
        .data(d => d.items)
        .join('rect')
          .attr('class', 'item')
          .attr('data-item', d => d);
    </script>
</body>
</html>

The generated SVG:

<svg width="500" height="500">
  <g class="project" data-project="One">
    <g class="stage" data-stage="foo">
      <rect class="range"></rect>
      <rect class="item" data-item="1"></rect>
      <rect class="item" data-item="2"></rect>
      <rect class="item" data-item="3"></rect>
    </g>
    <g class="stage" data-stage="bar">
      <rect class="range"></rect>
      <rect class="item" data-item="2"></rect>
      <rect class="item" data-item="3"></rect>
    </g>
  </g>
  <g class="project" data-project="Two">
    <g class="stage" data-stage="bar">
      <rect class="range"></rect>
      <rect class="item" data-item="2"></rect>
      <rect class="item" data-item="3"></rect>
    </g>
    <g class="stage" data-stage="baz">
      <rect class="range"></rect>
      <rect class="item" data-item="3"></rect>
      <rect class="item" data-item="4"></rect>
    </g>
  </g>
</svg>

Upvotes: 1

Related Questions