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

如何在MUI-X Charts堆叠百分比柱状图中显示正确Tooltip?

解决MUI-X Charts堆叠柱状图Tooltip数值错误问题

问题描述

我正尝试用MUI-X Charts制作堆叠柱状图,数据源是百分比数据,想要把不同剂量强度的数值堆叠展示,不同颜色对应不同剂量强度。目前我通过减去前序值来实现堆叠效果,但这样会导致悬浮Tooltip显示的数值不正确。我可以切换为非堆叠图表,但想知道怎么在保留堆叠效果的同时让Tooltip显示正确数值?

当前代码

styles.module.scss

.root {
  margin-top: 20px;
  margin-bottom:20px;
  :global {
    .MuiChartsAxis-directionY .MuiChartsAxis-label {
      transform: translateX(-5px) !important;
    }
  }
}

DrugEffectiveness.tsx

/**
 * 数据来源:https://www.bmj.com/content/326/7404/1423.long
 */

import React, { type ReactNode } from 'react'
import { BarChart } from '@mui/x-charts/BarChart'
import Typography from '@mui/material/Typography'

import styles from './styles.module.scss'

interface reductionType {
  strength: string
  reduction: number | null
}

interface drugType {
  name: string
  reduction: reductionType[]
}

interface seriesType {
  data: Array<number | null>
  label: string
  stack: string
}

const atorvastatin: drugType = {
  name: 'atorvastatin',
  reduction: [
    { strength: '5mg', reduction: 31 },
    { strength: '10mg', reduction: 37 },
    { strength: '20mg', reduction: 43 },
    { strength: '40mg', reduction: 49 },
    { strength: '80mg', reduction: 55 }
  ]
}

const fluvastatin: drugType = {
  name: 'fluvastatin',
  reduction: [
    { strength: '5mg', reduction: 10 },
    { strength: '10mg', reduction: 15 },
    { strength: '20mg', reduction: 21 },
    { strength: '40mg', reduction: 27 },
    { strength: '80mg', reduction: 33 }
  ]
}
const lovastatin: drugType = {
  name: 'lovastatin',
  reduction: [
    { strength: '5mg', reduction: null },
    { strength: '10mg', reduction: 21 },
    { strength: '20mg', reduction: 29 },
    { strength: '40mg', reduction: 37 },
    { strength: '80mg', reduction: 45 }
  ]
}
const pravastatin: drugType = {
  name: 'pravastatin',
  reduction: [
    { strength: '5mg', reduction: 15 },
    { strength: '10mg', reduction: 20 },
    { strength: '20mg', reduction: 24 },
    { strength: '40mg', reduction: 29 },
    { strength: '80mg', reduction: 33 }
  ]
}
const rosuvastatin: drugType = {
  name: 'rosuvastatin',
  reduction: [
    { strength: '5mg', reduction: 38 },
    { strength: '10mg', reduction: 43 },
    { strength: '20mg', reduction: 48 },
    { strength: '40mg', reduction: 53 },
    { strength: '80mg', reduction: 58 }
  ]
}
const simvastatin: drugType = {
  name: 'simvastatin',
  reduction: [
    { strength: '5mg', reduction: 23 },
    { strength: '10mg', reduction: 27 },
    { strength: '20mg', reduction: 32 },
    { strength: '40mg', reduction: 37 },
    { strength: '80mg', reduction: 42 }
  ]
}

const drugs: drugType[] = [atorvastatin, fluvastatin, lovastatin, pravastatin, rosuvastatin, simvastatin]

const strengths: string[] = simvastatin.reduction.map(s => s.strength)

const series5mg: seriesType = { data: [], stack: 'stack', label: '5mg' }
const series10mg: seriesType = { data: [], stack: 'stack', label: '10mg' }
const series20mg: seriesType = { data: [], stack: 'stack', label: '20mg' }
const series40mg: seriesType = { data: [], stack: 'stack', label: '40mg' }
const series80mg: seriesType = { data: [], stack: 'stack', label: '80mg' }

