原创

react + TS + d3.js 实现曲线图 报错问题解决

1. 结构

1.1 整体结构

// lemon
import React, { useRef, useState, useEffect, useReducer } from "react";
import * as d3 from "d3";
interface Data {}
const SimpleLine: React.FC = () => {
  const ref = useRef<SVGSVGElement>(null);
  useEffect(() => {});
  return (
    <>
      <svg ref={ref}></svg>
    </>
  );
};
export { SimpleLine };

1.2 SimpleLine函数组件

在SimpleLine组件中的svgDOM结构中进行SVG图表的绘制。由于绘制SVG图表中会产生副作用(直接操作DOM结构),所以这部分操作将在Effect Hook中执行。

我们想使用d3.js在svg标签中绘制数据图表,所以需要先获取到组件渲染后的真实DOM。这里使用ref来访问。

在TS环境下使用useRef()时,需要使用泛型声明需要保存的DOM类型。这里想要保存svg,对应的泛型为SVGSVGElement。可能不完全正确。

1.3 interface Data{}

这个接口用于声明在d3.js中需要操作的数据元(datum),可视化的数据集由许许多多的datum组成。

使用d3.js可视化的操作中,我们往往使用的是datum中的某个数据,使用Data来标识datum的类型(属性)可以避免误操作、报错、无法获取属性。

2. 代码

2.1 完整代码

import React, { useRef, useEffect, useState } from "react";
import * as d3 from "d3";
interface IData {
  date: Date;
  value: number;
}
const SimpleLine: React.FC = () => {
  const ref = useRef<SVGSVGElement>(null);
  const [width, setWidth] = useState(1600);
  const [height, setHeight] = useState(800);
  const [margin, setMargin] = useState({
    top: 160,
    right: 80,
    bottom: 160,
    left: 80,
  });
  const innerWidth = width - margin.left - margin.right;
  const innerHeight = height - margin.top - margin.bottom;
  useEffect(() => {
    const svgSelection = d3.select(ref.current);
    var data: IData[] = [
      { date: new Date(2007, 3, 24), value: 93.24 },
      { date: new Date(2007, 3, 25), value: 95.35 },
      { date: new Date(2007, 3, 26), value: 98.84 },
      { date: new Date(2007, 3, 27), value: 99.92 },
      { date: new Date(2007, 3, 30), value: 99.8 },
      { date: new Date(2007, 4, 1), value: 99.47 },
    ];
    data.forEach((d) => {
      console.log(d.date);
    });
    const xValue = (d: IData): Date => d.date;
    const yValue = (d: IData): number => d.value;
    let xScale: d3.ScaleTime<number, number>,
      yScale: d3.ScaleLinear<number, number>;
    let datesKeys: Date[];
    const g = svgSelection
      .append("g")
      .attr("id", "maingroup")
      .attr("transform", `translate(${margin.left}, ${margin.top})`);
    const init = (data: IData[]) => {
      svgSelection.attr("width", width).attr("height", height);
      let minX = d3.min(data, xValue) || new Date();
      let maxX = d3.max(data, xValue) || new Date();
      let minY = d3.min(data, yValue) || 0;
      let maxY = d3.max(data, yValue) || 0;
      xScale = d3
        .scaleTime()
        .domain([minX, maxX])
        .range([0, innerWidth])
        .nice();
      yScale = d3
        .scaleLinear()
        .domain([maxY, minY])
        .range([0, innerHeight])
        .nice();
      datesKeys = Array.from(new Set(data.map((d) => d.date)));
      // Adding axes
      const xAxis = d3
        .axisBottom(xScale)
        .tickValues(Array.from(new Set(data.map((e) => e.date))))
        .tickSize(-innerHeight);
      const yAxis = d3.axisLeft(yScale).tickSize(-innerWidth);
      const xAxisGroup = g
        .append("g")
        .call(xAxis)
        .attr("transform", `translate(0, ${innerHeight})`);
      const yAxisGroup = g.append("g").call(yAxis);
      g.selectAll(".tick text").attr("font-size", "1em");
      g.append("path").attr("id", "alterPath");
    };
    const renderUpdate = (data: IData[]) => {
      const line = d3
        .line<IData>()
        .x((d: IData) => {
          return xScale(xValue(d)) || 0;
        })
        .y((d: IData) => {
          return yScale(yValue(d)) || 0;
        })
        .curve(d3.curveCardinal.tension(0.5));
      // lineEmpty is typically used for the first animation that raise the line up;
      const lineEmpty = d3
        .line<IData>()
        .x((d: IData) => {
          return xScale(xValue(d)) || 0;
        })
        .y((d: IData) => {
          return yScale(d3.min(data, yValue) || 0) || 0;
        })
        .curve(d3.curveCardinal.tension(0.5));
      // .curve(d3.curveCardinal.tension(0.5));
      const maingroup = d3.select("#maingroup");
      // maingroup
      //   .append("path")
      //   .attr("d", line(data) || "")
      //   .attr("stroke", "black")
      //   .attr("fill", "none");
      const pathUpdate = maingroup.selectAll(".datacurve").data(data);
      const pathEnter = pathUpdate
        .enter()
        .append("path")
        .attr("class", "datacurve")
        .attr("fill", "none")
        .attr("stroke", "steelblue")
        .attr("stroke-width", 2.5)
        .attr("d", line(data) || "");
      pathUpdate
        .merge(d3.selectAll(".datacurve"))
        .transition()
        .duration(2000)
        .ease(d3.easeLinear)
        .attr("d", lineEmpty(data) || "");
      console.log(line(data));
      console.log(lineEmpty(data));
    };
    init(data);
    renderUpdate(data);
  });
  return (
    <>
      <svg ref={ref}></svg>
    </>
  );
};
export { SimpleLine };

