d3(React)正负值柱状图全负值场景下X轴定位异常问题
D3(React技术栈)正负值柱状图全负值场景X轴异常修复
问题描述
- 已实现效果:将X轴固定在原点位置,混合正负值数据场景下,正值柱形展示在原点上方、负值柱形展示在原点下方
- 异常表现:数据集全部为负值时,X轴展示位置超出可视区域
- 效果参考:
- 正负值混合正常效果:

- 全负值异常效果:

- 正负值混合正常效果:
- 已尝试无效方案:直接使用画布高度值替代
yScale(0)参与柱形位置、高度计算
现有问题代码
柱状图调用层代码
const DisplayBarChart = (props) => { const data = props.data; const yLabel = ''; const xLabel = ''; const width = 460; const height = 385; const translateX = 60; const translateY = 20; const multicolor = 'true'; const noZeroes = data.length > 0 ? props.data.filter((d) => d.value !== 0) : []; const yMinValue = d3.min(noZeroes, (d) => d.value) - 1; const gridlines = 'false'; const barlabel = 'true'; const textSymbol = '%'; const sign = '+'; return ( <div className={ defaultTheme.bordered } id="peer"> <h2 className="pct-h2-line">{props.title}</h2> {data.length > 0 && <PCTBarChart data={ data } labelx={ xLabel } labely={ yLabel } width={ width } height={ height } allowMulticolor={ multicolor } translateX={ translateX } translateY={ translateY } multicolors={ ['#FF44CC', '#6f6f6f'] } color={ '#009BDF' } YAxisMinValue={ yMinValue } YAxisMaxValue={ 0.5 } gridlines={ gridlines } textSymbol={ textSymbol } barlabel={ barlabel } sign={ sign } /> } </div> ); };
柱状图组件核心代码
/* eslint-disable no-console */ /* eslint-disable max-statements */ import React, { useRef, useEffect } from 'react'; import * as d3 from 'd3'; function PCTbarChart(props) { const width = props.width * 0.75; const height = props.height * 0.65; const data = props.data; const ref = useRef(null); const cache = useRef(props.data); const allowMulticolor = props.allowMulticolor; const multicolors = props.multicolors; const color = props.color; const colorScale = d3.scaleOrdinal().range(multicolors); const yMinValue = props.YAxisMinValue; const yMaxValue = props.YAxisMaxValue; const yAxisTickFormat = props.yAxisTickFormat; useEffect( () => { // defining Axis Scales const xScale = d3.scaleBand().rangeRound([0, width / 1.1]).padding(0.5); const yScale = d3.scaleLinear() .domain([yMinValue, d3.max(data, (d) => d.value) + yMaxValue]) .rangeRound([height, 0]); d3.select(ref.current).selectAll('g').remove(); const group = d3.select(ref.current); const groupWithAxis = group.selectAll('g.Axis').data([0]); const groupWithData = group.selectAll('g.bar').data(data); // initializing X and Y Scales with Values xScale.domain(data.map((d) => d.name)); const groupAxisUpdate = groupWithAxis .enter() .append('g') .attr('class', 'Axis'); const groupWithUpdate = groupWithData .enter() .append('g') .attr('class', 'barGroup'); const bars = groupWithUpdate .append('rect') .merge(groupWithData.select('.bar')); const xAxis = d3.axisBottom(xScale) .tickSize(0); const yAxis = d3.axisLeft(yScale) .tickFormat(yAxisTickFormat); // creating the x axis groupAxisUpdate .append('g') .classed('x-axis', true) .style('font-size', '0px') .attr('transform', `translate(0,${yScale(0)})`) .call(xAxis); groupAxisUpdate .append('g') .classed('y-axis', true) .style('font-size', '5px') .call(yAxis); bars .attr('class', 'bar') .attr('x', (d) => xScale(d.name)) .transition() .duration(950) .attr('y', (d) => (d.value > 0 ? yScale(d.value) : yScale(0))) .attr('width', xScale.bandwidth()) .attr('height', (d) => (d.value > 0 ? Math.abs(yScale(d.value) - yScale(0)) : Math.abs(yScale(0) - yScale(d.value)))) .delay((d, i) => i * 550) .style('fill', (d) => ((allowMulticolor === 'true') ? colorScale(d.value) : color)); // labelling the bars if (props.barlabel === 'true') { groupWithUpdate.append('text') .data(data) .attr('class', 'label') .attr('y', (d) => (d.value > 0 ? yScale(d.value) - 5 : yScale(d.value) + 15)) .attr('x', (d) => xScale(d.name) + 15) .text((d) => { if (d.value > 0) { return `${props.sign}${d.value}${props.textSymbol}`; } return `${d.value}${props.textSymbol}`; }); } cache.current = props.data; }, [props.data] ); return ( <svg width={ props.width } height={ props.height }> <g ref={ ref } transform={ `translate(${props.translateX},${props.translateY})` } /> </svg> ); } export default PCTbarChart;
问题根因
全负值场景下,当前Y轴domain的最大值为数据集最大值 + 固定偏移0.5,由于数据集所有值都为负,计算后的domain最大值仍然小于0,此时yScale(0)会返回小于0的坐标值,直接将X轴平移到SVG可视区域外,导致显示异常。之前直接替换为画布高度的方案没有同步修正Y轴domain边界,无法适配刻度映射逻辑,因此无效。
修复步骤
1. 修正Y轴domain边界,强制0值处于可视区间
计算Y轴上下边界时,保证上界不小于0、下界不大于0,让0点始终落在Y轴的坐标映射范围内:
// 替换原有yScale定义逻辑 const noZeroData = data.filter(d => d.value !== 0); const dataMax = d3.max(data, d => d.value); const dataMin = d3.min(noZeroData, d => d.value); // 强制domain包含0值 const yDomainMax = Math.max(0, dataMax) + yMaxValue; const yDomainMin = Math.min(0, dataMin) - 1; const yScale = d3.scaleLinear() .domain([yDomainMin, yDomainMax]) .rangeRound([height, 0]);
提示:父组件传入YAxisMinValue/YAxisMaxValue时也需要做相同边界判断,避免传入值导致0点跑出可视区
2. 抽离零轴坐标公共变量
提前计算零轴对应的Y坐标,所有依赖零轴位置的逻辑统一复用该值,减少重复计算导致的误差:
const zeroLineY = yScale(0);
3. 替换所有零轴位置引用
将代码中所有用到yScale(0)的位置替换为zeroLineY,同时修正标签位置逻辑,保证全负值场景下标签显示在柱形靠近零轴的一侧:
// X轴平移逻辑修改 .attr('transform', `translate(0,${zeroLineY})`) // 柱形位置、高度逻辑修改 .attr('y', (d) => d.value >= 0 ? yScale(d.value) : zeroLineY) .attr('height', (d) => Math.abs(yScale(d.value) - zeroLineY)) // 标签位置逻辑修改 .attr('y', (d) => d.value >= 0 ? yScale(d.value) - 5 : zeroLineY + 15)
4. 优化柱形颜色映射逻辑
原多色逻辑通过ordinal比例尺按值映射颜色,全负值场景下颜色分配会混乱,改为按值正负分配固定颜色,保证视觉一致性:
.style('fill', (d) => { if (allowMulticolor !== 'true') return color; return d.value >= 0 ? multicolors[0] : multicolors[1]; })
内容的提问来源于stack exchange,提问作者manoj
相关产品推荐
相关产品推荐