const getReduction = ({ drug, strength }: { drug: drugType, strength: string }): number | null => {
  const filteredDrug = drug.reduction.filter((drugItem: reductionType) => drugItem.strength === strength)
  return filteredDrug.length > 0
    ? filteredDrug[0].reduction
    : null
}
strengths.forEach((strength: string) => {
  drugs.forEach((drug: drugType) => {
    switch (strength) {
      case '5mg' :
        series5mg.data.push(getReduction({ drug, strength }))
        break
      case '10mg':
        series10mg.data.push(Number(getReduction({ drug, strength })) - Number(getReduction({
          drug,
          strength: '5mg'
        })))
        break
      case '20mg':
        series20mg.data.push(Number(getReduction({ drug, strength })) - Number(getReduction({
          drug,
          strength: '10mg'
        })))
        break
      case '40mg':
        series40mg.data.push(Number(getReduction({ drug, strength })) - Number(getReduction({
          drug,
          strength: '20mg'
        })))
        break
      case '80mg':
        series80mg.data.push(Number(getReduction({ drug, strength })) - Number(getReduction({
          drug,
          strength: '40mg'
        })))
        break
    }
  })
})
const valueFormatter = (val: number | null): string => {
  return val !== null ? val + '%' : ''
}

const DrugEffectiveness = (): ReactNode => (
  <div className={`${styles.root} DrugEffectiveness`}>
    <Typography variant="h2">Percentage reduction in LDL cholesterol</Typography>
    <BarChart
      title="tester"
      width={800}
      slotProps={{ legend: { hidden: true } }}
      height={400}
      xAxis={[{
        scaleType: 'band',
        data: drugs.map((d: drugType) => d.name)
      }]}

      series={[
        { ...series5mg, valueFormatter },
        { ...series10mg, valueFormatter },
        { ...series20mg, valueFormatter },
        { ...series40mg, valueFormatter },
        { ...series80mg, valueFormatter }
      ]}
      yAxis={[{
        label: 'Percentage reduction',
        valueFormatter: (v) => v + '%',
        labelStyle: {
          fontSize: 14

        },
        tickLabelStyle: {
          angle: -45,
          textAnchor: 'end',
          fontSize: 12
        }

      }]}
    />
  </div>
)

export default DrugEffectiveness

解决方案:利用MUI-X内置堆叠+自定义Tooltip

你不需要手动计算差值实现堆叠,MUI-X Charts的BarChart本身支持堆叠功能,只要给每个series设置相同的stack属性即可,同时通过自定义Tooltip读取原始数据展示正确数值。

核心修改步骤

  1. 恢复原始数据:重构series生成逻辑,直接使用各剂量的原始百分比,不再计算差值。
  2. 自定义Tooltip:通过slotProps.tooltip重写Tooltip渲染逻辑,从原始drug数据中获取对应药物和剂量的真实数值。

修改后的关键代码

// 重构series生成,直接使用原始数据
const seriesList: seriesType[] = strengths.map(strength => ({
  data: drugs.map(drug => getReduction({ drug, strength })),
  label: strength,
  stack: 'stack'
}))

// 自定义Tooltip组件
const CustomTooltip = ({ active, payload }: any) => {
  if (active && payload && payload.length) {
    return (
      <div style={{ backgroundColor: 'white', padding: '8px', border: '1px solid #ccc' }}>
        <p>{payload[0].xLabel}</p>
        {payload.map((entry: any) => {
          // 从原始数据中获取真实百分比
          const targetDrug = drugs.find(d => d.name === entry.xLabel);
          const targetReduction = targetDrug?.reduction.find(r => r.strength === entry.series.label);
          const realValue = targetReduction?.reduction;
          return (
            <p key={entry.series.label} style={{ color: entry.color }}>
              {entry.series.label}: {realValue !== null ? `${realValue}%` : 'N/A'}
            </p>
          );
        })}
      </div>
    );
  }
  return null;
};

// 更新BarChart配置
const DrugEffectiveness = (): ReactNode => (
  <div className={`${styles.root} DrugEffectiveness`}>
    <Typography variant="h2">Percentage reduction in LDL cholesterol</Typography>
    <BarChart
      title="tester"
      width={800}
      height={400}
      xAxis={[{
        scaleType: 'band',
        data: drugs.map((d: drugType) => d.name)
      }]}
      series={seriesList.map(s => ({ ...s, valueFormatter }))}
      yAxis={[{
        label: 'Percentage reduction',
        valueFormatter: (v) => v + '%',
        labelStyle: { fontSize: 14 },
        tickLabelStyle: { angle: -45, textAnchor: 'end', fontSize: 12 }
      }]}
      slotProps={{
        legend: { hidden: true },
        tooltip: { content: <CustomTooltip /> }
      }}
    />
  </div>
)

效果说明

  • MUI-X会自动处理堆叠逻辑,同stack的series数值会被累加,展示正确的堆叠高度。
  • 自定义Tooltip直接读取原始数据源的百分比,避免了手动计算差值导致的Tooltip数值错误。
  • 完美保留堆叠视觉效果的同时,Tooltip展示用户需要的真实数据。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 08:50:12