feat(source): ContribPie material contribution pie chart component

This commit is contained in:
zty 2026-07-07 21:28:38 -04:00
parent 561c5158f1
commit 1a4b752a01
2 changed files with 69 additions and 0 deletions

View File

@ -0,0 +1,59 @@
export interface PieItem {
name: string
rate: number
isSrc: boolean
}
interface Props {
items: PieItem[]
colors?: string[]
}
const DEFAULT_COLORS = ['#C0392B', '#E67E22', '#E0A100', '#159A86', '#2E7FC1', '#6FB1E0', '#9AA7B4']
const R = 74
const CX = 90
const CY = 90
function arc(cx: number, cy: number, r: number, a0: number, a1: number) {
const p = (a: number) => [cx + r * Math.cos((a - 90) * Math.PI / 180), cy + r * Math.sin((a - 90) * Math.PI / 180)]
const [x0, y0] = p(a0)
const [x1, y1] = p(a1)
const large = a1 - a0 > 180 ? 1 : 0
return `M ${cx} ${cy} L ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1} Z`
}
export default function ContribPie({ items, colors = DEFAULT_COLORS }: Props) {
const total = items.reduce((s, it) => s + it.rate, 0)
if (!items.length || total <= 0) {
return <div className="muted" style={{ padding: 20 }}></div>
}
const color = (i: number) => colors[Math.min(i, colors.length - 1)]
let acc = 0
const slices = items.map((it, i) => {
const a0 = acc / total * 360
acc += it.rate
const a1 = acc / total * 360
return { d: arc(CX, CY, R, a0, a1), c: color(i), pct: it.rate / total * 100 }
})
return (
<div className="pie-wrap">
<svg className="pie-svg" width={180} height={180} viewBox="0 0 180 180">
{slices.map((s, i) => (
<path key={i} d={s.d} fill={s.c} stroke="#fcfcfb" strokeWidth={2}>
<title>{items[i].name}{(items[i].rate * 100).toFixed(1)}%</title>
</path>
))}
</svg>
<div className="pie-legend">
{items.map((it, i) => (
<div className="pie-lg" key={it.name + i}>
<i style={{ background: color(i) }} />
<span className="pie-lg-nm">{it.name}{it.isSrc && <b className="pie-src"> ()</b>}</span>
<span className="pie-lg-v">{(it.rate * 100).toFixed(1)}%</span>
</div>
))}
</div>
</div>
)
}

View File

@ -255,3 +255,13 @@
.rbc-lg{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted,#8a8f98);} .rbc-lg{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted,#8a8f98);}
.rbc-lg i{width:11px;height:11px;border-radius:3px;display:inline-block;} .rbc-lg i{width:11px;height:11px;border-radius:3px;display:inline-block;}
.rbc-lg-dash{width:14px;height:0;border-top:1px dashed #C0392B;border-radius:0;} .rbc-lg-dash{width:14px;height:0;border-top:1px dashed #C0392B;border-radius:0;}
/* 材料贡献饼图 */
.pie-wrap{display:flex;gap:22px;align-items:center;flex-wrap:wrap;padding:6px 2px 2px;}
.pie-svg{flex-shrink:0;}
.pie-legend{display:flex;flex-direction:column;gap:8px;min-width:200px;flex:1;}
.pie-lg{display:flex;align-items:center;gap:9px;font-size:13px;}
.pie-lg i{width:12px;height:12px;border-radius:3px;flex-shrink:0;}
.pie-lg-nm{flex:1;color:var(--ink,#1f2328);}
.pie-src{color:#C0392B;font-weight:700;}
.pie-lg-v{font-weight:700;color:var(--ink,#1f2328);}