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
相关产品推荐
相关产品推荐

