d3-v7 数据可视化折线图+悬浮窗

发布于:2025-03-13 ⋅ 阅读:(11) ⋅ 点赞:(0)

#TestD3{
  width: 100%;
  height: 35%;
  position: relative;

  .tooltip {
    opacity: 0;
    position: absolute;
    padding: 0.6em 1em;
    background: #fff;
    text-align: center;
    border: 1px solid #ddd;
    z-index: 10;
    transition: all 0.2s ease-out;
    pointer-events: none;
    transform: translate(-50%, calc(-100% - 14px));
  }

  .tooltip:before {
    content: '';
    position: absolute;
    bottom: 0;
    left: 50%;
    width: 12px;
    height: 12px;
    background: white;
    border: 1px solid #ddd;
    border-top-color: transparent;
    border-left-color: transparent;
    transform: translate(-50%, 50%) rotate(45deg);
    transform-origin: center center;
    z-index: 10;
  }

  .tooltip-range {
    margin-bottom: 0.2em;
    font-weight: 600;
  }
}
import './index.less';
import * as d3 from 'd3';
import { useEffect, useRef} from 'react';
import useAppState from "../../store";

const TestD3 = () => {
    const svgRef = useRef(null); // 用于保存 SVG 元素的引用
    const containerRef = useRef(null); // 用于保存父节点的引用
    const {testData}=useAppState();
    const draw = (width:number, height:number,dataset: any[]) => {
         // 每次绘制前清除旧图表
        d3.select('#TestD3').select('svg').remove();

        let padding = 30;
        const tooltip = d3.select('#tooltip');

        let xScale = d3
            .scaleBand()
            .domain(d3.range(dataset.length))
            .range([padding, width - padding])
            .padding(0) // 设置柱子之间的间隙
            .paddingInner(1); // 设置柱子内部的间隙

        let yScale = d3
            .scaleLinear()
            .domain([0, d3.max(dataset, function (d) {
                return d.value;
            })])
            .range([height - padding, padding]);

        // 创建 SVG 元素并保存引用
        const svg = d3
            .select('#TestD3')
            .append('svg')
            .attr('width', width)
            .attr('height', height)

        svgRef.current = svg.node(); // 保存 SVG 元素的引用

        let line = d3
            .line()
            .x(function (d, i) {
                return xScale(i) + xScale.bandwidth() / 2;
            })
            .y(function (d) {
                return yScale(d.value);
            })
            .curve(d3.curveCardinal);

        svg
            .append('path')
            .datum(dataset)
            .attr('class', 'line')
            .attr('d', line)
            .attr('fill', 'none')
            .attr('stroke', '#69b3a2')
            .attr('stroke-width', '3px');

        let xAxis = d3
            .axisBottom(xScale)
            .tickFormat(function (d, i) {
                return dataset[i].name;
            });

        let yAxis = d3.axisLeft(yScale);

        svg
            .append('g')
            .attr('class', 'x-axis')
            .attr('transform', 'translate(0,' + (height - padding) + ')')
            .call(xAxis);

        svg
            .append('g')
            .attr('class', 'y-axis')
            .attr('transform', 'translate(' + padding + ',0)')
            .call(yAxis);

        // 绘制圆点
        svg
            .selectAll('.circle')
            .data(dataset)
            .enter()
            .append('circle')
            .attr('class', 'circle')
            .attr('cx', function (d, i) {
                return xScale(i) + xScale.bandwidth() / 2;
            })
            .attr('cy', function (d) {
                return yScale(d.value);
            })
            .attr('r', 4)
            .attr('fill', '#ffffff')
            .attr('stroke', '#69b3a2')
            .attr('stroke-width', '3px');

        // 每个区域绘制一个矩形用于触发事件
        let _w = (width - padding * 2) / (dataset.length - 1);
        svg
            .selectAll('.rect')
            .data(dataset)
            .enter()
            .append('rect')
            .attr('class', 'rect')
            .attr('x', function (d, i) {
                return xScale(i) - _w / 2;
            })
            .attr('y', padding)
            .attr('width', _w)
            .attr('height', height - padding * 2)
            .attr('fill', 'transparent')
            .on('mouseover', function (e,d) {
                console.log(e,d)
                tooltip.style('opacity', 1);
                tooltip.select('#range').text(d.name);
                tooltip.select('#count').text(d.value);
                tooltip
                    .style('left', xScale(d.index) + xScale.bandwidth() / 2  + 'px')
                    .style('top', yScale(d.value)  + 'px');
            })
            .on('mouseout', function (d) {
                tooltip.style('opacity', 0);
            });
    };


    useEffect(() => {
        if(containerRef.current){
            const { width, height } = containerRef.current.getBoundingClientRect();
            let dataset = [
                { name: '苹果', value: 50,index:0 },
                { name: '橙子', value: 30,index:1 },
                { name: '香蕉', value: 70,index:2 },
                { name: '核桃', value: 20,index:3 },
                { name: '芒果', value: 60,index:4 },
                { name: '梨子', value: 100,index:5 },
                { name: '菠萝', value: 80,index:6 },
                { name: '葡萄', value: 90,index:7 },
                { name: '草莓', value: 35,index:8 },
                { name: '西瓜', value: 75,index:9 },
                { name: '桃子', value: 55,index:10 },
                { name: '樱桃', value: 25,index:11 },
            ];
            dataset.forEach(function (d) {
                d.value = Math.floor(Math.random() * 80) + 20;
            });
            draw(width,height,dataset);
        }
    }, [testData]);
    return (
        <>
            <div id="TestD3" ref={containerRef}>
                <div id="tooltip" className="tooltip">
                    <div className="tooltip-range">
                        Name: <span id="range"></span>
                    </div>
                    <div className="tooltip-value">
                        Value: <span id="count"></span>
                    </div>
                </div>
            </div>
        </>
    );
};

export default TestD3;