2.2 报错解决

2.2.1 domain()
let yScale = d3.scaleLinear().domain([d3. min(data, yValue), d3.max(data, yValue)]).range([0, innerHeight]).nice();

报错
Type 'number | undefined' is not assignable to type 'NumberValue'.
Type 'undefined' is not assignable to type 'NumberValue'.ts(2322)

说参数有可能是undefined,不合理。

let minY = d3.min(data, yValue) || 0;
let maxY = d3.max(data, yValue) || 0;

let yScale = d3.scaleLinear().domain([minY, maxY)]).range([0, innerHeight]).nice();
2.2.2 line()

 const line = d3
        .line()
        .x((d: IData) => {
          return xScale(xValue(d)) || 0;
        })
        .y((d: IData) => {
          return yScale(yValue(d)) || 0;
        })
        .curve(d3.curveCardinal.tension(0.5));

 const pathEnter = pathUpdate
        .enter()
        .append("path")
        .attr("class", "datacurve")
        .attr("fill", "none")
        .attr("stroke", "steelblue")
        .attr("stroke-width", 2.5)
        .attr("d", line(data) || "");

报错

  1. x(d: IData)处:
    Type '[number, number]' is not assignable to type 'IData'
  2. attr("d", line(data) || ""):
    Argument of type 'IData[]' is not assignable to parameter of type '[number, number][]'

都是说类型不匹配

const line = d3
        .line<IData>()
        .x((d: IData) => {
          return xScale(xValue(d)) || 0;
        })
        .y((d: IData) => {
          return yScale(yValue(d)) || 0;
        })
        .curve(d3.curveCardinal.tension(0.5));


 const pathEnter = pathUpdate
        .enter()
        .append("path")
        .attr("class", "datacurve")
        .attr("fill", "none")
        .attr("stroke", "steelblue")
        .attr("stroke-width", 2.5)
        .attr("d", line(data) || "");

d3.line()用于生成一个line生成器,由于生成器不知道它将来会调用什么样的datum来生成line,所以使用泛型在编译阶段来限制类型。

2.2.3 selection.merge()

const pathUpdate = maingroup.selectAll(".datacurve").data(data);

const pathEnter = pathUpdate
        .enter()
        .append("path")
        .attr("class", "datacurve")
        .attr("fill", "none")
        .attr("stroke", "steelblue")
        .attr("stroke-width", 2.5)
        .attr("d", line(data) || "");

pathUpdate
        .merge(pathEnter)
        .transition()
        .duration(2000)
        .ease(d3.easeLinear)
        .attr("d", lineEmpty(data) || "");

报错
pathEnter和pathUpdate泛型不相同,无法合并
pathEnter: d3.Selection<SVGPathElement, IData, d3.BaseType, unknown>
pathUpdate: d3.Selection<d3.BaseType, IData, d3.BaseType, unknown>

pathUpdate
        .merge(d3.selectAll(".datacurve"))
        .transition()
        .duration(2000)
        .ease(d3.easeLinear)
        .attr("d", lineEmpty(data) || "");

这里我们使用selectAll来获得泛型相同的DOM

结束

拥抱TS是一个漫长且疼苦的过程。使用TS来操作第三方库时,单单熟悉API是不够的,还要了解设计的思想(声明的泛型和类型)避免陷入类型陷阱。

整个DBUG的过程足足有一下午,尝试过查看官方文档、接口声明,最终将问题定位到泛型上。

原文地址:https://www.cnblogs.com/xiaoxu-xmy/p/13762730.html
GitHub: https://github.com/lemon-Xu/Learning-d3.-Js
作者: lemon

原文地址:https://www.cnblogs.com/xiaoxu-xmy/p/13762730.html