You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用D3/SVG实现指定时间轴功能?附React代码求指导

Timeline image

时间轴实现改造方案

核心问题修正点

  • 原代码按数组索引固定排列元素,未根据日期的时间顺序映射到对应坐标
  • 缺少时间轴必备的刻度线、时间标签等基础样式
  • 未利用D3的时间比例尺实现日期与像素坐标的精准转换

完整改造代码

先安装依赖:npm install d3 @types/d3

import React, { useRef, useEffect, useState } from 'react';
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch';
import { scaleTime, axisBottom, select } from 'd3';

const Timeline = () => {
    const svgRef = useRef<SVGSVGElement>(null);
    const [timelineWidth, setTimelineWidth] = useState(800);
    const startDate = new Date(2020, 0, 1);
    const endDate = new Date(2023, 11, 31);
    const numDates = 100;

    // 生成并排序随机日期
    const generateSortedDates = (start: Date, end: Date, count: number) => {
        const dateArray = [];
        const startMillis = start.getTime();
        const endMillis = end.getTime();
        const rangeMillis = endMillis - startMillis;

        for (let i = 0; i < count; i++) {
            const randomMillis = startMillis + Math.random() * rangeMillis;
            dateArray.push(new Date(randomMillis));
        }
        return dateArray.sort((a, b) => a.getTime() - b.getTime());
    };
    const dates = generateSortedDates(startDate, endDate, numDates);

    // 初始化D3时间比例尺与轴
    useEffect(() => {
        const handleResize = () => {
            if (svgRef.current) {
                const width = svgRef.current.parentElement?.clientWidth || 800;
                setTimelineWidth(width);
                renderAxis(width);
            }
        };

        const renderAxis = (width: number) => {
            if (!svgRef.current) return;
            // 清除旧轴
            select(svgRef.current).select('.timeline-axis').remove();
            
            // 创建时间比例尺:将日期范围映射到SVG宽度范围(留左右边距)
            const xScale = scaleTime()
                .domain([startDate, endDate])
                .range([50, width - 50]);
            
            // 创建底部时间轴
            const xAxis = axisBottom(xScale)
                .ticks(12) // 显示12个刻度(约每年3个)
                .tickFormat(d => d.toLocaleDateString('en-US', { year: 'numeric', month: 'short' }));
            
            // 绘制轴
            select(svgRef.current)
                .append('g')
                .attr('class', 'timeline-axis')
                .attr('transform', `translate(0, 60)`)
                .call(xAxis)
                .selectAll('text')
                .style('font-size', '10px')
                .style('fill', '#333');
        };

        window.addEventListener('resize', handleResize);
        handleResize();
        return () => window.removeEventListener('resize', handleResize);
    }, [startDate, endDate]);

    // 生成时间点矩形
    const renderDateRects = () => {
        const xScale = scaleTime()
            .domain([startDate, endDate])
            .range([50, timelineWidth - 50]);
        
        return dates.map((date, idx) => {
            const x = xScale(date);
            return (
                <g key={idx}>
                    {/* 时间点标记 */}
                    <rect
                        x={x - 5}
                        y={20}
                        width={10}
                        height={10}
                        fill="steelblue"
                        rx={2}
                    />
                    {/* 日期标签(可选,避免重叠可隐藏) */}
                    <text
                        x={x}
                        y={40}
                        textAnchor="middle"
                        fill="#333"
                        fontSize="8"
                    >
                        {date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
                    </text>
                </g>
            );
        });
    };

    return (
        <div style={{ width: '100%', overflow: 'hidden', border: '1px solid #eee' }}>
            <TransformWrapper
                initialScale={1}
                minScale={0.5}
                maxScale={3}
                centerZoomedOut={true}
            >
                <TransformComponent>
                    <svg ref={svgRef} width={timelineWidth} height={80}>
                        {/* 时间轴背景线 */}
                        <line
                            x1="50"
                            y1="60"
                            x2={timelineWidth - 50}
                            y2="60"
                            stroke="#ddd"
                            strokeWidth={2}
                        />
                        {/* 时间点元素 */}
                        <g>{renderDateRects()}</g>
                    </svg>
                </TransformComponent>
            </TransformWrapper>
        </div>
    );
};

export default Timeline;

关键说明

  • D3时间比例尺:通过scaleTime将日期范围转换为SVG的像素坐标,确保时间点按实际先后顺序排列
  • react-zoom-pan-pinch配置:设置缩放范围、初始缩放比例,适配长时间轴的拖拽浏览需求
  • 响应式处理:监听容器宽度变化,动态调整时间轴宽度与刻度
  • 样式优化:添加背景线、圆角矩形,调整标签位置避免重叠

内容的提问来源于stack exchange,提问作者BlueSea321

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 15:35:41