Legendenelemente der Gruppe D3

Nov 19 2020

Wie kann ich den Kreis und den Text für jedes Legendenelement gruppieren?

Ich habe ein Problem mit enter/ exit. Ich kann die Gruppen ( g) erstellen , aber die text/ circleElemente nicht anhängen .

li.selectAll("g")
  .data(items, ([key]) => key)
  .call((d) => d.enter().append("text")
    .attr("y", (d, i) => (i * 1.25) + "em")
    .attr("x", "1em")
    .text(([key]) => key))
  .call((d) => d.exit().remove())
  .call((d) => d.enter().append("circle")
    .attr("cy", (d, i) => (i * 1.25 - 0.25) + "em")
    .attr("cx", 0)
    .attr("r", "0.4em")
    .style("fill", ([key, value]) => value.color))

Hier ist das aktuell gerenderte SVG-Markup:

<g class="legend-items">
  <text y="0em" x="1em">Amanda</text>
  <text y="1.25em" x="1em">Linda</text>
  <text y="2.5em" x="1em">Dorothy</text>
  <text y="3.75em" x="1em">Betty</text>
  <text y="5em" x="1em">Helen</text>
  <text y="6.25em" x="1em">Patricia</text>
  <text y="7.5em" x="1em">Jessica</text>
  <text y="8.75em" x="1em">Ashley</text>
  <text y="10em" x="1em">Deborah</text>
  <circle cy="-0.25em" cx="0" r="0.4em" style="fill: rgb(228, 26, 28);"></circle>
  <circle cy="1em" cx="0" r="0.4em" style="fill: rgb(247, 129, 191);"></circle>
  <circle cy="2.25em" cx="0" r="0.4em" style="fill: rgb(255, 127, 0);"></circle>
  <circle cy="3.5em" cx="0" r="0.4em" style="fill: rgb(77, 175, 74);"></circle>
  <circle cy="4.75em" cx="0" r="0.4em" style="fill: rgb(255, 255, 51);"></circle>
  <circle cy="6em" cx="0" r="0.4em" style="fill: rgb(153, 153, 153);"></circle>
  <circle cy="7.25em" cx="0" r="0.4em" style="fill: rgb(166, 86, 40);"></circle>
  <circle cy="8.5em" cx="0" r="0.4em" style="fill: rgb(55, 126, 184);"></circle>
  <circle cy="9.75em" cx="0" r="0.4em" style="fill: rgb(152, 78, 163);"></circle>
</g>

Dies ist das gewünschte Markup:

<g class="legend-items">
  <g class="legend-item" data-key="Amanda">
    <text y="0em" x="1em">Amanda</text>
    <circle cy="-0.25em" cx="0" r="0.4em" style="fill: rgb(228, 26, 28);"></circle>
  </g>
  <g class="legend-item" data-key="Linda">
    <text y="1.25em" x="1em">Linda</text>
    <circle cy="1em" cx="0" r="0.4em" style="fill: rgb(247, 129, 191);"></circle>
  </g>
  <g class="legend-item" data-key="Dorothy">
    <text y="2.5em" x="1em">Dorothy</text>
    <circle cy="2.25em" cx="0" r="0.4em" style="fill: rgb(255, 127, 0);"></circle>
  </g>
  <g class="legend-item" data-key="Betty">
    <text y="3.75em" x="1em">Betty</text>
    <circle cy="3.5em" cx="0" r="0.4em" style="fill: rgb(77, 175, 74);"></circle>
  </g>
  <g class="legend-item" data-key="Helen">
    <text y="5em" x="1em">Helen</text>
    <circle cy="4.75em" cx="0" r="0.4em" style="fill: rgb(255, 255, 51);"></circle>
  </g>
  <g class="legend-item" data-key="Patricia">
    <text y="6.25em" x="1em">Patricia</text>
    <circle cy="6em" cx="0" r="0.4em" style="fill: rgb(153, 153, 153);"></circle>
  </g>
  <g class="legend-item" data-key="Jessica">
    <text y="7.5em" x="1em">Jessica</text>
    <circle cy="7.25em" cx="0" r="0.4em" style="fill: rgb(166, 86, 40);"></circle>
  </g>
  <g class="legend-item" data-key="Ashley">
    <text y="8.75em" x="1em">Ashley</text>
    <circle cy="8.5em" cx="0" r="0.4em" style="fill: rgb(55, 126, 184);"></circle>
  </g>
  <g class="legend-item" data-key="Deborah">
    <text y="10em" x="1em">Deborah</text>
    <circle cy="9.75em" cx="0" r="0.4em" style="fill: rgb(152, 78, 163);"></circle>
  </g>
