|
|
@@ -137,7 +137,7 @@ export default function Sparkline({
|
|
|
if (points[i].value > points[maxIdx].value) maxIdx = i;
|
|
|
}
|
|
|
if (minIdx === maxIdx) return null;
|
|
|
- return { min: points[minIdx], max: points[maxIdx] };
|
|
|
+ return { min: points[minIdx], max: points[maxIdx], minIdx, maxIdx };
|
|
|
}, [points, extrema?.show]);
|
|
|
|
|
|
const fmtExtrema = extrema?.formatter ?? yFormatter;
|
|
|
@@ -145,120 +145,126 @@ export default function Sparkline({
|
|
|
const maxColor = extrema?.maxColor ?? DEFAULT_MAX_COLOR;
|
|
|
|
|
|
return (
|
|
|
- <ResponsiveContainer width="100%" height={height} className="sparkline-svg">
|
|
|
- <AreaChart data={points} margin={{ top: 6, right: 6, bottom: showAxes ? 14 : 4, left: showAxes ? 4 : 4 }}>
|
|
|
- <defs>
|
|
|
- <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
|
|
|
- <stop offset="0%" stopColor={stroke} stopOpacity={fillOpacity} />
|
|
|
- <stop offset="100%" stopColor={stroke} stopOpacity={0} />
|
|
|
- </linearGradient>
|
|
|
- </defs>
|
|
|
- {showGrid && (
|
|
|
- <CartesianGrid stroke="rgba(128, 128, 140, 0.35)" strokeDasharray="3 4" vertical={false} />
|
|
|
- )}
|
|
|
- <XAxis
|
|
|
- dataKey="label"
|
|
|
- hide={!showAxes}
|
|
|
- tick={{ fontSize: 10, fill: 'var(--ant-color-text-tertiary)' }}
|
|
|
- axisLine={false}
|
|
|
- tickLine={false}
|
|
|
- interval={0}
|
|
|
- ticks={xTickIndexes?.map((i) => points[i]?.label).filter(Boolean) as string[] | undefined}
|
|
|
- />
|
|
|
- <YAxis
|
|
|
- domain={yDomain}
|
|
|
- hide={!showAxes}
|
|
|
- tick={{ fontSize: 10, fill: 'var(--ant-color-text-tertiary)' }}
|
|
|
- axisLine={false}
|
|
|
- tickLine={false}
|
|
|
- tickFormatter={yFormatter}
|
|
|
- ticks={yTicks}
|
|
|
- width={48}
|
|
|
- />
|
|
|
- {showTooltip && (
|
|
|
- <Tooltip
|
|
|
- cursor={{ stroke: 'var(--ant-color-border)', strokeDasharray: '2 4' }}
|
|
|
- contentStyle={{
|
|
|
- background: 'var(--ant-color-bg-elevated)',
|
|
|
- border: '1px solid var(--ant-color-border-secondary)',
|
|
|
- borderRadius: 6,
|
|
|
- fontSize: 12,
|
|
|
- padding: '6px 10px',
|
|
|
- boxShadow: '0 4px 14px rgba(0, 0, 0, 0.12)',
|
|
|
- }}
|
|
|
- labelStyle={{ color: 'var(--ant-color-text-tertiary)', marginBottom: 4, fontSize: 11 }}
|
|
|
- itemStyle={{ color: 'var(--ant-color-text)', padding: 0, fontWeight: 500 }}
|
|
|
- formatter={(v) => [fmtTooltip(Number(v) || 0), '']}
|
|
|
- labelFormatter={(label) => (tooltipLabelFormatter ? tooltipLabelFormatter(String(label)) : String(label))}
|
|
|
- separator=""
|
|
|
+ <div className="sparkline-container">
|
|
|
+ {extremaPoints && (
|
|
|
+ <div className="sparkline-extrema" aria-hidden="true">
|
|
|
+ <span className="extrema-item" style={{ color: maxColor }}>
|
|
|
+ ▲ {fmtExtrema(extremaPoints.max.value)}
|
|
|
+ </span>
|
|
|
+ <span className="extrema-item" style={{ color: minColor }}>
|
|
|
+ ▼ {fmtExtrema(extremaPoints.min.value)}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <ResponsiveContainer width="100%" height={height} className="sparkline-svg">
|
|
|
+ <AreaChart
|
|
|
+ data={points}
|
|
|
+ margin={{
|
|
|
+ top: showAxes ? 14 : 6,
|
|
|
+ right: showAxes ? 12 : 6,
|
|
|
+ bottom: showAxes ? 26 : 4,
|
|
|
+ left: 4,
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <defs>
|
|
|
+ <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
|
|
|
+ <stop offset="0%" stopColor={stroke} stopOpacity={fillOpacity} />
|
|
|
+ <stop offset="100%" stopColor={stroke} stopOpacity={0} />
|
|
|
+ </linearGradient>
|
|
|
+ </defs>
|
|
|
+ {showGrid && (
|
|
|
+ <CartesianGrid stroke="rgba(128, 128, 140, 0.35)" strokeDasharray="3 4" vertical={false} />
|
|
|
+ )}
|
|
|
+ <XAxis
|
|
|
+ dataKey="label"
|
|
|
+ hide={!showAxes}
|
|
|
+ tick={{ fontSize: 10, fill: 'var(--ant-color-text-tertiary)' }}
|
|
|
+ axisLine={false}
|
|
|
+ tickLine={false}
|
|
|
+ tickMargin={14}
|
|
|
+ interval={0}
|
|
|
+ ticks={xTickIndexes?.map((i) => points[i]?.label).filter(Boolean) as string[] | undefined}
|
|
|
/>
|
|
|
- )}
|
|
|
- {referenceLines?.map((rl, idx) => (
|
|
|
- <ReferenceLine
|
|
|
- key={`ref-${idx}-${rl.y}`}
|
|
|
- y={rl.y}
|
|
|
- stroke={rl.color || stroke}
|
|
|
- strokeDasharray={rl.dash || '5 4'}
|
|
|
- strokeWidth={1.4}
|
|
|
- label={rl.label ? {
|
|
|
- value: rl.label,
|
|
|
- position: 'insideTopRight',
|
|
|
- fill: rl.color || stroke,
|
|
|
- fontSize: 10,
|
|
|
- fontWeight: 600,
|
|
|
- } : undefined}
|
|
|
- ifOverflow="extendDomain"
|
|
|
+ <YAxis
|
|
|
+ domain={yDomain}
|
|
|
+ hide={!showAxes}
|
|
|
+ tick={{ fontSize: 10, fill: 'var(--ant-color-text-tertiary)', dx: -4 }}
|
|
|
+ axisLine={false}
|
|
|
+ tickLine={false}
|
|
|
+ tickMargin={8}
|
|
|
+ tickFormatter={yFormatter}
|
|
|
+ ticks={yTicks}
|
|
|
+ width={56}
|
|
|
/>
|
|
|
- ))}
|
|
|
- {extremaPoints && (
|
|
|
- <>
|
|
|
- <ReferenceDot
|
|
|
- x={extremaPoints.max.label}
|
|
|
- y={extremaPoints.max.value}
|
|
|
- r={4.5}
|
|
|
- fill={maxColor}
|
|
|
- stroke="var(--ant-color-bg-elevated)"
|
|
|
- strokeWidth={2}
|
|
|
- label={{
|
|
|
- value: `▲ ${fmtExtrema(extremaPoints.max.value)}`,
|
|
|
- position: 'top',
|
|
|
- fontSize: 10.5,
|
|
|
- fill: maxColor,
|
|
|
- fontWeight: 600,
|
|
|
- offset: 8,
|
|
|
+ {showTooltip && (
|
|
|
+ <Tooltip
|
|
|
+ cursor={{ stroke: 'var(--ant-color-border)', strokeDasharray: '2 4' }}
|
|
|
+ contentStyle={{
|
|
|
+ background: 'var(--ant-color-bg-elevated)',
|
|
|
+ border: '1px solid var(--ant-color-border-secondary)',
|
|
|
+ borderRadius: 6,
|
|
|
+ fontSize: 12,
|
|
|
+ padding: '6px 10px',
|
|
|
+ boxShadow: '0 4px 14px rgba(0, 0, 0, 0.12)',
|
|
|
}}
|
|
|
- ifOverflow="extendDomain"
|
|
|
+ labelStyle={{ color: 'var(--ant-color-text-tertiary)', marginBottom: 4, fontSize: 11 }}
|
|
|
+ itemStyle={{ color: 'var(--ant-color-text)', padding: 0, fontWeight: 500 }}
|
|
|
+ formatter={(v) => [fmtTooltip(Number(v) || 0), '']}
|
|
|
+ labelFormatter={(label) => (tooltipLabelFormatter ? tooltipLabelFormatter(String(label)) : String(label))}
|
|
|
+ separator=""
|
|
|
/>
|
|
|
- <ReferenceDot
|
|
|
- x={extremaPoints.min.label}
|
|
|
- y={extremaPoints.min.value}
|
|
|
- r={4.5}
|
|
|
- fill={minColor}
|
|
|
- stroke="var(--ant-color-bg-elevated)"
|
|
|
- strokeWidth={2}
|
|
|
- label={{
|
|
|
- value: `▼ ${fmtExtrema(extremaPoints.min.value)}`,
|
|
|
- position: 'bottom',
|
|
|
- fontSize: 10.5,
|
|
|
- fill: minColor,
|
|
|
+ )}
|
|
|
+ {referenceLines?.map((rl, idx) => (
|
|
|
+ <ReferenceLine
|
|
|
+ key={`ref-${idx}-${rl.y}`}
|
|
|
+ y={rl.y}
|
|
|
+ stroke={rl.color || stroke}
|
|
|
+ strokeDasharray={rl.dash || '5 4'}
|
|
|
+ strokeWidth={1.4}
|
|
|
+ label={rl.label ? {
|
|
|
+ value: rl.label,
|
|
|
+ position: 'insideTopRight',
|
|
|
+ fill: rl.color || stroke,
|
|
|
+ fontSize: 10,
|
|
|
fontWeight: 600,
|
|
|
- offset: 8,
|
|
|
- }}
|
|
|
+ } : undefined}
|
|
|
ifOverflow="extendDomain"
|
|
|
/>
|
|
|
- </>
|
|
|
- )}
|
|
|
- <Area
|
|
|
- type="monotone"
|
|
|
- dataKey="value"
|
|
|
- stroke={stroke}
|
|
|
- strokeWidth={strokeWidth}
|
|
|
- fill={`url(#${gradId})`}
|
|
|
- dot={false}
|
|
|
- activeDot={showMarker ? { r: markerRadius, fill: stroke, strokeWidth: 0 } : false}
|
|
|
- isAnimationActive={false}
|
|
|
- />
|
|
|
- </AreaChart>
|
|
|
- </ResponsiveContainer>
|
|
|
+ ))}
|
|
|
+ {extremaPoints && (
|
|
|
+ <>
|
|
|
+ <ReferenceDot
|
|
|
+ x={extremaPoints.max.label}
|
|
|
+ y={extremaPoints.max.value}
|
|
|
+ r={4.5}
|
|
|
+ fill={maxColor}
|
|
|
+ stroke="var(--ant-color-bg-elevated)"
|
|
|
+ strokeWidth={2}
|
|
|
+ ifOverflow="extendDomain"
|
|
|
+ />
|
|
|
+ <ReferenceDot
|
|
|
+ x={extremaPoints.min.label}
|
|
|
+ y={extremaPoints.min.value}
|
|
|
+ r={4.5}
|
|
|
+ fill={minColor}
|
|
|
+ stroke="var(--ant-color-bg-elevated)"
|
|
|
+ strokeWidth={2}
|
|
|
+ ifOverflow="extendDomain"
|
|
|
+ />
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ <Area
|
|
|
+ type="monotone"
|
|
|
+ dataKey="value"
|
|
|
+ stroke={stroke}
|
|
|
+ strokeWidth={strokeWidth}
|
|
|
+ fill={`url(#${gradId})`}
|
|
|
+ dot={false}
|
|
|
+ activeDot={showMarker ? { r: markerRadius, fill: stroke, strokeWidth: 0 } : false}
|
|
|
+ isAnimationActive={false}
|
|
|
+ />
|
|
|
+ </AreaChart>
|
|
|
+ </ResponsiveContainer>
|
|
|
+ </div>
|
|
|
);
|
|
|
}
|