D3 Bar Chart + React Hooks - выход (). Удалить () не работает - PullRequest
1 голос
/ 20 апреля 2020

Я использую реагирование и d3, пытаясь создать простую гистограмму, которая обновляет график при обновлении данных. Диаграмма обновляется, когда изменяются данные, но, похоже, она располагается поверх старой диаграммы. Я думаю, что проблема связана с функцией d3 exit (). Remove ().

Насколько я понимаю, метод выхода d3 должен возвращать массив элементов, которые должны быть удалены, однако, когда я консоль регистрирую это, я вижу массив "неопределенных". Я очень благодарен за любую помощь!

Вот коды и поля: https://codesandbox.io/s/gifted-field-n66hw?file= / src / Barchart. js

Вот фрагмент кода:

import React, { useEffect, useRef } from "react";
import * as d3 from "d3";

const BarChart = props => {
  const { randomData, width, height, padding } = props;
  const ref = useRef(null);

  function colorGradient(v) {
    return "rgb(0, " + v * 5 + ", 0";
  }

  //insert & remove elements using D3
  useEffect(() => {
    if (randomData.length > 0 && ref.current) {
      const group = d3.select(ref.current);

      // the data operator binds data items with DOM elements
      // the resulting selection contains the enter and exit subselections
      const update = group
        .append("g")
        .selectAll("rect")
        .data(randomData);

      let bars = update
        .enter() // create new dom elements for added data items
        .append("rect")
        .merge(update)
        .attr("x", (d, i) => i * (width / randomData.length))
        .attr("y", d => height - d * 5)
        .attr("width", width / randomData.length - padding)
        .attr("height", d => d * 5)
        .attr("fill", d => colorGradient(d));

      let labels = update
        .enter()
        .append("text")
        .text(d => d)
        .attr("text-anchor", "middle")
        .attr(
          "x",
          (d, i) =>
            i * (width / randomData.length) +
            (width / randomData.length - padding) / 2
        )
        .attr("y", d => height - d * 5 + 12)
        .style("font-family", "sans-serif")
        .style("font-size", 12)
        .style("fill", "#ffffff");

      update.exit().remove();
    }
  }, [randomData, height, padding, width]);

  return (
    <svg width={width} height={height}>
      <g ref={ref} />
    </svg>
  );
};

export default BarChart;

1 Ответ

1 голос
/ 20 апреля 2020

Каждый раз, когда вы обновляете диаграмму, вы запускаете это:

 const update = group
    .append("g")             // create a new g
    .selectAll("rect")       // select all the rectangles in that g (which are none)
    .data(randomData);      

update теперь пустой выбор, нет rect s для выбора во вновь созданном g. Поэтому, когда мы используем update.enter(), элемент DOM создается для каждого элемента в массиве данных. Использование Enter создаст элемент для каждого элемента в массиве данных, который еще не имеет соответствующего элемента.

update.exit() будет пустым, поскольку в update не выбрано ни одного элемента, поэтому ничего не будет удалено. Ранее созданные полосы не затрагиваются, вы их не выбираете.

Если мы изменим ваш код просто для удаления .append("g"), это приблизит нас к работе ( например ). Полосы были окрашены в белый цвет, поэтому они не были видны, я изменил цвет заливки, чтобы выбор обновления был видимым


Если мы удалим .append("g"), у нас есть некоторые другие проблемы при обновлении:

  • вы не выходите из текста (поскольку вы не выбираете text с .selectAll(), только rect элементами), а
  • вы объединение текста и прямоугольников в одну выделенную область, что немного проблематично c, если вы хотите расположить и покрасить их по-разному при обновлении.

Вторая проблема может быть объяснена немного подробнее:

update.enter().append("text") // returns a selection of newly created text elements
   .merge(update)             // merges the selection of newly created text with existing rectangles
   .attr("fill", ....         // affects both text and rects.

Эти две проблемы могут быть решены при правильном использовании цикла ввода / обновления / выхода.

Следует отметить, что шаблон выхода обновления ввода D3 не предназначен для ввода элементов более одного раза с одним и тем же оператором, вы вводите текст и выполняете инструкции с одним и тем же оператором ввода, см. Здесь .

Таким образом, один из вариантов - использовать две выборки, одну для текста и одну для ректов:

  const updateRect = group
    .selectAll("rect")
    .data(randomData);

  let bars = updateRect
    .enter() // create new dom elements for added data items
    .append("rect")
    .merge(updateRect)
    .attr("x", (d, i) => i * (width / randomData.length))
    .attr("y", d => height - d * 5)
    .attr("width", width / randomData.length - padding)
    .attr("height", d => d * 5)
    .attr("fill", d => colorGradient(d));

  const updateText = group
    .selectAll("text")
    .data(randomData);

  let labels = updateText
    .enter()
    .append("text")
    .merge(updateText)
    .text(d => d)
    .attr("text-anchor", "middle")
    .attr(
      "x",
      (d, i) =>
        i * (width / randomData.length) +
        (width / randomData.length - padding) / 2
    )
    .attr("y", d => height - d * 5 + 12)
    .style("font-family", "sans-serif")
    .style("font-size", 12)
    .style("fill", "#fff");

  updateRect.exit().remove();
  updateText.exit().remove();

Здесь в песочнице форме.

Другой вариант - использовать родительский g для хранения как прямоугольника, так и текста, это может быть сделано разными способами, но если вам не нужен переход между значениями или количеством столбцов, вероятно, будет наиболее просто, например:

  const update = group
    .selectAll("g")
    .data(randomData);

  // add a g for every extra datum
  const enter = update.enter().append("g")
    // give them a rect and text element:
    enter.append("rect");
    enter.append("text");

  // merge update and enter:
  const bars = update.merge(enter);

  // modify the rects
  bars.select("rect")
    .attr("x", (d, i) => i * (width / randomData.length))
    .attr("y", d => height - d * 5)
    .attr("width", width / randomData.length - padding)
    .attr("height", d => d * 5)
    .attr("fill", d => { return colorGradient(d)});

  // modify the texts:
  bars.select("text")
    .text(d => d)
    .attr("text-anchor", "middle")
    .attr(
      "x",
      (d, i) =>
        i * (width / randomData.length) +
        (width / randomData.length - padding) / 2
    )
    .attr("y", d => height - d * 5 + 12)
    .style("font-family", "sans-serif")
    .style("font-size", 12)
    .style("fill", "#ffffff");

Вот что в sandox форме.

Немного дальнейшего объяснения: selection.select() выбирает первый соответствующий элемент для каждого элемента в selection - чтобы мы могли выбрать единственный прямоугольник в каждом родительском элементе g (который мы добавляем при вводе родительского элемента) с bars.select("rect") выше D3 передает родительские данные ребенку при добавлении выше. Примечание. Если бы у нас были вложенные данные (несколько столбцов или текстов на элемент массива данных), нам бы потребовались вложенные циклы ввода / вывода / обновления .

...