</g>

Auf diese Weise kann ich einen Ereignis-Listener hinzufügen .legend-itemund das Datenattribut verwenden key, um Querverweise mit der Zeilenreihe zu erstellen .

Hinweis: Der folgende Legendencode ist eine aktualisierte Version (6.2.0) von d3-legend . Ich habe den Code leicht angepasst.


Beispiel

Hier ist ein funktionierendes Beispiel für das, was ich bisher habe.

(function() {
  d3.legend = function(g) {
    g.each(function() {
      var g = d3.select(this),
        items = {},
        svg = d3.select(g.property("nearestViewportElement")),
        legendPadding = g.attr("data-style-padding") || 5,
        lb = g.selectAll(".legend-box").data([true]),
        li = g.selectAll(".legend-items").data([true]);

      lb = lb.enter().append("rect").classed("legend-box", true).merge(lb);
      li = li.enter().append("g").classed("legend-items", true).merge(li);

      svg.selectAll("[data-legend]").each(function() {
        var self = d3.select(this);
        items[self.attr("data-legend")] = {
          pos: self.attr("data-legend-pos") || this.getBBox().y,
          color: self.attr("data-legend-color") ||
            self.style("stroke") ||
            self.style("fill")
        };
      });

      items = Object.entries(items)
        .sort(([keyA, valA], [keyB, valB]) => valA.pos - valB.pos);

      li.selectAll("text")
        .data(items, ([key]) => key)
        .call((d) => d.enter().append("text")
          .attr("y", (d, i) => (i * 1.25) + "em")
          .attr("x", "1em")
          .text(([key]) => key))
        .call((d) => d.exit().remove())

      li.selectAll("circle")
        .data(items, ([key]) => key)
        .call((d) => d.enter().append("circle")
          .attr("cy", (d, i) => (i * 1.25 - 0.25) + "em")
          .attr("cx", 0)
          .attr("r", "0.4em")
          .style("fill", ([key, value]) => value.color))

      // Reposition and resize the box
      var lbbox = li.node().getBBox();
      lb.attr("x", lbbox.x - legendPadding)
        .attr("y", lbbox.y - legendPadding)
        .attr("height", lbbox.height + 2 * legendPadding)
        .attr("width", lbbox.width + 2 * legendPadding);
    });

    return g;
  };
})();

const apiUrl = "https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/5_OneCatSevNumOrdered.csv";

const palette = [
  "#e41a1c",
  "#377eb8",
  "#4daf4a",
  "#984ea3",
  "#ff7f00",
  "#ffff33",
  "#a65628",
  "#f781bf",
  "#999999"
];

const chartConfig = {
  groupingKey: "name",
  xField: "year",
  yField: "n"
};

const mapperFn = ({ year, n, prop, ...rest }) => ({
  ...rest,
  year: parseInt(year, 10),
  n: parseFloat(n),
  prop: parseFloat(prop)
});

const { groupingKey, xField, yField } = chartConfig;

const margin = { top: 10, right: 120, bottom: 20, left: 60 },
  width = 400 - margin.left - margin.right,
  height = 200 - margin.top - margin.bottom;

