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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 10:06:34