iter(g276): 新增迷你趋势线组件
This commit is contained in:
@@ -83,3 +83,4 @@ export { codeBlock } from './codeBlock.js';
|
||||
export { signaturePad } from './signaturePad.js';
|
||||
export { loading } from './loading.js';
|
||||
export { relativeTime } from './relativeTime.js';
|
||||
export { sparkline } from './sparkline.js';
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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;
|
||||
}
|
||||
@@ -1087,3 +1087,13 @@ export interface RelativeTimeProps {
|
||||
fallback?: string; // 时间非法时的占位文案;缺省「—」
|
||||
ariaLabel?: string; // 可访问名(如「下单时间」),让读屏知道这个时间是什么
|
||||
}
|
||||
|
||||
/*** 迷你趋势线入参 ***/
|
||||
export interface SparklineProps {
|
||||
values: number[]; // 数值序列(按时间顺序);非有限数会被剔除
|
||||
width?: number; // 宽度(px);缺省 120
|
||||
height?: number; // 高度(px);缺省 32
|
||||
area?: boolean; // 是否填充折线下方面积;缺省 true
|
||||
marker?: boolean; // 是否标记末点;缺省 true(一眼看出当前值位置)
|
||||
ariaLabel?: string; // 可访问名;缺省「趋势图」(趋势无法只靠图形传达,请把结论写进旁边文案)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user