d3.csv(apiUrl).then((jsonData) => {
  const data = jsonData.map(mapperFn);
  const ref = document.querySelector('.d3-chart > svg');
  const svgElement = d3.select(ref);

  // Empty the children
  svgElement.selectAll("*").remove();

  // Inner-SVG
  const svgInner = svgElement
    .append("g")
    .attr("transform", `translate(${margin.left}, ${margin.top})`);

  // Group the data: I want to draw one line per group
  const groupedData = [ ...d3.group(data, (d) => d[groupingKey]) ]
    .map(([key, values]) => ({ key, values }));
    
  const halfway = Math.floor(groupedData.length / 2);
  const dataLeft = groupedData.slice(0, halfway);
  const dataRight = groupedData.slice(halfway);

  // Add X axis --> it is a date format
  var xScale = d3
    .scaleLinear()
    .domain(d3.extent(data, (d) => d[xField]))
    .range([0, width]);

  svgInner
    .append("g")
    .attr("class", "d3-axis d3-axis-bottom")
    .attr("transform", `translate(0, ${height})`) .call( d3 .axisBottom(xScale) .ticks(2) .tickFormat((year) => year.toString()) ); // Add Y axis var yScaleLeft = d3 .scaleLinear() .domain([ 0, d3.max( dataLeft.flatMap((d) => d.values), (d) => d[yField] ) ]) .range([height, 0]); var yScaleRight = d3 .scaleLinear() .domain([ 0, d3.max( dataRight.flatMap((d) => d.values), (d) => d[yField] ) ]) .range([height, 0]); svgInner .append("g") .attr("class", "d3-axis d3-axis-right") .call(d3.axisLeft(yScaleLeft)); svgInner .append("g") .attr("class", "d3-axis d3-axis-right") .attr("transform", `translate(${width}, 0)`)
    .call(d3.axisRight(yScaleRight));

  // Color palette
  var res = groupedData.map((d) => d.key).sort(); // list of group names

  var color = d3.scaleOrdinal().domain(res).range(palette);

  svgInner
    .append("g")
    .attr("transform", `translate(${margin.left}, ${margin.top})`);

  // Draw the line
  svgInner
    .selectAll(".line")
    .data(dataLeft)
    .enter()
    .append("path")
    .attr("fill", "none")
    .attr("data-legend", (d) => d.key)
    .attr("stroke", (d) => color(d.key))
    .attr("stroke-width", 1.5)
    .attr("d", function(d) {
      return d3
        .line()
        .x((d) => xScale(d[xField]))
        .y((d) => yScaleLeft(+d[yField]))(d.values);
    });

  svgInner
    .selectAll(".line")
    .data(dataRight)
    .enter()
    .append("path")
    .attr("fill", "none")
    .attr("data-legend", (d) => d.key)
    .attr("stroke", (d) => color(d.key))
    .attr("stroke-width", 1.5)
    .attr("d", function(d) {
      return d3
        .line()
        .x((d) => xScale(d[xField]))
        .y((d) => yScaleRight(+d[yField]))(d.values);
    });

  let legend = svgInner
    .append("g")
    .attr("class", "legend")
    .attr("transform", "translate(280,20)")
    .style("font-size", "12px")
    .call(d3.legend);
});
:root {
  --chart-background: #272727;
  --chart-foreground: #d7d7d7;
}

.d3-chart {
  background: var(--chart-background);
}

.d3-chart .d3-axis line,
.d3-chart .d3-axis path {
  stroke: var(--chart-foreground);
}

.d3-chart .d3-axis text {
  fill: var(--chart-foreground);
}

.d3-chart .legend-box {
  fill: none;
}

