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

BarChart动态键场景下默认Tooltip无数据显示问题

Nivo ResponsiveBar 工具提示为空问题

问题描述

使用Nivo的ResponsiveBar组件开发柱状图时,除默认Tooltip外其余功能均正常。鼠标悬停在柱状条上时,Tooltip无任何数据展示。图表采用动态生成的键作为图例,图例显示正常,但相同的键在Tooltip中无法正常匹配数据。

问题现象

柱状条通过动态生成的键能正常渲染,但Tooltip为空;同一组键对应的图例显示完全正常。

代码片段

图表配置

<ResponsiveBar
      data={barData}
      keys={chartKeys}
      indexBy="year"
      margin={{ top: 50, right: 130, bottom: 50, left: 60 }}
      padding={0.3}
      valueScale={{ type: "linear" }}
      indexScale={{ type: "band", round: true }}
      colors={{ scheme: "nivo" }}
      borderColor={{
        from: "color",
        modifiers: [["darker", 1.6]],
      }}
      axisTop={null}
      axisRight={null}
      axisBottom={{
        tickSize: 5,
        tickPadding: 5,
        tickRotation: 0,
        legend: "year",
        legendPosition: "middle",
        legendOffset: 32,
      }}
      axisLeft={{
        tickSize: 5,
        tickPadding: 5,
        tickRotation: 0,
        legend: "credito_vigente",
        legendPosition: "middle",
        legendOffset: -40,
      }}
      enableTotals={true}
      labelSkipWidth={12}
      labelSkipHeight={12}
      labelTextColor={{ from: "color", modifiers: [["darker", 1.6]] }}
      legends={[
        {
          dataFrom: "keys",
          anchor: "bottom-right",
          direction: "column",
          justify: false,
          translateX: 120,
          translateY: 0,
          itemsSpacing: 1, // 缩小项间距
          itemWidth: 80, // 更小的宽度
          itemHeight: 15, // 更小的高度
          itemDirection: "left-to-right",
          itemOpacity: 0.85,
          symbolSize: 12, // 更小的符号尺寸
          effects: [
            {
              on: "hover",
              style: {
                itemOpacity: 1,
              },
            },
          ],
        },
      ]}
      role="application"
      ariaLabel="Nivo bar chart demo"
      barAriaLabel={(e) =>
        `${e.id}: ${e.formattedValue} in year: ${e.indexValue}`
      }
    />

数据处理逻辑

const [dataByYear, setDataByYear] = useState<AñoConSusActividades[]>([]);
  const [chartKeys, setChartKeys] = useState<string[]>([]); // 存储唯一键的状态
  const transformAndSetData = useCallback(
    (data: PresupuestoPorProgramaUNI[]) => {
      const groupedData = data.reduce((acc, curr) => {
        // 在累积数据中查找已有的年份条目
        const yearEntry = acc.find(
          (entry) => entry.year === curr.impacto_presupuestario_anio
        );

        // 构造新的活动对象
        const activity = {
          actividad_desc: curr.actividad_desc,
          credito_vigente: Number(curr.credito_vigente.toFixed(0)),
        };

        if (yearEntry) {
          // 如果该年份的条目已存在,将新活动推入该年份的活动数组
          yearEntry.activities.push(activity);
        } else {
          // 如果该年份的条目不存在,创建新条目并加入当前活动
          acc.push({
            year: curr.impacto_presupuestario_anio,
            activities: [activity],
          });
        }
        return acc;
      }, [] as AñoConSusActividades[]);

      // 更新分组数据状态
      setDataByYear(groupedData);

      // 根据新数据更新图表键
      setChartKeys(extractUniqueKeys(groupedData));
    },
    [setDataByYear, setChartKeys]
  ); // 包含这些依赖项

  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch("/api/presupuestoUni");
        const jsonData = await response.json();
        const dataRes: PresupuestoPorProgramaUNI[] = jsonData.dataRes;
        console.log("获取的数据:", dataRes); // 打印原始获取的数据
        transformAndSetData(dataRes);
      } catch (error) {
        console.error("获取数据失败:", error);
      }
    }

    fetchData();
  }, [transformAndSetData]); // 仅当transformAndSetData变化时重新运行,由于useCallback,这种情况不会发生

  const extractUniqueKeys = (data: AñoConSusActividades[]): string[] => {
    const allKeys = new Set(
      data.flatMap((yearGroup) =>
        yearGroup.activities.map((activity) => activity.actividad_desc)
      )
    );
    return Array.from(allKeys);
  };

  // 考虑是否可以直接在transformAndSetData中处理这部分逻辑
  const barData = dataByYear.map((yearGroup) => {
    // 定义带有索引签名的累加器
    // 告诉TypeScript该对象可以有任意数量的属性,属性键为字符串,属性值为数字
    const activitiesAccumulator: Record<string, number> = {};

    yearGroup.activities.forEach((activity) => {
      activitiesAccumulator[activity.actividad_desc] = activity.credito_vigente;
    });

    return {
      year: yearGroup.year.toString(), // 与ResponsiveBar组件中的'indexBy'对应
      ...activitiesAccumulator,
    };
  });

问题演示

存在演示视频展示Tooltip为空的现象。


解决方案

1. 标准化键值匹配

动态生成的键可能存在空格、特殊字符或大小写不一致的问题,导致Tooltip无法匹配对应数据。对键进行标准化处理:

// 新增键标准化函数
const normalizeKey = (key: string) => key.trim().toLowerCase();

// 修改extractUniqueKeys
const extractUniqueKeys = (data: AñoConSusActividades[]): string[] => {
  const allKeys = new Set(
    data.flatMap((yearGroup) =>
      yearGroup.activities.map((activity) => normalizeKey(activity.actividad_desc))
    )
  );
  return Array.from(allKeys);
};

// 修改barData生成逻辑
const barData = dataByYear.map((yearGroup) => {
  const activitiesAccumulator: Record<string, number> = {};
  yearGroup.activities.forEach((activity) => {
    const normalizedKey = normalizeKey(activity.actividad_desc);
    activitiesAccumulator[normalizedKey] = activity.credito_vigente;
  });
  return {
    year: yearGroup.year.toString(),
    ...activitiesAccumulator,
  };
});

2. 校验数据有效性

确保barData中所有属性值都是有效数字,避免NaN或undefined导致Tooltip无数据:

// 修改活动对象构造逻辑
const activity = {
  actividad_desc: curr.actividad_desc,
  credito_vigente: isNaN(Number(curr.credito_vigente)) ? 0 : Number(curr.credito_vigente.toFixed(0)),
};

3. 确保Tooltip功能启用

显式添加enableTooltip={true}到ResponsiveBar组件,避免默认配置被意外覆盖:

<ResponsiveBar
  // ...其他配置
  enableTooltip={true}
/>

4. 同步状态更新时机

用useMemo包裹barData生成逻辑,确保数据与dataByYear同步更新:

const barData = useMemo(() => {
  return dataByYear.map((yearGroup) => {
    const activitiesAccumulator: Record<string, number> = {};
    yearGroup.activities.forEach((activity) => {
      activitiesAccumulator[activity.actividad_desc] = activity.credito_vigente;
    });
    return {
      year: yearGroup.year.toString(),
      ...activitiesAccumulator,
    };
  });
}, [dataByYear]);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 17:45:00