42 lines
2.6 KiB
TypeScript
42 lines
2.6 KiB
TypeScript
import { h } from '@ui-arch/frame';
|
|
import type { SparklineProps } from './types.js';
|
|
|
|
// SVG 命名空间:createElement 不认 svg 标签,必须走 createElementNS
|
|
const NS = 'http://www.w3.org/2000/svg';
|
|
// 缺省尺寸:120×32 塞得进统计卡片的右上角
|
|
const W = 120;
|
|
const H = 32;
|
|
|
|
/*** 建 SVG 元素并批量写属性:svg 的属性都得 setAttribute,写个小工具省掉每处重复 ***/
|
|
function svgEl(tag: string, attrs: Record<string, string | number>): SVGElement {
|
|
const el = document.createElementNS(NS, tag);
|
|
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));
|
|
return el;
|
|
}
|
|
|
|
/*** 迷你趋势线:把一串数值压成一条折线(可选面积填充 + 末点标记),不画坐标轴、不带交互,只表达走向 ***/
|
|
export function sparkline(props: SparklineProps): SVGElement {
|
|
const values = props.values.filter((v) => Number.isFinite(v)); // 脏数据先剔掉,否则整条线会被一个 undefined 拉平
|
|
const w = props.width ?? W;
|
|
const hgt = props.height ?? H;
|
|
const pad = 2; // 内缩 2px:线宽与末点圆点不会被 viewBox 边缘裁掉
|
|
const svg = svgEl('svg', { viewBox: `0 0 ${w} ${hgt}`, width: w, height: hgt, class: 'ua-spark', role: 'img', 'aria-label': props.ariaLabel ?? '趋势图' });
|
|
if (values.length < 2) return svg; // 少于两点没有「趋势」可言:给空图,别画一根假横线
|
|
const min = Math.min(...values);
|
|
const max = Math.max(...values);
|
|
const span = max - min || 1; // 全等值时除零保护:所有点落在中线
|
|
const x = (i: number): number => pad + (i * (w - pad * 2)) / (values.length - 1); // 首末点贴边,中间均分
|
|
const y = (v: number): number => pad + (1 - (v - min) / span) * (hgt - pad * 2); // 值越大越靠上(SVG y 轴向下)
|
|
const points = values.map((v, i) => `${x(i).toFixed(2)},${y(v).toFixed(2)}`).join(' ');
|
|
if (props.area !== false) {
|
|
// 面积:折线两端下探到底边闭合,填充走 currentColor 的低透明度(颜色由 CSS 的 color 决定)
|
|
svg.append(svgEl('polygon', { points: `${points} ${x(values.length - 1).toFixed(2)},${hgt} ${x(0).toFixed(2)},${hgt}`, class: 'ua-spark-area' }));
|
|
}
|
|
svg.append(svgEl('polyline', { points, class: 'ua-spark-line', 'stroke-linejoin': 'round', 'stroke-linecap': 'round' }));
|
|
if (props.marker !== false) {
|
|
// 末点标记:一眼看出当前值落在哪,不用去数最后一段的斜率
|
|
svg.append(svgEl('circle', { cx: x(values.length - 1).toFixed(2), cy: y(values[values.length - 1]).toFixed(2), r: 2, class: 'ua-spark-dot' }));
|
|
}
|
|
return svg;
|
|
}
|