그룹 D3 전설 아이템

Nov 19 2020

각 범례 항목의 원과 텍스트를 어떻게 그룹화 할 수 있습니까?

enter/에 문제가 exit있습니다. 그룹 ( g)을 만들 수 있지만 text/ circle요소를 추가 할 수는 없습니다 .

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))

현재 렌더링 된 SVG 마크 업은 다음과 같습니다.

<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>

다음은 원하는 마크 업입니다.

<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>

이렇게하면 이벤트 리스너를 추가 .legend-item하고 데이터 속성 key을 사용하여 라인 시리즈와 상호 참조 할 수 있습니다 .

참고 : 아래의 범례 코드 는 d3-legend 의 업데이트 된 버전 (6.2.0)입니다 . 코드를 약간 수정했습니다.


여기 내가 지금까지 가지고있는 작업의 예가 있습니다.

(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>

답변

2 RubenHelsloot Nov 19 2020 at 22:22

결과 .enter()는 해당 datum값이 있지만 관련된 노드가없는 선택입니다 . 그렇기 때문에 우리는 일반적으로 를 호출 한 후 마지막 순간에만 병합합니다 .append("g").

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

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

g두 번 추가 할 수 없기 때문에 g요소 를 추가 한 다음 요소 를 추가 한 text다음 g. 이것은 다음과 같이 보일 것입니다.

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())

당신이 D3 버전 6을 사용하고 있기 때문에, 당신은 또한 새로운 사용할 수 있습니다 .join()당신은 당신에 기능을 적용 할 수있는, 기능을 .enter(), .exit()그리고 선택 (암시) 업데이트 부품 및 병합 된 결과를 반환합니다 :

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()
  );

다음 스 니펫으로 이어집니다.

(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>