无法在 d3 和 NextJs 上显示折线图

Cannot show line graph on d3 and NextJs

提问人:Mendes 提问时间:11/6/2023 更新时间:11/6/2023 访问量:16

问:

我正在使用 d3 和 NextJs 13 编写一个 SpeedGraph 组件。目标是接收速度和时间(epoch)对的数组,绘制一个简单的折线图,显示每个时间戳的速度。

代码如下:

SpeedChart.tsx

"use client"

import { useRef, useEffect } from "react";
import moment from "moment";

import styles from "./SpeedChart.module.css";

import {
    select,
    scaleLinear,
    axisBottom,
    max,
    axisLeft,
    line,
    scaleOrdinal,
    schemeCategory10,
    scaleTime,
} from "d3";

const HEIGHT = 500;
const WIDTH = 500;

const renderChart = (svgRef, data) => {
    let duration = 10;
    let dateFormat = "L LT";

    let height = HEIGHT;
    let width = WIDTH;

    const title = "SPEED CHART GRAPH";

    let margin = {
        top: 10,
        bottom: 10,
        right: 10,
        left: 10
    }

    let innerHeight = height - margin.top - margin.bottom; //480
    let innerWidth = width - margin.left - margin.right; // 480
    
    // Get values from data
    const xValue = (d) => d.date_time;
    const yValue = (d) => d.speed;

    let minDT = 0;
    let maxDT = 0;
    data.map(d => {
        if (d.date_time > maxDT) maxDT = d.date_time;
        if (d.date_time < minDT) minDT = d.date_time;
    })

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

    let xScale = scaleTime()
        .domain([minDT, maxDT])
        .range([0, innerWidth]);

    const yAxis = scaleLinear()
        .domain([0, max(data, yValue)])
        .range([innerHeight, 0])
        .nice();

    const colorScale = scaleOrdinal(schemeCategory10);

    const graph = select(svgRef.current);
    
    graph
        .append("svg")
        .attr("width", innerWidth)
        .attr("height", innerHeight)
        .classed("SpeedChartGraphContainerSvg", true)
        .attr("transform", `translate(${margin.left},${margin.top})`);

    graph.select("svg").append("g");

    graph
        .select("g")
        .append("g")
        .call(axisLeft(yAxis).tickSize(-innerWidth))
        .classed("horizontalLines", true);
    graph
        .select("g")
        .append("g")
        .call(
            axisBottom(xScale).tickFormat((d, i) => {
                return moment(d).format(dateFormat);
            })
        )
        .attr("transform", `translate(0,${innerHeight})`)
        .selectAll("text")
        .attr("transform", "rotate(330)")
        .style("text-anchor", "end");

    let nested = [];
    //nest().key(colorValue).entries(data);

    colorScale.domain(nested.map((d) => d.key));

    const lineGenerator = line()
        .x((d) => xScale(xValue(d)))
        .y((d) => innerHeight - yScale(yValue(d)));

    graph
        .select("g")
        .selectAll(".LinePath")
        .data(nested)
        .enter()
        .append("path")
        .classed("LinePath", true)
        .attr("d", (d) => lineGenerator(d.values))
        .transition()
        .duration(4000)
        .attr("stroke", (d) => colorScale(d.key))
        .style("fill", "none");

    const handleDurationConverter = (value) => {
        let duration = moment.duration(value);
        let x = null;
        if (duration.years())
            x =
                duration.years() +
                "y " +
                Math.floor(duration.asDays()) +
                "d " +
                duration.hours() +
                "h " +
                duration.minutes() +
                "m ";
        else if (Math.floor(duration.asDays()))
            x =
                Math.floor(duration.asDays()) +
                "d " +
                duration.hours() +
                "h " +
                duration.minutes() +
                "m ";
        else if (duration.hours())
            x = duration.hours() + "h " + duration.minutes() + "m ";
        else x = duration.minutes() + "m ";
        return x;
    };

    graph
        .select("g")
        .selectAll("circle")
        .data(data)
        .enter()
        .append("circle")
        .attr("cx", (d) => xScale(xValue(d)))
        .attr("cy", (d) => innerHeight - yScale(yValue(d)))
        .attr("r", 4)
        .attr("fill", (d) => colorScale(d.key))
        .on("mouseenter", (d) => {
            graph
                .select("g")
                .selectAll(".tooltip")
                .data([d])
                .join("text")
                .attr("class", "SpeedChartTooltip")
                .text(
                    `${moment.unix(d.data[0].dateTime / 1000).format("L LT")} - ${duration
                        ? handleDurationConverter(d.data[0].value)
                        : d.data[0].value
                    }`
                )
                .attr("x", (d) => xScale(xValue(d)))
                .attr("y", (d) => innerHeight - yScale(yValue(d)) - 10)
                .style("visibility", "visible");
        })
        .on("mouseleave", (d) => {
            graph.select("g").selectAll(".SpeedChartTooltip").remove();
        });

    graph
        .select("svg")
        .append("text")
        .text(title)
        .classed("title", true)
        .attr("transform", `translate(${innerWidth / 2},-20)`)
        .style("text-anchor", "middle");
};

const SpeedChart = (props) => {
    const svgRef = useRef();

    if (!props.data) return <h3>"No Data"</h3>

    useEffect(() => {
        renderChart(svgRef, props.data);
    }, [svgRef]);

    return (
        <div className={styles.container} ref={svgRef} />
    );
};

export default SpeedChart;

SpeedChart.module.css

.SpeedChartContainer{
    display: flex;
    flex-direction: column;    
    margin: 40px 0px 0px 40px;
}

.SpeedChartGraphContainer{
    flex:3;
}
.SpeedChartGraphContainer svg{
    overflow: visible;
}

.SubtitleContainer {
    padding-top: 50px;
}

.SpeedChartSubtitle{
    align-self: flex-end;
    flex: 1;
}

.SpeedChartGraphContainer .horizontalLines .tick line{
    opacity: 0.2;
}

.LinePath {
    stroke-width: 1;
    stroke-linejoin: round;
    stroke-linecap: round;
}

.title{
    font-size: 16px;
}
.tick text{
    font-size: 12px;
}
.legendBox{
    font-size: 12px;
}
.SpeedChartTooltip {
    position: relative;
    display: inline-block;
    border-bottom: 1px dotted black;
}

我怎么称呼它:

import SpeedChart from "../SpeedChart";
import styles from "./App.module.css";

const App = () => {


    const data = [
        {
            date_time: 1699276312738211946,
            speed: 1.0915E4
        },
        {
            date_time: 1699276313739358704,
            speed: 1.0917E4
        },
        {
            date_time: 1699276314740502972,
            speed: 1.0919E4
        },
        {
            date_time: 1699276315741013020,
            speed: 1.0921E4
        },
        {
            date_time: 1699276316741536807,
            speed: 1.0923E4
        },
        {
            date_time: 1699276317741867170,
            speed: 1.0925E4
        },
        {
            date_time: 1699276318742266805,
            speed: 1.0927E4
        },
        {
            date_time: 1699276319742930549,
            speed: 1.0929E4
        },
        {
            date_time: 1699276320767546107,
            speed: 1.0931E4
        },
        {
            date_time: 1699276321774277296,
            speed: 1.0933E4
        }];


    return (
        <div className={styles.container}>
            <h1><SpeedChart data={data} /></h1>
        </div>)
}

export default App;

这是我在屏幕上看到的:

enter image description here

一个附带问题,useEffect 被调用了两次,所以看到 da d3 将图形加倍。

如何修复图形并使useEffect仅调用一次?

JavaScript d3.js next.js13

评论


答: 暂无答案