141 lines
5.2 KiB
Plaintext
141 lines
5.2 KiB
Plaintext
---
|
|
type ChartItem = {
|
|
label: string;
|
|
value: number;
|
|
href?: string;
|
|
};
|
|
|
|
interface Props {
|
|
title: string;
|
|
subtitle?: string;
|
|
type: 'pie' | 'line';
|
|
items: ChartItem[];
|
|
totalLabel?: string;
|
|
valueLabel?: string;
|
|
}
|
|
|
|
const { title, subtitle, type, items, totalLabel = '篇文章', valueLabel = '篇' } = Astro.props;
|
|
const colors = ['#3eb8be', '#f2a65a', '#7f9cf5', '#68b684', '#f07f7f', '#b18ce8', '#e0b34f', '#5ca4d3'];
|
|
const safeItems = items.filter((item) => item.value > 0);
|
|
const total = safeItems.reduce((sum, item) => sum + item.value, 0);
|
|
const circumference = 100;
|
|
let offset = 0;
|
|
|
|
const pieItems = safeItems.map((item, index) => {
|
|
const percent = total ? item.value / total : 0;
|
|
const dash = percent * circumference;
|
|
const segment = {
|
|
...item,
|
|
color: colors[index % colors.length],
|
|
dash,
|
|
offset,
|
|
percentText: `${Math.round(percent * 100)}%`,
|
|
};
|
|
offset -= dash;
|
|
return segment;
|
|
});
|
|
|
|
const lineWidth = 640;
|
|
const lineHeight = 240;
|
|
const padding = { top: 24, right: 24, bottom: 42, left: 40 };
|
|
const plotWidth = lineWidth - padding.left - padding.right;
|
|
const plotHeight = lineHeight - padding.top - padding.bottom;
|
|
const maxValue = Math.max(1, ...safeItems.map((item) => item.value));
|
|
const linePoints = safeItems.map((item, index) => {
|
|
const x = safeItems.length === 1 ? padding.left + plotWidth / 2 : padding.left + (index / (safeItems.length - 1)) * plotWidth;
|
|
const y = padding.top + plotHeight - (item.value / maxValue) * plotHeight;
|
|
return { ...item, x, y };
|
|
});
|
|
const points = linePoints.map((point) => `${point.x},${point.y}`).join(' ');
|
|
const areaPoints = linePoints.length
|
|
? `${padding.left},${padding.top + plotHeight} ${points} ${padding.left + plotWidth},${padding.top + plotHeight}`
|
|
: '';
|
|
const labelStep = Math.max(1, Math.ceil(linePoints.length / 6));
|
|
---
|
|
|
|
<section class={`stats-chart stats-chart-${type}`} aria-labelledby={`${type}-chart-title`}>
|
|
<div class="stats-chart-head">
|
|
<h2 id={`${type}-chart-title`}>{title}</h2>
|
|
{subtitle && <p>{subtitle}</p>}
|
|
</div>
|
|
|
|
{type === 'pie' ? (
|
|
<div class="stats-chart-body">
|
|
<svg class="pie-chart" viewBox="0 0 120 120" role="img" aria-label={title}>
|
|
<circle class="pie-chart-track" cx="60" cy="60" r="42" pathLength={circumference}></circle>
|
|
{pieItems.map((item) => (
|
|
<circle
|
|
class="pie-chart-segment"
|
|
cx="60"
|
|
cy="60"
|
|
r="42"
|
|
pathLength={circumference}
|
|
stroke={item.color}
|
|
stroke-dasharray={`${item.dash} ${circumference - item.dash}`}
|
|
stroke-dashoffset={item.offset}
|
|
></circle>
|
|
))}
|
|
<text x="60" y="56" text-anchor="middle" class="pie-chart-total">{total}</text>
|
|
<text x="60" y="72" text-anchor="middle" class="pie-chart-label">{totalLabel}</text>
|
|
</svg>
|
|
|
|
<div class="chart-legend">
|
|
{pieItems.map((item, index) => {
|
|
const content = (
|
|
<>
|
|
<span class="chart-legend-color" style={`--chart-color: ${item.color}`}></span>
|
|
<span class="chart-legend-label">{item.label}</span>
|
|
<strong>{item.value}</strong>
|
|
<span class="chart-legend-percent">{item.percentText}</span>
|
|
</>
|
|
);
|
|
|
|
return item.href ? (
|
|
<a href={item.href} class="chart-legend-item" style={`--item-index: ${index}`}>
|
|
{content}
|
|
</a>
|
|
) : (
|
|
<div class="chart-legend-item" style={`--item-index: ${index}`}>
|
|
{content}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div class="line-chart-wrap">
|
|
<svg class="line-chart" viewBox={`0 0 ${lineWidth} ${lineHeight}`} role="img" aria-label={title} preserveAspectRatio="none">
|
|
<line class="line-chart-axis" x1={padding.left} y1={padding.top + plotHeight} x2={padding.left + plotWidth} y2={padding.top + plotHeight}></line>
|
|
<line class="line-chart-axis" x1={padding.left} y1={padding.top} x2={padding.left} y2={padding.top + plotHeight}></line>
|
|
{[0, 0.5, 1].map((ratio) => (
|
|
<>
|
|
<line
|
|
class="line-chart-grid"
|
|
x1={padding.left}
|
|
y1={padding.top + plotHeight - ratio * plotHeight}
|
|
x2={padding.left + plotWidth}
|
|
y2={padding.top + plotHeight - ratio * plotHeight}
|
|
></line>
|
|
<text class="line-chart-y-label" x={padding.left - 12} y={padding.top + plotHeight - ratio * plotHeight + 4} text-anchor="end">
|
|
{Math.round(maxValue * ratio)}
|
|
</text>
|
|
</>
|
|
))}
|
|
{linePoints.length > 0 && <polygon class="line-chart-area" points={areaPoints}></polygon>}
|
|
{linePoints.length > 0 && <polyline class="line-chart-line" points={points}></polyline>}
|
|
{linePoints.map((point, index) => (
|
|
<>
|
|
<circle class="line-chart-dot" cx={point.x} cy={point.y} r="4"></circle>
|
|
<title>{`${point.label}: ${point.value} ${valueLabel}`}</title>
|
|
{index % labelStep === 0 && (
|
|
<text class="line-chart-x-label" x={point.x} y={lineHeight - 12} text-anchor="middle">
|
|
{point.label}
|
|
</text>
|
|
)}
|
|
</>
|
|
))}
|
|
</svg>
|
|
</div>
|
|
)}
|
|
</section>
|