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

如何用Shadcn Rechart RadialBarChart实现溢出部分视觉差异化?

问题描述

我用shadcn/ui实现了百分比展示功能,参考了官方的RadialBarChart示例。目前基础功能已完成,但需要实现数值溢出时的视觉差异化:当数值超过100%(比如115%),100%的部分保留常规颜色,溢出的15%部分使用渐变背景linear-gradient(128.22deg, #E76E50 31.34%, #18181B 71.24%)。希望仅通过该库原生实现,官方文档没有相关说明,现有代码如下:

import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ChartConfig, ChartContainer } from '@/components/ui/chart';

const chartConfig = {} satisfies ChartConfig;

export function RadialChart() {
  const value = 120; // Example value over 100%
  const baseBarSize = 12;
  const overflowBarSize = 16;
  const barSize = value > 100 ? overflowBarSize : baseBarSize;

  const chartData = [
    {
      browser: 'safari',
      value: value,
      fill: 'var(--chart-1)',
    },
  ];

  const endAngle = -90 + (value / 100) * 360;

  return (
    <Card className='flex flex-col'>
      <CardHeader className='items-center pb-0'>
        <CardTitle>Radial Chart - Text</CardTitle>
      </CardHeader>
      <CardContent className='flex-1 pb-0'>
        <ChartContainer config={chartConfig} className='mx-auto aspect-square max-h-[250px]'>
          <RadialBarChart
            data={chartData}
            startAngle={-90}
            endAngle={endAngle}
            innerRadius={80}
            outerRadius={110}
          >
            <PolarGrid
              gridType='circle'
              radialLines={false}
              stroke='none'
              className='first:fill-muted last:fill-background'
              polarRadius={[86, 74]}
            />
            <RadialBar
              dataKey='value'
              background
              cornerRadius={10}
              // @ts-expect-error BarSize old version
              barSize={barSize}
            />
            <PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
              <Label
                content={({ viewBox }) => {
                  if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
                    return (
                      <text
                        x={viewBox.cx}
                        y={viewBox.cy}
                        textAnchor='middle'
                        dominantBaseline='middle'
                      >
                        <tspan
                          x={viewBox.cx}
                          y={viewBox.cy}
                          className='fill-foreground text-4xl font-bold'
                        >
                          {chartData[0].value.toLocaleString()}%
                        </tspan>
                        <tspan
                          x={viewBox.cx}
                          y={(viewBox.cy || 0) + 24}
                          className='fill-muted-foreground'
                        >
                          Club
                        </tspan>
                      </text>
                    );
                  }
                }}
              />
            </PolarRadiusAxis>
          </RadialBarChart>
        </ChartContainer>
      </CardContent>
    </Card>
  );
}

效果示例:

  • 无溢出图表:无溢出图表
  • 溢出图表:溢出图表
解决方案

shadcn/ui的图表组件基于Recharts实现,我们可以通过添加两个RadialBar组件分别渲染100%以内的部分和溢出部分,以此实现颜色区分:

  1. 拆分数值:将原始数值拆分为baseValue(最多100)和overflowValue(超过100的部分)
  2. 分别配置两个RadialBar:一个对应baseValue用常规颜色,另一个对应overflowValue用指定渐变
  3. 调整角度和半径:确保两个Bar的位置衔接自然

修改后的代码如下:

import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ChartConfig, ChartContainer } from '@/components/ui/chart';
import { RadialBarChart, RadialBar, PolarGrid, PolarRadiusAxis, Label } from 'recharts';

const chartConfig = {} satisfies ChartConfig;

export function RadialChart() {
  const value = 120; // Example value over 100%
  const baseBarSize = 12;
  const overflowBarSize = 16;
  const barSize = value > 100 ? overflowBarSize : baseBarSize;

  // 拆分数值:基础部分(最多100)和溢出部分
  const baseValue = Math.min(value, 100);
  const overflowValue = Math.max(value - 100, 0);

  // 准备两组数据,分别对应基础和溢出部分
  const baseData = [{ value: baseValue, fill: 'var(--chart-1)' }];
  const overflowData = [{ value: overflowValue }];

  // 计算总角度和溢出部分的起始角度
  const totalEndAngle = -90 + (value / 100) * 360;
  const overflowStartAngle = 270; // 100%对应的角度是270度(-90+360)

  return (
    <Card className='flex flex-col'>
      <CardHeader className='items-center pb-0'>
        <CardTitle>Radial Chart - Text</CardTitle>
      </CardHeader>
      <CardContent className='flex-1 pb-0'>
        <ChartContainer config={chartConfig} className='mx-auto aspect-square max-h-[250px]'>
          {/* 基础部分的RadialBarChart渲染100%以内的内容 */}
          <RadialBarChart
            data={baseData}
            startAngle={-90}
            endAngle={baseValue === 100 ? overflowStartAngle : -90 + (baseValue / 100) * 360}
            innerRadius={80}
            outerRadius={110}
          >
            <PolarGrid
              gridType='circle'
              radialLines={false}
              stroke='none'
              className='first:fill-muted last:fill-background'
              polarRadius={[86, 74]}
            />
            <RadialBar
              dataKey='value'
              background={value <= 100} // 只有非溢出时显示背景
              cornerRadius={10}
              barSize={barSize}
            />
            {/* 中心文本只渲染一次 */}
            <PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
              <Label
                content={({ viewBox }) => {
                  if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
                    return (
                      <text
                        x={viewBox.cx}
                        y={viewBox.cy}
                        textAnchor='middle'
                        dominantBaseline='middle'
                      >
                        <tspan
                          x={viewBox.cx}
                          y={viewBox.cy}
                          className='fill-foreground text-4xl font-bold'
                        >
                          {value.toLocaleString()}%
                        </tspan>
                        <tspan
                          x={viewBox.cx}
                          y={(viewBox.cy || 0) + 24}
                          className='fill-muted-foreground'
                        >
                          Club
                        </tspan>
                      </text>
                    );
                  }
                }}
              />
            </PolarRadiusAxis>
          </RadialBarChart>

          {/* 溢出部分的RadialBarChart,仅当有溢出时渲染 */}
          {overflowValue > 0 && (
            <RadialBarChart
              data={overflowData}
              startAngle={overflowStartAngle}
              endAngle={totalEndAngle}
              innerRadius={80}
              outerRadius={110}
            >
              <RadialBar
                dataKey='value'
                cornerRadius={10}
                barSize={barSize}
                // 使用指定渐变作为填充
                fill='url(#overflowGradient)'
              />
              {/* 定义渐变SVG元素 */}
              <defs>
                <linearGradient
                  id='overflowGradient'
                  gradientTransform='rotate(128.22)'
                >
                  <stop offset='31.34%' stopColor='#E76E50' />
                  <stop offset='71.24%' stopColor='#18181B' />
                </linearGradient>
              </defs>
            </RadialBarChart>
          )}
        </ChartContainer>
      </CardContent>
    </Card>
  );
}

关键说明

  • 拆分数值后,用两个独立的RadialBarChart分别渲染基础和溢出部分,确保颜色互不干扰
  • 溢出部分的渐变通过SVG的<linearGradient>定义,并通过url(#overflowGradient)引用
  • 仅当数值超过100%时才渲染溢出的RadialBarChart,避免不必要的DOM元素
  • 调整角度计算,确保溢出部分从100%的位置(270度)开始衔接

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 09:39:49