首页 > 解决方案 > d3雷达图在React中呈现不一致

问题描述

我的图表昨晚正在渲染。现在,它有时会完全渲染,有时会部分渲染,或者通常根本不渲染。我在开发工具中没有错误。我将我的 d3 代码复制到一个简单的 javascript 文件中,其中只有最新的 d3 脚本,并且它可以工作。任何见解将不胜感激。提前非常感谢。

如果有帮助,请回购:https ://github.com/EvanPoe/d3-chart-to-react

import React from "react";
import * as d3 from "d3";

function RadarChart() {

  
  /////////////////////////////////////////////////////////
  /////////////// The Radar Chart Function ////////////////
  /////////////// Written by Nadieh Bremer ////////////////
  ////////////////// VisualCinnamon.com ///////////////////
  /////////// Inspired by the code of alangrafu ///////////
  /////////////////////////////////////////////////////////

  /* Radar chart design created by Nadieh Bremer - VisualCinnamon.com */

  //////////////////////// Set-Up //////////////////////////////

  var margin = { top: 100, right: 100, bottom: 100, left: 100 },
    width = Math.min(700, window.innerWidth - 10) - margin.left - margin.right,
    height = Math.min(
      width,
      window.innerHeight - margin.top - margin.bottom - 20
    );

  ////////////////////////// Data //////////////////////////////

  var data = [
    [
      //Tsircon
      { axis: "BALLISTIC", value: 65 },
      { axis: "Glide", value: 39 },
      { axis: "20G TURN", value: 43 },
      { axis: "FISH HOOK DIVE", value: 50 },
      { axis: "PHUGOID MOTION", value: 60 },
      { axis: "ENERGY SCRUB", value: 52 },
    ],
    //   [
    //     //Kinzhal
    //     { axis: "BALLISTIC", value: 0.27 },
    //     { axis: "Glide", value: 0.16 },
    //     { axis: "20G TURN", value: 0.35 },
    //     { axis: "FISH HOOK DIVE", value: 0.13 },
    //     { axis: "PHUGOID MOTION", value: 0.2 },
    //     { axis: "ENERGY SCRUB", value: 0.13 },
    //   ],
    //   [
    //     //Example Missile Overlay
    //     { axis: "BALLISTIC", value: 0.26 },
    //     { axis: "Glide", value: 0.1 },
    //     { axis: "20G TURN", value: 0.3 },
    //     { axis: "FISH HOOK DIVE", value: 0.14 },
    //     { axis: "PHUGOID MOTION", value: 0.22 },
    //     { axis: "ENERGY SCRUB", value: 0.04 },
    //   ],
  ];
  //////////////////// Draw the Chart //////////////////////////

  var color = d3.scaleBand().range(["#739CC4", "#CC333F", "#00A0B0"]);

  var radarChartOptions = {
    w: width,
    h: height,
    margin: margin,
    maxValue: 0.5,
    levels: 5,
    roundStrokes: false,
    color: color,
  };
  //Call function to draw the Radar chart
  RadarChart(".radarChart", data, radarChartOptions);

  function RadarChart(id, data, options) {
    var cfg = {
      w: 600, //Width of the circle
      h: 600, //Height of the circle
      margin: { top: 20, right: 20, bottom: 20, left: 20 }, //The margins of the SVG
      levels: 3, //How many levels or inner circles should there be drawn
      maxValue: 0, //What is the value that the biggest circle will represent
      labelFactor: 1.25, //How much farther than the radius of the outer circle should the labels be placed
      wrapWidth: 60, //The number of pixels after which a label needs to be given a new line
      opacityArea: 0.35, //The opacity of the area of the blob
      dotRadius: 4, //The size of the colored circles of each blog
      opacityCircles: 0.1, //The opacity of the circles of each blob
      strokeWidth: 2, //The width of the stroke around each blob
      roundStrokes: false, //If true the area and stroke will follow a round path (cardinal-closed)
      color: d3.scaleBand(d3.schemeCategory10), //Color function
    };

    //Put all of the options into a variable called cfg
    if ("undefined" !== typeof options) {
      for (var i in options) {
        if ("undefined" !== typeof options[i]) {
          cfg[i] = options[i];
        }
      } //for i
    } //if

    //If the supplied maxValue is smaller than the actual one, replace by the max in the data
    var maxValue = Math.max(
      cfg.maxValue,
      d3.max(data, function (i) {
        return d3.max(
          i.map(function (o) {
            return o.value;
          })
        );
      })
    );

    var allAxis = data[0].map(function (i, j) {
        return i.axis;
      }), //Names of each axis
      total = allAxis.length, //The number of different axes
      radius = Math.min(cfg.w / 2, cfg.h / 2), //Radius of the outermost circle
      Format = d3.format("%"), //Percentage formatting
      angleSlice = (Math.PI * 2) / total; //The width in radians of each "slice"

    //Scale for the radius
    var rScale = d3.scaleLinear().range([0, radius]).domain([0, maxValue]);

    //////////// Create the container SVG and g /////////////

    //Remove whatever chart with the same id/class was present before
    d3.select(id).select("svg").remove();

    //Initiate the radar chart SVG
    var svg = d3
      .select(id)
      .append("svg")
      .attr("width", cfg.w + cfg.margin.left + cfg.margin.right)
      .attr("height", cfg.h + cfg.margin.top + cfg.margin.bottom)
      .attr("class", "radar" + id);
    //Append a g element
    var g = svg
      .append("g")
      .attr(
        "transform",
        "translate(" +
          (cfg.w / 2 + cfg.margin.left) +
          "," +
          (cfg.h / 2 + cfg.margin.top) +
          ")"
      );

    ////////// Glow filter ///////////

    //Filter for the outside glow
    var filter = g.append("defs").append("filter").attr("id", "glow"),
      feGaussianBlur = filter
        .append("feGaussianBlur")
        .attr("stdDeviation", "2.5")
        .attr("result", "coloredBlur"),
      feMerge = filter.append("feMerge"),
      feMergeNode_1 = feMerge.append("feMergeNode").attr("in", "coloredBlur"),
      feMergeNode_2 = feMerge.append("feMergeNode").attr("in", "SourceGraphic");

    /////////////// Draw the Circular grid //////////////////

    //Wrapper for the grid & axes
    var axisGrid = g.append("g").attr("class", "axisWrapper");

    //Draw the background circles
    axisGrid
      .selectAll(".levels")
      .data(d3.range(1, cfg.levels + 1).reverse())
      .enter()
      .append("circle")
      .attr("class", "gridCircle")
      .attr("r", function (d, i) {
        return (radius / cfg.levels) * d;
      })
      .style("fill", "#CDCDCD")
      .style("stroke", "#CDCDCD")
      .style("fill-opacity", cfg.opacityCircles)
      .style("filter", "url(#glow)");

    //Text indicating at what % each level is
    axisGrid
      .selectAll(".axisLabel")
      .data(d3.range(1, cfg.levels + 1).reverse())
      .enter()
      .append("text")
      .attr("class", "axisLabel")
      .attr("x", 4)
      .attr("y", function (d) {
        return (-d * radius) / cfg.levels;
      })
      .attr("dy", "0.4em")
      .style("font-size", "10px")
      .attr("fill", "#737373")
      .text(function (d, i) {
        return Format((maxValue * d) / cfg.levels);
      });

    //////////////////// Draw the axes //////////////////////

    //Create the straight lines radiating outward from the center
    var axis = axisGrid
      .selectAll(".axis")
      .data(allAxis)
      .enter()
      .append("g")
      .attr("class", "axis");
    //Append the lines
    axis
      .append("line")
      .attr("x1", 0)
      .attr("y1", 0)
      .attr("x2", function (d, i) {
        return rScale(maxValue * 1.1) * Math.cos(angleSlice * i - Math.PI / 2);
      })
      .attr("y2", function (d, i) {
        return rScale(maxValue * 1.1) * Math.sin(angleSlice * i - Math.PI / 2);
      })
      .attr("class", "line")
      .style("stroke", "white")
      .style("stroke-width", "2px");

    //Append the labels at each axis
    axis
      .append("text")
      .attr("class", "legend")
      .style("font-size", "11px")
      .attr("text-anchor", "middle")
      .attr("dy", "0.35em")
      .attr("x", function (d, i) {
        return (
          rScale(maxValue * cfg.labelFactor) *
          Math.cos(angleSlice * i - Math.PI / 2)
        );
      })
      .attr("y", function (d, i) {
        return (
          rScale(maxValue * cfg.labelFactor) *
          Math.sin(angleSlice * i - Math.PI / 2)
        );
      })
      .text(function (d) {
        return d;
      })
      .call(wrap, cfg.wrapWidth);

    svg
      .append("text")
      .attr("x", width / 4.8)
      .attr("y", 75 - margin.top / 2)
      .attr("text-anchor", "middle")
      .style("font-size", "24px")
      .style("font-weight", "bold")
      .style("fill", "white")
      .text("MANEUVERS");

    svg
      .append("text")
      .attr("x", width / 4.2)
      .attr("y", 95 - margin.top / 2)
      .attr("text-anchor", "middle")
      .style("font-size", "16px")
      .style("fill", "white")
      .text("initiate: % ground track");

    svg
      .append("text")
      .attr("x", width / 3.6)
      .attr("y", 115 - margin.top / 2)
      .attr("text-anchor", "middle")
      .style("font-size", "16px")
      .style("fill", "#8CD9FF")
      .text("% variability");

    svg
      .append("text")
      .attr("x", width / 8)
      .attr("y", 135 - margin.top / 2)
      .attr("text-anchor", "middle")
      .style("font-size", "16px")
      .style("fill", "white")
      .text("sample:");

    svg
      .append("text")
      .attr("x", width / 3.4)
      .attr("y", 135 - margin.top / 2)
      .attr("text-anchor", "middle")
      .style("font-size", "16px")
      .style("fill", "#76ADDB")
      .text("% trajectories");

    ///////////// Draw the radar chart blobs ////////////////

    //The radial line function
    var radarLine = d3
      .lineRadial()
      .curve(d3.curveLinearClosed)
      .radius(function (d) {
        return rScale(d.value);
      })
      .angle(function (d, i) {
        return i * angleSlice;
      });
    if (cfg.roundStrokes) {
      radarLine.curve(d3.curveCardinalClosed);
    }

    //Create a wrapper for the blobs
    var blobWrapper = g
      .selectAll(".radarWrapper")
      .data(data)
      .enter()
      .append("g")
      .attr("class", "radarWrapper");

    //Append the backgrounds
    blobWrapper
      .append("path")
      .attr("class", "radarArea")
      .attr("d", function (d, i) {
        return radarLine(d);
      })
      .style("fill", function (d, i) {
        return cfg.color(i);
      })
      .style("fill-opacity", cfg.opacityArea)
      .on("mouseover", function (d, i) {
        //Dim all blobs
        d3.selectAll(".radarArea")
          .transition()
          .duration(200)
          .style("fill-opacity", 0.1);
        //Bring back the hovered over blob
        d3.select(this).transition().duration(200).style("fill-opacity", 0.7);
      })
      .on("mouseout", function () {
        //Bring back all blobs
        d3.selectAll(".radarArea")
          .transition()
          .duration(200)
          .style("fill-opacity", cfg.opacityArea);
      });

    //Create the outlines
    blobWrapper
      .append("path")
      .attr("class", "radarStroke")
      .attr("d", function (d, i) {
        return radarLine(d);
      })
      .style("stroke-width", cfg.strokeWidth + "px")
      .style("stroke", function (d, i) {
        return cfg.color(i);
      })
      .style("fill", "none")
      .style("filter", "url(#glow)");

    //Append the circles
    blobWrapper
      .selectAll(".radarCircle")
      .data(function (d, i) {
        return d;
      })
      .enter()
      .append("circle")
      .attr("class", "radarCircle")
      .attr("r", cfg.dotRadius)
      .attr("cx", function (d, i) {
        return rScale(d.value) * Math.cos(angleSlice * i - Math.PI / 2);
      })
      .attr("cy", function (d, i) {
        return rScale(d.value) * Math.sin(angleSlice * i - Math.PI / 2);
      })
      .style("fill", function (d, i, j) {
        return cfg.color(j);
      })
      .style("fill-opacity", 0.8);

    //////// Append invisible circles for tooltip ///////////

    //Wrapper for the invisible circles on top
    var blobCircleWrapper = g
      .selectAll(".radarCircleWrapper")
      .data(data)
      .enter()
      .append("g")
      .attr("class", "radarCircleWrapper");

    //Append a set of invisible circles on top for the mouseover pop-up
    blobCircleWrapper
      .selectAll(".radarInvisibleCircle")
      .data(function (d, i) {
        return d;
      })
      .enter()
      .append("circle")
      .attr("class", "radarInvisibleCircle")
      .attr("r", cfg.dotRadius * 1.5)
      .attr("cx", function (d, i) {
        return rScale(d.value) * Math.cos(angleSlice * i - Math.PI / 2);
      })
      .attr("cy", function (d, i) {
        return rScale(d.value) * Math.sin(angleSlice * i - Math.PI / 2);
      })
      .style("fill", "none")
      .style("pointer-events", "all")
      .on("mouseover", function (d, i) {
        let newX = parseFloat(d3.select(this).attr("cx")) - 10;
        let newY = parseFloat(d3.select(this).attr("cy")) - 10;

        tooltip
          .attr("x", newX)
          .attr("y", newY)
          .text(Format(d.value))
          .transition()
          .duration(200)
          .style("opacity", 1);
      })
      .on("mouseout", function () {
        tooltip.transition().duration(200).style("opacity", 0);
      });

    //Set up the small tooltip for when you hover over a circle
    var tooltip = g.append("text").attr("class", "tooltip").style("opacity", 0);

    /////////////////// Helper Function /////////////////////

    //Taken from http://bl.ocks.org/mbostock/7555321
    //Wraps SVG text
    function wrap(text, width) {
      text.each(function () {
        var text = d3.select(this),
          words = text.text().split(/\s+/).reverse(),
          word,
          line = [],
          lineNumber = 0,
          lineHeight = 1.4, // ems
          y = text.attr("y"),
          x = text.attr("x"),
          dy = parseFloat(text.attr("dy")),
          tspan = text
            .text(null)
            .append("tspan")
            .attr("x", x)
            .attr("y", y)
            .attr("dy", dy + "em");

        while ((word = words.pop())) {
          line.push(word);
          tspan.text(line.join(" "));
          if (tspan.node().getComputedTextLength() > width) {
            line.pop();
            tspan.text(line.join(" "));
            line = [word];
            tspan = text
              .append("tspan")
              .attr("x", x)
              .attr("y", y)
              .attr("dy", ++lineNumber * lineHeight + dy + "em")
              .text(word);
          }
        }
      });
    } //wrap
  } //RadarChart
  return (
    <div>
      <h1>Hi, I'm the radar chart</h1>
      <svg className="radarChart container"></svg>
    </div>
  );
}
export default RadarChart;

package.json 依赖项

    "d3": "^6.7.0",
    "d3-scale": "^3.3.0",
    "gh-pages": "^3.1.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",

标签: reactjsd3.js

解决方案


RadarChart只有在正确安装组件后,您才能在组件内运行 D3 代码:

  1. 添加useRef钩子并将引用绑定到SVG:
const svgRef = useRef(); 
...
return (
  <div>
    <h1>Hi, I'm the radar chart</h1>
    <svg ref={svgRef} className="radarChart container" ></svg>
  </div>
);
  1. 添加useEffect依赖svgRef
useEffect(() => {
  if (svgRef.current) {
    const svg = d3.select(svgRef.current);
    drawChart(svg);
  }
}, [svgRef]);
  1. 将所有 D3 代码放入drawChart
const drawChart = svg => {
  ...
  svg.selectAll("*").remove();

  //Initiate the radar chart SVG
  svg
    .attr("width", cfg.w + cfg.margin.left + cfg.margin.right)
    .attr("height", cfg.h + cfg.margin.top + cfg.margin.bottom)
  ...
}

看看它在沙箱中工作


推荐阅读