如何用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%以内的部分和溢出部分,以此实现颜色区分:
- 拆分数值:将原始数值拆分为
baseValue(最多100)和overflowValue(超过100的部分) - 分别配置两个RadialBar:一个对应baseValue用常规颜色,另一个对应overflowValue用指定渐变
- 调整角度和半径:确保两个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
相关产品推荐
相关产品推荐

