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): 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; }