.d3-chart .legend .legend-items text {
  fill: var(--chart-foreground);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<div class="d3-chart">
  <svg width="400px" height="200px" />
</div>

Antworten

2 RubenHelsloot Nov 19 2020 at 22:22

Das Ergebnis von .enter()ist eine Auswahl, die entsprechende datumWerte hat, aber keine zugehörigen Knoten hat. Deshalb verschmelzen wir normalerweise erst im allerletzten Moment, nachdem wir angerufen haben .append("g").

g = li.selectAll("g")
  .data(items);

g
  .enter()
  .append("g")
  .merge(g);

Da Sie nicht gzweimal anhängen können , müssen Sie ein gElement und dann ein textElement anhängen und dann das zurückgeben g. Das würde ungefähr so ​​aussehen:

li.selectAll("g")
  .data(items, ([key]) => key)
  .call((d) => {
    const g = d.enter()
      .append("g");

    g.append("text")
      .attr("y", (d, i) => (i * 1.25) + "em")
      .attr("x", "1em")
      .text(([key]) => key);

    g.append("circle")
      .attr("cy", (d, i) => (i * 1.25 - 0.25) + "em")
      .attr("cx", 0)
      .attr("r", "0.4em")
      .style("fill", ([key, value]) => value.color)

    return g;
  })
  .call((d) => d.exit().remove())

Da Sie jedoch d3 v6 verwenden, können Sie auch die neue verwenden .join()Funktionalität, in dem Sie auf die Funktionen Ihrer anwenden können .enter(), .exit()und (implizit) Update Teile der Auswahl, und das gibt das fusionierte Ergebnis:

li.selectAll("g")
  .data(items, ([key]) => key)
  .join(
    enter => {
      const g = enter.append("g");
      g.append("text")
        .attr("y", (d, i) => (i * 1.25) + "em")
        .attr("x", "1em")
        .text(([key]) => key);
      g.append("circle")
        .attr("cy", (d, i) => (i * 1.25 - 0.25) + "em")
        .attr("cx", 0)
        .attr("r", "0.4em")
        .style("fill", ([key, value]) => value.color);
      return g;
    },
    update => update,
    exit => exit.remove()
  );

Das führt zu folgendem Snippet:

(function() {
  d3.legend = function(g) {
    g.each(function() {
      var g = d3.select(this),
        items = {},
        svg = d3.select(g.property("nearestViewportElement")),
        legendPadding = g.attr("data-style-padding") || 5,
        lb = g.selectAll(".legend-box").data([true]),
        li = g.selectAll(".legend-items").data([true]);

      lb = lb.enter().append("rect").classed("legend-box", true).merge(lb);
      li = li.enter().append("g").classed("legend-items", true).merge(li);

      svg.selectAll("[data-legend]").each(function() {
        var self = d3.select(this);
        items[self.attr("data-legend")] = {
          pos: self.attr("data-legend-pos") || this.getBBox().y,
          color: self.attr("data-legend-color") ||
            self.style("stroke") ||
            self.style("fill")
        };
      });

      items = Object.entries(items)
        .sort(([keyA, valA], [keyB, valB]) => valA.pos - valB.pos);

      li.selectAll("g")
        .data(items, ([key]) => key)
        .join(
          enter => {
            const g = enter.append("g");
            g.append("text")
              .attr("y", (d, i) => (i * 1.25) + "em")
              .attr("x", "1em")
              .text(([key]) => key);
            g.append("circle")
              .attr("cy", (d, i) => (i * 1.25 - 0.25) + "em")
              .attr("cx", 0)
              .attr("r", "0.4em")
              .style("fill", ([key, value]) => value.color);
            return g;
          },
          update => update,
          exit => exit.remove()
        );

      // Reposition and resize the box
      var lbbox = li.node().getBBox();
      lb.attr("x", lbbox.x - legendPadding)
        .attr("y", lbbox.y - legendPadding)
        .attr("height", lbbox.height + 2 * legendPadding)
        .attr("width", lbbox.width + 2 * legendPadding);
    });

    return g;
  };
})();

const apiUrl = "https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/5_OneCatSevNumOrdered.csv";

const palette = [
  "#e41a1c",
  "#377eb8",
  "#4daf4a",
  "#984ea3",
  "#ff7f00",
  "#ffff33",
  "#a65628",
  "#f781bf",
  "#999999"
];

const chartConfig = {
  groupingKey: "name",
  xField: "year",
  yField: "n"
};

const mapperFn = ({ year, n, prop, ...rest }) => ({
  ...rest,
  year: parseInt(year, 10),
  n: parseFloat(n),
  prop: parseFloat(prop)
});

const { groupingKey, xField, yField } = chartConfig;

const margin = { top: 10, right: 120, bottom: 20, left: 60 },
  width = 400 - margin.left - margin.right,
  height = 200 - margin.top - margin.bottom;

d3.csv(apiUrl).then((jsonData) => {
  const data = jsonData.map(mapperFn);
  const ref = document.querySelector('.d3-chart > svg');
  const svgElement = d3.select(ref);

  // Empty the children
  svgElement.selectAll("*").remove();

  // Inner-SVG
  const svgInner = svgElement
    .append("g")
    .attr("transform", `translate(${margin.left}, ${margin.top})`);

  // Group the data: I want to draw one line per group
  const groupedData = [ ...d3.group(data, (d) => d[groupingKey]) ]
    .map(([key, values]) => ({ key, values }));
    
  const halfway = Math.floor(groupedData.length / 2);
  const dataLeft = groupedData.slice(0, halfway);
  const dataRight = groupedData.slice(halfway);

  // Add X axis --> it is a date format
  var xScale = d3
    .scaleLinear()
    .domain(d3.extent(data, (d) => d[xField]))
    .range([0, width]);

  svgInner
    .append("g")
    .attr("class", "d3-axis d3-axis-bottom")
    .attr("transform", `translate(0, ${height})`) .call( d3 .axisBottom(xScale) .ticks(2) .tickFormat((year) => year.toString()) ); // Add Y axis var yScaleLeft = d3 .scaleLinear() .domain([ 0, d3.max( dataLeft.flatMap((d) => d.values), (d) => d[yField] ) ]) .range([height, 0]); var yScaleRight = d3 .scaleLinear() .domain([ 0, d3.max( dataRight.flatMap((d) => d.values), (d) => d[yField] ) ]) .range([height, 0]); svgInner .append("g") .attr("class", "d3-axis d3-axis-right") .call(d3.axisLeft(yScaleLeft)); svgInner .append("g") .attr("class", "d3-axis d3-axis-right") .attr("transform", `translate(${width}, 0)`)
    .call(d3.axisRight(yScaleRight));

  // Color palette
  var res = groupedData.map((d) => d.key).sort(); // list of group names

  var color = d3.scaleOrdinal().domain(res).range(palette);

  svgInner
    .append("g")
    .attr("transform", `translate(${margin.left}, ${margin.top})`);

  // Draw the line
  svgInner
    .selectAll(".line")
    .data(dataLeft)
    .enter()
    .append("path")
    .attr("fill", "none")
    .attr("data-legend", (d) => d.key)
    .attr("stroke", (d) => color(d.key))
    .attr("stroke-width", 1.5)
    .attr("d", function(d) {
      return d3
        .line()
        .x((d) => xScale(d[xField]))
        .y((d) => yScaleLeft(+d[yField]))(d.values);
    });

  svgInner
    .selectAll(".line")
    .data(dataRight)
    .enter()
    .append("path")
    .attr("fill", "none")
    .attr("data-legend", (d) => d.key)
    .attr("stroke", (d) => color(d.key))
    .attr("stroke-width", 1.5)
    .attr("d", function(d) {
      return d3
        .line()
        .x((d) => xScale(d[xField]))
        .y((d) => yScaleRight(+d[yField]))(d.values);
    });

  let legend = svgInner
    .append("g")
    .attr("class", "legend")
    .attr("transform", "translate(280,20)")
    .style("font-size", "12px")
    .call(d3.legend);
});
:root {
  --chart-background: #272727;
  --chart-foreground: #d7d7d7;
}

.d3-chart {
  background: var(--chart-background);
}

.d3-chart .d3-axis line,
.d3-chart .d3-axis path {
  stroke: var(--chart-foreground);
}

.d3-chart .d3-axis text {
  fill: var(--chart-foreground);
}

.d3-chart .legend-box {
  fill: none;
}

.d3-chart .legend .legend-items text {
  fill: var(--chart-foreground);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<div class="d3-chart">
  <svg width="400px" height="200px" />
</div>