如何为D3折线图零点上下区域设置纯色区分显示?
问题描述
最近刚上手D3.js,正在把原有Chart.js的图表迁移过来,大部分都成功了,但有个折线图没法实现和Chart.js一致的效果。
在Chart.js里可以通过内置的fill属性设置折线零点上下区域的填充色,代码示例:
...config, fill: { above: '#4E4AFF20', below: '#FF515114', target: 'origin' }, ...config
对应的效果是折线在零点上方的区域用浅蓝填充,下方用浅红填充,线条对应区域也是纯色。但D3.js没有类似内置功能,当前实现的是渐变效果,和预期不符,需要解决两个问题:
- 实现Chart.js那样的零点上下区域分色填充
- 线条和填充区域使用纯色而非渐变过渡
解决方案
D3.js需要手动处理零点上下的路径分割,分别绘制填充区域和线条,核心思路是拆分数据为零点上下的独立分段,为每个分段设置对应纯色,具体实现如下:
修改后的完整代码
import * as d3 from 'd3'; import { useEffect, useRef } from 'react'; interface Data { x: number; y: number; } const width = 350; const height = 117; const zeroPoint = 0; const data: Data[] = [ { x: 0, y: -20 }, { x: 10, y: -20 }, { x: 20, y: -20 }, { x: 40, y: -20 }, { x: 50, y: -20 }, { x: 60, y: -20 }, { x: 70, y: -20 }, { x: 80, y: 0 }, { x: 90, y: 20 }, { x: 100, y: 20 }, { x: 110, y: 20 }, { x: 120, y: 20 }, { x: 130, y: 20 }, { x: 140, y: 20 }, { x: 150, y: 20 } ]; // 拆分数据为零点上下的分段,补充跨零点的交点 const splitDataByZero = (data: Data[], zeroY: number) => { const segments: { data: Data[], type: 'above' | 'below' | 'zero' }[] = []; let currentSegment: Data[] = []; for (let i = 0; i < data.length; i++) { const curr = data[i]; const next = data[i + 1]; currentSegment.push(curr); // 处理跨零点的情况:计算折线与零点线的交点 if (next && ((curr.y < zeroY && next.y > zeroY) || (curr.y > zeroY && next.y < zeroY))) { const t = (zeroY - curr.y) / (next.y - curr.y); const intersectionX = curr.x + t * (next.x - curr.x); const intersection = { x: intersectionX, y: zeroY }; currentSegment.push(intersection); segments.push({ data: [...currentSegment], type: curr.y < zeroY ? 'below' : 'above' }); currentSegment = [intersection]; } // 处理到达零点的分段切换 else if (curr.y === zeroY && next && next.y !== zeroY) { segments.push({ data: [...currentSegment], type: 'zero' }); currentSegment = [curr]; } // 处理分段结束 else if (i === data.length - 1 || (next && ((curr.y >= zeroY && next.y < zeroY) || (curr.y <= zeroY && next.y > zeroY)))) { const type = curr.y > zeroY ? 'above' : curr.y < zeroY ? 'below' : 'zero'; segments.push({ data: [...currentSegment], type }); currentSegment = []; } } return segments; }; // 生成连接到零点线的填充区域路径 const generateAreaPath = (data: Data[], xScale: d3.ScaleLinear<number, number>, yScale: d3.ScaleLinear<number, number>, zeroY: number) => { const areaGenerator = d3.area<Data>() .x(d => xScale(d.x)) .y0(yScale(zeroY)) .y1(d => yScale(d.y)); return areaGenerator(data); }; // 生成线条路径 const generateLinePath = (data: Data[], xScale: d3.ScaleLinear<number, number>, yScale: d3.ScaleLinear<number, number>) => { const lineGenerator = d3.line<Data>() .x(d => xScale(d.x)) .y(d => yScale(d.y)); return lineGenerator(data); }; export const Chart: React.FC = () => { const ref = useRef<SVGSVGElement>(null); const drawZeroLine = (svg: d3.Selection<SVGSVGElement, unknown, null, undefined>, yScale: d3.ScaleLinear<number, number>) => { svg .append('line') .attr('x1', '0') .attr('y1', yScale(zeroPoint)) .attr('x2', width) .attr('y2', yScale(zeroPoint)) .attr('stroke', '#c4c4c4'); }; const createChart = (data: Data[]) => { const svg = d3.select(ref.current!).attr('viewBox', `0 0 ${width} ${height}`); svg.selectAll('*').remove(); const [minX, maxX] = d3.extent(data, d => d.x); const [minY, maxY] = d3.extent(data, d => d.y); const xScale = d3.scaleLinear().domain([minX!, maxX!]).range([0, width]); const yScale = d3.scaleLinear() .domain([minY!, maxY!]) .range([height, 0]); // 拆分数据为零点上下的独立分段 const segments = splitDataByZero(data, zeroPoint); // 绘制填充区域 segments.forEach(segment => { if (segment.type === 'above' || segment.type === 'below') { const fillColor = segment.type === 'above' ? '#4E4AFF20' : '#FF515114'; svg.append('path') .attr('d', generateAreaPath(segment.data, xScale, yScale, zeroPoint)) .attr('fill', fillColor) .attr('stroke', 'none'); } }); // 绘制线条 segments.forEach(segment => { if (segment.type === 'above' || segment.type === 'below') { const strokeColor = segment.type === 'above' ? '#4E4AFF' : '#FF5151'; svg.append('path') .attr('d', generateLinePath(segment.data, xScale, yScale)) .attr('stroke', strokeColor) .attr('stroke-width', 2) .attr('fill', 'none'); } else if (segment.type === 'zero') { svg.append('path') .attr('d', generateLinePath(segment.data, xScale, yScale)) .attr('stroke', '#c4c4c4') .attr('stroke-width', 2) .attr('fill', 'none'); } }); drawZeroLine(svg, yScale); }; useEffect(() => { createChart(data); }, []); return <svg ref={ref} />; };
关键实现细节
- 数据拆分:
splitDataByZero函数遍历原始数据,识别跨零点的分段,通过线性插值计算折线与零点线的交点,将数据拆分为独立的上方/下方/零点分段 - 填充区域:使用
d3.area生成填充路径,y0固定为零点的y坐标,确保填充区域自动连接到零点线 - 纯色设置:每个分段单独设置填充色和线条色,完全匹配Chart.js的分色效果,避免渐变过渡
内容的提问来源于stack exchange,提问作者yourAverageDeveloper
相关产品推荐
相关产品推荐

