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) || "");
报错
- x(d: IData)处:
Type '[number, number]' is not assignable to type 'IData'- 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