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

Visx Streamgraph自定义类型与数据访问器问题求助

流图数据结构与访问器修正方案

核心问题

你的代码出现路径NaN的原因有三个:

  • 数据结构不匹配visx/Stack组件的预期格式
  • getY0/getY1错误固定取数组索引项,未对应堆叠计算后的边界值
  • keys生成逻辑不符合新数据结构要求

修正步骤

1. 调整数据结构

Stack组件要求输入数据为按时间点分组的对象数组,每个对象包含当前时间t和所有系列的value。如果原始数据是按系列存储的(每个系列为{t: number, value: number}[]),必须先转换格式:

// 原始系列数据类型
type RawSeries = { t: number; value: number }[];
// 转换后的数据类型:每个时间点对应所有系列的value
type TimePoint = { t: number; [key: string]: number };

// 转换函数
const convertToTimePointData = (rawData: RawSeries[]) => {
  // 收集并排序所有时间点
  const allTimes = [...new Set(rawData.flatMap(s => s.map(d => d.t)))].sort((a, b) => a - b);
  return allTimes.map(t => {
    const point: TimePoint = { t };
    rawData.forEach((series, idx) => {
      // 匹配当前时间点的系列值,无数据则补0
      point[`series${idx}`] = series.find(d => d.t === t)?.value || 0;
    });
    return point;
  });
};

2. 修正keys生成逻辑

替换原有的_.range(data[0].length),改用转换后数据的系列字段作为keys:

const keys = Object.keys(processedData[0]).filter(key => key !== 't');

3. 修正比例尺与访问器

Stack组件的x0/x1对应堆叠后的横向边界,y对应每个时间点的垂直位置(由t映射),调整如下:

// 计算时间范围与最大堆叠总和
const tExtent = d3.extent(processedData, d => d.t) as [number, number];
const maxTotalValue = d3.max(processedData, d => keys.reduce((sum, key) => sum + d[key], 0)) || 1;

// 比例尺:t越大,y坐标越靠下
const yScale = scaleLinear({
  domain: tExtent,
  range: [0, height],
});
// x轴对应堆叠后的value总和
const xScale = scaleLinear({
  domain: [0, maxTotalValue],
  range: [0, width],
});

// 修正Stack访问器
const getX0 = (d: [number, number]) => xScale(d[0]);
const getX1 = (d: [number, number]) => xScale(d[1]);
const getY = (_: any, i: number) => yScale(processedData[i].t);

完整修正代码

import { scaleLinear, scaleOrdinal, extent, max } from '@visx/scale';
import { Stack, curveCatmullRom } from '@visx/shape';
import { Group } from '@visx/group';
import { useSpring, animated } from '@react-spring/web';

// 原始系列数据类型
type RawSeries = { t: number; value: number }[];
// 转换后的数据类型
type TimePoint = { t: number; [key: string]: number };

export type StreamGraphProps = {
  data: RawSeries[];
  width: number;
  height: number;
  animate?: boolean;
};

export default function Streamgraph({ data, width, height, animate = true }: StreamGraphProps) {
  if (data.length === 0 || width < 10) return null;

  // 转换数据结构
  const allTimes = [...new Set(data.flatMap(s => s.map(d => d.t)))].sort((a, b) => a - b);
  const processedData: TimePoint[] = allTimes.map(t => {
    const point: TimePoint = { t };
    data.forEach((series, idx) => {
      point[`series${idx}`] = series.find(d => d.t === t)?.value || 0;
    });
    return point;
  });

  // 定义系列keys
  const keys = Object.keys(processedData[0]).filter(key => key !== 't');

  // 计算比例尺
  const tExtent = extent(processedData, d => d.t) as [number, number];
  const maxTotalValue = max(processedData, d => keys.reduce((sum, key) => sum + d[key], 0)) || 1;

  const yScale = scaleLinear({
    domain: tExtent,
    range: [0, height],
  });
  const xScale = scaleLinear({
    domain: [0, maxTotalValue],
    range: [0, width],
  });
  const colorScale = scaleOrdinal({
    domain: keys.map((_, idx) => idx),
    range: ['#ffc409', '#f14702', '#262d97', 'white', '#036ecd', '#9ecadd'],
  });
  const patternScale = scaleOrdinal({
    domain: keys.map((_, idx) => idx),
    range: ['gor', 'gpo', 'gpb', 'gpr', 'gpur', 'gsp', 'gtb'],
  });

  // Stack访问器
  const getX0 = (d: [number, number]) => xScale(d[0]);
  const getX1 = (d: [number, number]) => xScale(d[1]);
  const getY = (_: any, i: number) => yScale(processedData[i].t);

  return (
    <svg width={width} height={height}>
      <GradientPinkBlue id="gpb" />
      <GradientPurpleOrange id="gpo" />
      <GradientOrangeRed id="gor" />
      <GradientPinkRed id="gpr" />
      <GradientPurpleRed id="gpur" />
      <GradientSteelPurple id="gsp" />
      <GradientTealBlue id="gtb" />

      <Group>
        <Stack<TimePoint, string>
          data={processedData}
          keys={keys}
          offset="silhouette"
          order="insideout"
          x0={getX0}
          x1={getX1}
          y={getY}
          curve={curveCatmullRom}
        >
          {({ stacks, path }) =>
            stacks.map((stack, idx) => {
              const pathString = path(stack) || '';
              const tweened = animate ? useSpring({ pathString }) : { pathString };
              const color = colorScale(idx);
              const pattern = patternScale(idx);
              return (
                <g key={`series-${stack.key}`}>
                  <animated.path d={tweened.pathString} fill={color} />
                  <animated.path d={tweened.pathString} fill={`url(#${pattern})`} />
                </g>
              );
            })
          }
        </Stack>
      </Group>
    </svg>
  );
}

关键说明

  • 数据转换是核心:Stack组件仅能处理按维度(时间点)分组的对象数组,无法直接识别按系列存储的数组结构
  • 访问器逻辑需匹配堆叠计算:x0/x1是Stack计算后的堆叠边界,y是时间点对应的垂直坐标,不能直接使用原始t值作为堆叠边界
  • 比例尺适配需求:y轴范围对应时间区间,确保时间推进方向垂直向下;x轴范围对应最大堆叠总和,避免内容溢出

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 02:35:53