zoukankan      html  css  js  c++  java
  • 原创

    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

  • 相关阅读:
    Leetcode 238. Product of Array Except Self
    Leetcode 103. Binary Tree Zigzag Level Order Traversal
    Leetcode 290. Word Pattern
    Leetcode 205. Isomorphic Strings
    Leetcode 107. Binary Tree Level Order Traversal II
    Leetcode 102. Binary Tree Level Order Traversal
    三目运算符
    简单判断案例— 分支结构的应用
    用switch判断月份的练习
    java基本打印练习《我行我素购物系统》
  • 原文地址:https://www.cnblogs.com/xiaoxu-xmy/p/13762730.html
Copyright © 2011-2022 走看看