agentcss 0.0.0-stage → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/01-tokens/README.md +236 -0
- package/01-tokens/behavior/dismissable.js +132 -0
- package/01-tokens/behavior/emit.js +76 -0
- package/01-tokens/behavior/focus-return.js +81 -0
- package/01-tokens/behavior/roving.js +152 -0
- package/01-tokens/behavior/typeahead.js +124 -0
- package/01-tokens/focus-ring-demo.html +133 -0
- package/01-tokens/focus-ring.css +102 -0
- package/01-tokens/page.css +244 -0
- package/01-tokens/polyfill.js +116 -0
- package/01-tokens/responsive-demo.html +171 -0
- package/01-tokens/scrollbar-demo.html +194 -0
- package/01-tokens/scrollbar.css +179 -0
- package/01-tokens/theme-toggle.js +103 -0
- package/01-tokens/tokens.css +972 -0
- package/01-tokens/typography.css +156 -0
- package/02-primitives/badge/README.md +71 -0
- package/02-primitives/badge/badge.css +141 -0
- package/02-primitives/badge/demo.html +101 -0
- package/02-primitives/button/README.md +99 -0
- package/02-primitives/button/button.css +353 -0
- package/02-primitives/button/demo.html +200 -0
- package/02-primitives/card/README.md +145 -0
- package/02-primitives/card/card.css +246 -0
- package/02-primitives/card/demo.html +170 -0
- package/02-primitives/choice/README.md +161 -0
- package/02-primitives/choice/choice.css +395 -0
- package/02-primitives/choice/demo.html +277 -0
- package/02-primitives/combobox/README.md +79 -0
- package/02-primitives/combobox/combobox.css +223 -0
- package/02-primitives/combobox/combobox.js +485 -0
- package/02-primitives/combobox/demo.html +109 -0
- package/02-primitives/date-range/README.md +100 -0
- package/02-primitives/date-range/date-range.css +206 -0
- package/02-primitives/date-range/date-range.js +301 -0
- package/02-primitives/date-range/demo.html +104 -0
- package/02-primitives/input/README.md +170 -0
- package/02-primitives/input/demo.html +246 -0
- package/02-primitives/input/input.css +239 -0
- package/02-primitives/popover/README.md +80 -0
- package/02-primitives/popover/demo.html +150 -0
- package/02-primitives/popover/popover.css +187 -0
- package/02-primitives/popover/popover.js +331 -0
- package/02-primitives/progress/README.md +82 -0
- package/02-primitives/progress/demo.html +154 -0
- package/02-primitives/progress/progress.css +172 -0
- package/02-primitives/select/README.md +95 -0
- package/02-primitives/select/demo.html +121 -0
- package/02-primitives/select/select.css +151 -0
- package/02-primitives/select/select.js +549 -0
- package/02-primitives/separator/README.md +63 -0
- package/02-primitives/separator/demo.html +84 -0
- package/02-primitives/separator/separator.css +73 -0
- package/02-primitives/skeleton/README.md +69 -0
- package/02-primitives/skeleton/demo.html +110 -0
- package/02-primitives/skeleton/skeleton.css +96 -0
- package/02-primitives/switch/README.md +65 -0
- package/02-primitives/switch/demo.html +115 -0
- package/02-primitives/switch/switch.css +370 -0
- package/03-patterns/accordion/README.md +121 -0
- package/03-patterns/accordion/accordion.css +453 -0
- package/03-patterns/accordion/accordion.js +369 -0
- package/03-patterns/accordion/demo.html +223 -0
- package/03-patterns/content/README.md +103 -0
- package/03-patterns/content/content.css +286 -0
- package/03-patterns/content/demo.html +88 -0
- package/03-patterns/drawer/README.md +79 -0
- package/03-patterns/drawer/demo.html +101 -0
- package/03-patterns/drawer/drawer.css +129 -0
- package/03-patterns/drawer/drawer.js +258 -0
- package/03-patterns/dropdown/README.md +78 -0
- package/03-patterns/dropdown/demo.html +101 -0
- package/03-patterns/dropdown/dropdown.css +135 -0
- package/03-patterns/dropdown/dropdown.js +451 -0
- package/03-patterns/form-validation/README.md +147 -0
- package/03-patterns/form-validation/demo.html +280 -0
- package/03-patterns/form-validation/form-validation.css +263 -0
- package/03-patterns/list/README.md +150 -0
- package/03-patterns/list/demo.html +225 -0
- package/03-patterns/list/flip.js +201 -0
- package/03-patterns/list/list.css +162 -0
- package/03-patterns/nav/README.md +225 -0
- package/03-patterns/nav/demo.html +131 -0
- package/03-patterns/nav/nav.css +314 -0
- package/03-patterns/nav/nav.js +430 -0
- package/03-patterns/nav/toc.js +111 -0
- package/03-patterns/overlay/README.md +196 -0
- package/03-patterns/overlay/demo.html +241 -0
- package/03-patterns/overlay/overlay.css +319 -0
- package/03-patterns/overlay/overlay.js +563 -0
- package/03-patterns/pagination/README.md +114 -0
- package/03-patterns/pagination/demo.html +165 -0
- package/03-patterns/pagination/pagination.css +195 -0
- package/03-patterns/pagination/pagination.js +395 -0
- package/03-patterns/reveal/README.md +91 -0
- package/03-patterns/reveal/demo.html +106 -0
- package/03-patterns/reveal/reveal.css +93 -0
- package/03-patterns/reveal/reveal.js +172 -0
- package/03-patterns/states/README.md +141 -0
- package/03-patterns/states/demo.html +240 -0
- package/03-patterns/states/states.css +252 -0
- package/03-patterns/tabs/README.md +84 -0
- package/03-patterns/tabs/demo.html +139 -0
- package/03-patterns/tabs/tabs.css +172 -0
- package/03-patterns/tabs/tabs.js +307 -0
- package/03-patterns/tooltip/README.md +96 -0
- package/03-patterns/tooltip/demo.html +78 -0
- package/03-patterns/tooltip/tooltip.css +89 -0
- package/03-patterns/tooltip/tooltip.js +288 -0
- package/03-patterns/tree/README.md +94 -0
- package/03-patterns/tree/demo.html +138 -0
- package/03-patterns/tree/tree.css +113 -0
- package/03-patterns/tree/tree.js +456 -0
- package/04-recipes/analysis-report/README.md +80 -0
- package/04-recipes/analysis-report/analysis-report.css +187 -0
- package/04-recipes/analysis-report/demo.html +274 -0
- package/04-recipes/data-showcase/README.md +45 -0
- package/04-recipes/data-showcase/demo.html +247 -0
- package/04-recipes/detail-page/README.md +93 -0
- package/04-recipes/detail-page/demo.html +55 -0
- package/04-recipes/detail-page/detail-page.css +190 -0
- package/04-recipes/detail-page/full.html +60 -0
- package/04-recipes/detail-page/null.html +60 -0
- package/04-recipes/detail-page/partial.html +60 -0
- package/04-recipes/detail-page/verify-skeleton.py +19 -0
- package/04-recipes/longform/README.md +93 -0
- package/04-recipes/longform/agc-memory.svg +4 -0
- package/04-recipes/longform/content.html +610 -0
- package/04-recipes/longform/content.md +51 -0
- package/04-recipes/longform/longform.html +627 -0
- package/04-recipes/report/README.md +176 -0
- package/04-recipes/report/demo.html +355 -0
- package/04-recipes/report/report.css +364 -0
- package/04-recipes/table/README.md +103 -0
- package/04-recipes/table/demo.html +455 -0
- package/04-recipes/table/table.css +372 -0
- package/04-recipes/tierA-tokens.css +189 -0
- package/09-assets/README.md +74 -0
- package/09-assets/bar/README.md +94 -0
- package/09-assets/bar/bar.css +166 -0
- package/09-assets/bar/bar.js +320 -0
- package/09-assets/bar/demo.html +176 -0
- package/09-assets/echarts-adapter/demo.html +192 -0
- package/09-assets/echarts-adapter/echarts-adapter.js +277 -0
- package/09-assets/model-viewer/README.md +155 -0
- package/09-assets/model-viewer/demo.html +105 -0
- package/09-assets/model-viewer/model-viewer.css +143 -0
- package/09-assets/model-viewer/model-viewer.js +423 -0
- package/09-assets/scientific-plot/README.md +94 -0
- package/09-assets/scientific-plot/demo.html +252 -0
- package/09-assets/scientific-plot/scientific-plot.css +141 -0
- package/09-assets/scientific-plot/scientific-plot.js +610 -0
- package/09-assets/sparkline/README.md +153 -0
- package/09-assets/sparkline/demo.html +163 -0
- package/09-assets/sparkline/sparkline.css +158 -0
- package/09-assets/sparkline/sparkline.js +508 -0
- package/API.md +332 -0
- package/CHANGELOG.md +3116 -0
- package/CODE_OF_CONDUCT.md +64 -0
- package/CONTRIBUTING.md +89 -0
- package/ERRORS.md +1658 -0
- package/LICENSE +21 -0
- package/README.md +491 -2
- package/SECURITY.md +72 -0
- package/START-HERE.md +164 -0
- package/SUPPORT.md +109 -0
- package/adapters/presentation/README.md +113 -0
- package/adapters/presentation/check.js +101 -0
- package/adapters/presentation/deck.schema.json +74 -0
- package/adapters/presentation/to-doc.js +138 -0
- package/ai/START-HERE.md +104 -0
- package/ai/behaviors.json +380 -0
- package/ai/behaviors.schema.json +61 -0
- package/ai/capabilities.json +56 -0
- package/ai/cli.js +387 -0
- package/ai/components.json +1944 -0
- package/ai/components.meta.json +168 -0
- package/ai/components.schema.json +220 -0
- package/ai/contract.schema.json +147 -0
- package/ai/deprecations.json +55 -0
- package/ai/deprecations.schema.json +85 -0
- package/ai/motion-policy.json +22 -0
- package/ai/patch.js +323 -0
- package/ai/patch.schema.json +63 -0
- package/ai/profiles.json +92 -0
- package/ai/tokens.audience.json +55 -0
- package/ai/tokens.audience.schema.json +73 -0
- package/ai/tokens.json +762 -0
- package/ai/tokens.schema.json +58 -0
- package/ai/tokens.tree.json +752 -0
- package/ai/tokens.tree.schema.json +56 -0
- package/ai/validate.js +383 -0
- package/dist/01-tokens/behavior/dismissable.js +60 -0
- package/dist/01-tokens/behavior/emit.js +21 -0
- package/dist/01-tokens/behavior/focus-return.js +31 -0
- package/dist/01-tokens/behavior/roving.js +77 -0
- package/dist/01-tokens/behavior/typeahead.js +61 -0
- package/dist/01-tokens/focus-ring.css +1 -0
- package/dist/01-tokens/page.css +1 -0
- package/dist/01-tokens/polyfill.js +78 -0
- package/dist/01-tokens/scrollbar.css +1 -0
- package/dist/01-tokens/theme-toggle.js +61 -0
- package/dist/01-tokens/tokens.css +1 -0
- package/dist/01-tokens/typography.css +1 -0
- package/dist/02-primitives/badge/badge.css +1 -0
- package/dist/02-primitives/button/button.css +1 -0
- package/dist/02-primitives/card/card.css +1 -0
- package/dist/02-primitives/choice/choice.css +1 -0
- package/dist/02-primitives/combobox/combobox.css +1 -0
- package/dist/02-primitives/combobox/combobox.js +439 -0
- package/dist/02-primitives/date-range/date-range.css +1 -0
- package/dist/02-primitives/date-range/date-range.js +267 -0
- package/dist/02-primitives/input/input.css +1 -0
- package/dist/02-primitives/popover/popover.css +1 -0
- package/dist/02-primitives/popover/popover.js +257 -0
- package/dist/02-primitives/progress/progress.css +1 -0
- package/dist/02-primitives/select/select.css +1 -0
- package/dist/02-primitives/select/select.js +449 -0
- package/dist/02-primitives/separator/separator.css +1 -0
- package/dist/02-primitives/skeleton/skeleton.css +1 -0
- package/dist/02-primitives/switch/switch.css +1 -0
- package/dist/03-patterns/accordion/accordion.css +1 -0
- package/dist/03-patterns/accordion/accordion.js +219 -0
- package/dist/03-patterns/content/content.css +1 -0
- package/dist/03-patterns/drawer/drawer.css +1 -0
- package/dist/03-patterns/drawer/drawer.js +229 -0
- package/dist/03-patterns/dropdown/dropdown.css +1 -0
- package/dist/03-patterns/dropdown/dropdown.js +365 -0
- package/dist/03-patterns/form-validation/form-validation.css +1 -0
- package/dist/03-patterns/list/flip.js +117 -0
- package/dist/03-patterns/list/list.css +1 -0
- package/dist/03-patterns/nav/nav.css +1 -0
- package/dist/03-patterns/nav/nav.js +322 -0
- package/dist/03-patterns/nav/toc.js +76 -0
- package/dist/03-patterns/overlay/overlay.css +1 -0
- package/dist/03-patterns/overlay/overlay.js +398 -0
- package/dist/03-patterns/pagination/pagination.css +1 -0
- package/dist/03-patterns/pagination/pagination.js +273 -0
- package/dist/03-patterns/reveal/reveal.css +1 -0
- package/dist/03-patterns/reveal/reveal.js +138 -0
- package/dist/03-patterns/states/states.css +1 -0
- package/dist/03-patterns/tabs/tabs.css +1 -0
- package/dist/03-patterns/tabs/tabs.js +242 -0
- package/dist/03-patterns/tooltip/tooltip.css +1 -0
- package/dist/03-patterns/tooltip/tooltip.js +213 -0
- package/dist/03-patterns/tree/tree.css +1 -0
- package/dist/03-patterns/tree/tree.js +383 -0
- package/dist/04-recipes/analysis-report/analysis-report.css +1 -0
- package/dist/04-recipes/detail-page/detail-page.css +1 -0
- package/dist/04-recipes/report/report.css +1 -0
- package/dist/04-recipes/table/table.css +1 -0
- package/dist/04-recipes/tierA-tokens.css +1 -0
- package/dist/09-assets/bar/bar.css +1 -0
- package/dist/09-assets/bar/bar.js +222 -0
- package/dist/09-assets/echarts-adapter/echarts-adapter.js +185 -0
- package/dist/09-assets/model-viewer/model-viewer.css +1 -0
- package/dist/09-assets/model-viewer/model-viewer.js +320 -0
- package/dist/09-assets/scientific-plot/scientific-plot.css +1 -0
- package/dist/09-assets/scientific-plot/scientific-plot.js +568 -0
- package/dist/09-assets/sparkline/sparkline.css +1 -0
- package/dist/09-assets/sparkline/sparkline.js +265 -0
- package/dist/COSTS.md +56 -0
- package/docs/AI-CONTRACT.md +197 -0
- package/docs/BACKLOG.md +127 -0
- package/docs/BENCHMARK.md +252 -0
- package/docs/GUIDANCE.md +154 -0
- package/docs/INVARIANT.md +372 -0
- package/docs/LAYER-BASELINE.md +158 -0
- package/docs/RELEASING.md +275 -0
- package/docs/RFC-THEME-SCOPE.md +141 -0
- package/docs/ROADMAP.md +182 -0
- package/index.html +82 -0
- package/package.json +107 -4
- package/types/index.d.ts +310 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
(function (global) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var DEFAULT_MARGIN = { top: 18, right: 22, bottom: 56, left: 68 };
|
|
5
|
+
var SERIES_CLASSES = 5;
|
|
6
|
+
var DASHES = [[], [7, 4], [2, 3], [10, 4, 2, 4], [1, 3]];
|
|
7
|
+
|
|
8
|
+
var plotUid = 0;
|
|
9
|
+
|
|
10
|
+
function isNum(v) { return typeof v === 'number' && isFinite(v); }
|
|
11
|
+
|
|
12
|
+
function esc(s) {
|
|
13
|
+
return String(s == null ? '' : s)
|
|
14
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
15
|
+
.replace(/"/g, '"');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 数字格式化:科学图里小数位是**信息**,不能顺手 toLocaleString 了事 */
|
|
19
|
+
function fmt(v, p) {
|
|
20
|
+
if (!isNum(v)) return '';
|
|
21
|
+
if (p === undefined || p === null) {
|
|
22
|
+
/* 默认:够用就行,但不要出现 1.0000000000000002 */
|
|
23
|
+
return String(+v.toPrecision(12));
|
|
24
|
+
}
|
|
25
|
+
return v.toFixed(p);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cssVar(name, fallback) {
|
|
29
|
+
if (typeof getComputedStyle !== 'function' || !document.documentElement) return fallback;
|
|
30
|
+
var v = getComputedStyle(document.documentElement).getPropertyValue(name);
|
|
31
|
+
v = v ? v.trim() : '';
|
|
32
|
+
return v || fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* --------------------------------------------------------------- 比例尺 */
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 造一个比例尺。
|
|
39
|
+
* @param {{scale?:string, domain:number[]}} axis
|
|
40
|
+
* @param {number[]} range 像素范围 [起, 止]
|
|
41
|
+
* @returns {{to:function(number):?number, type:string, domain:number[]}}
|
|
42
|
+
*
|
|
43
|
+
* 🔴 对数轴:domain 必须 > 0。给 0 或负数直接抛错 ——
|
|
44
|
+
* "悄悄把 0 当 1e-9" 会画出一条看起来正常、实际错误的曲线。
|
|
45
|
+
*/
|
|
46
|
+
function makeScale(axis, range) {
|
|
47
|
+
var type = (axis && axis.scale) || 'linear';
|
|
48
|
+
var d = (axis && axis.domain) || [];
|
|
49
|
+
var d0 = d[0], d1 = d[1];
|
|
50
|
+
var r0 = range[0], r1 = range[1];
|
|
51
|
+
|
|
52
|
+
if (type !== 'linear' && type !== 'log') {
|
|
53
|
+
throw new Error('scientific-plot: 未知的 scale「' + type + '」(只支持 linear / log)');
|
|
54
|
+
}
|
|
55
|
+
if (!isNum(d0) || !isNum(d1)) {
|
|
56
|
+
throw new Error('scientific-plot: ' + type + ' 轴必须给 domain:[min,max]');
|
|
57
|
+
}
|
|
58
|
+
if (type === 'log' && (d0 <= 0 || d1 <= 0)) {
|
|
59
|
+
throw new Error('scientific-plot: 对数轴的 domain 必须 > 0(收到 [' + d0 + ', ' + d1 + '])');
|
|
60
|
+
}
|
|
61
|
+
if (d0 === d1) {
|
|
62
|
+
throw new Error('scientific-plot: domain 的上下限相同(' + d0 + '),无法定标');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
var to;
|
|
66
|
+
if (type === 'linear') {
|
|
67
|
+
to = function (v) { return r0 + (v - d0) / (d1 - d0) * (r1 - r0); };
|
|
68
|
+
} else {
|
|
69
|
+
var l0 = Math.log(d0), l1 = Math.log(d1);
|
|
70
|
+
to = function (v) {
|
|
71
|
+
if (!isNum(v) || v <= 0) return null; /* 非正数在对数轴上**没有位置** */
|
|
72
|
+
return r0 + (Math.log(v) - l0) / (l1 - l0) * (r1 - r0);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
to: to, type: type, domain: [d0, d1],
|
|
78
|
+
/* 反向:像素 → 值(给十字线一类的交互留口子,目前 demo 用不到) */
|
|
79
|
+
invert: function (px) {
|
|
80
|
+
var t = (px - r0) / (r1 - r0);
|
|
81
|
+
return type === 'linear' ? d0 + t * (d1 - d0) : Math.exp(l0 + t * (l1 - l0));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ----------------------------------------------------------------- 刻度 */
|
|
87
|
+
|
|
88
|
+
/** 线性轴的"好看"刻度:1 / 2 / 5 × 10^n */
|
|
89
|
+
function linearTicks(d0, d1, target) {
|
|
90
|
+
target = target || 6;
|
|
91
|
+
var span = d1 - d0;
|
|
92
|
+
if (!isNum(span) || span <= 0) return [d0];
|
|
93
|
+
var step = Math.pow(10, Math.floor(Math.log(span / target) / Math.LN10));
|
|
94
|
+
var err = (span / target) / step;
|
|
95
|
+
if (err >= 7.5) step *= 10;
|
|
96
|
+
else if (err >= 3.5) step *= 5;
|
|
97
|
+
else if (err >= 1.5) step *= 2;
|
|
98
|
+
|
|
99
|
+
var out = [];
|
|
100
|
+
var t = Math.ceil(d0 / step) * step;
|
|
101
|
+
/* 用乘法推进而不是累加,避免浮点误差把刻度漂到 0.30000000000000004 */
|
|
102
|
+
var i = 0;
|
|
103
|
+
while (t <= d1 + step * 1e-9 && i < 200) {
|
|
104
|
+
out.push(+t.toPrecision(12));
|
|
105
|
+
i++;
|
|
106
|
+
t = Math.ceil(d0 / step) * step + i * step;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 对数轴刻度:主刻度是 10 的幂;跨度小时补 2× / 5× 次刻度 */
|
|
112
|
+
function logTicks(d0, d1) {
|
|
113
|
+
var e0 = Math.floor(Math.log(d0) / Math.LN10);
|
|
114
|
+
var e1 = Math.ceil(Math.log(d1) / Math.LN10);
|
|
115
|
+
var decades = e1 - e0;
|
|
116
|
+
/* 跨度 ≥ 6 个数量级时只保留 10 的幂 —— 否则标签会挤成一团 */
|
|
117
|
+
var mantissas = decades >= 6 ? [1] : [1, 2, 5];
|
|
118
|
+
|
|
119
|
+
var out = [];
|
|
120
|
+
for (var e = e0; e <= e1; e++) {
|
|
121
|
+
var base = Math.pow(10, e);
|
|
122
|
+
for (var i = 0; i < mantissas.length; i++) {
|
|
123
|
+
var v = base * mantissas[i];
|
|
124
|
+
if (v >= d0 * (1 - 1e-9) && v <= d1 * (1 + 1e-9)) out.push(v);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/* 排序 + 按精度去重:浮点会把同一个刻度算出两份(1000 与 1000.0000000000001) */
|
|
128
|
+
out.sort(function (a, b) { return a - b; });
|
|
129
|
+
var seen = {}, uniq = [];
|
|
130
|
+
out.forEach(function (v) {
|
|
131
|
+
var k = v.toPrecision(12);
|
|
132
|
+
if (!seen[k]) { seen[k] = 1; uniq.push(v); }
|
|
133
|
+
});
|
|
134
|
+
return uniq;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ------------------------------------------------------------ 序列分组 */
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 把数据按"缺失"切成若干连续段。
|
|
141
|
+
* 🔴 这是"缺失值 ≠ 零"的落地点:跨过 null 连线 = 编造了一段不存在的趋势。
|
|
142
|
+
*/
|
|
143
|
+
function splitByGaps(data, xAccessor, yAccessor) {
|
|
144
|
+
var segs = [], cur = [];
|
|
145
|
+
(data || []).forEach(function (p) {
|
|
146
|
+
var x = xAccessor(p), y = yAccessor(p);
|
|
147
|
+
if (!isNum(x) || !isNum(y)) {
|
|
148
|
+
if (cur.length) { segs.push(cur); cur = []; }
|
|
149
|
+
} else {
|
|
150
|
+
cur.push([x, y, p]);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
if (cur.length) segs.push(cur);
|
|
154
|
+
return segs;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ------------------------------------------------------------- 采样函数 */
|
|
158
|
+
|
|
159
|
+
/** 把函数采样成数据点。**不做表达式求值** —— 只调用调用方给的真函数。 */
|
|
160
|
+
function sampleFunction(fn, a, b, n) {
|
|
161
|
+
n = n || 200;
|
|
162
|
+
var out = [];
|
|
163
|
+
if (typeof fn !== 'function') return out;
|
|
164
|
+
for (var i = 0; i <= n; i++) {
|
|
165
|
+
var x = a + (b - a) * (i / n);
|
|
166
|
+
var y;
|
|
167
|
+
try { y = fn(x); } catch (e) { y = null; }
|
|
168
|
+
out.push([x, isNum(y) ? y : null]);
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 把一段数据切成"坐标都有效"的连续子段。
|
|
175
|
+
*
|
|
176
|
+
* 🔴 为什么不能直接 `xs.to(x) + ',' + ys.to(y)` 拼字符串:
|
|
177
|
+
* `makeScale.to()` 对**对数轴上的非正数返回 null**(它在那条轴上没有位置),
|
|
178
|
+
* 直接拼 ⇒ 产出 `128,null` ⇒ 浏览器把**整条** polyline 判为非法而丢弃,
|
|
179
|
+
* 读者看到的是"这条曲线不存在",而不是"这里有几个点画不出来"。
|
|
180
|
+
* ⇒ 必须**在点级别**断,而且要把断掉的点**计数**告诉调用方。
|
|
181
|
+
*
|
|
182
|
+
* @param {Array} seg [[x,y],…],不含缺失值
|
|
183
|
+
* @param {Object} tally { n: number } —— 被跳过的点数累加器(可为 null)
|
|
184
|
+
* @returns {Array} 若干段 [[[px,py],…],…],每段 ≥1 个点
|
|
185
|
+
*/
|
|
186
|
+
function toRuns(seg, xs, ys, tally) {
|
|
187
|
+
var runs = [], cur = [];
|
|
188
|
+
(seg || []).forEach(function (q) {
|
|
189
|
+
var px = xs.to(q[0]), py = ys.to(q[1]);
|
|
190
|
+
/* 对数轴 ≤0 ⇒ null;非有限值 ⇒ NaN/Infinity。**两者都不能进 graphics */
|
|
191
|
+
if (px == null || py == null || !isFinite(px) || !isFinite(py)) {
|
|
192
|
+
if (tally) tally.n++;
|
|
193
|
+
if (cur.length) runs.push(cur);
|
|
194
|
+
cur = [];
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
cur.push([px, py]);
|
|
198
|
+
});
|
|
199
|
+
if (cur.length) runs.push(cur);
|
|
200
|
+
return runs;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* ------------------------------------------------------------- 主渲染 */
|
|
204
|
+
|
|
205
|
+
function render(el, spec) {
|
|
206
|
+
if (!el) throw new Error('scientific-plot: 缺少挂载元素');
|
|
207
|
+
spec = spec || {};
|
|
208
|
+
|
|
209
|
+
var W = spec.width || 640;
|
|
210
|
+
var H = spec.height || 400;
|
|
211
|
+
var m = {};
|
|
212
|
+
var mk = spec.margin || {};
|
|
213
|
+
m.top = mk.top === undefined ? DEFAULT_MARGIN.top : mk.top;
|
|
214
|
+
m.right = mk.right === undefined ? DEFAULT_MARGIN.right : mk.right;
|
|
215
|
+
m.bottom = mk.bottom === undefined ? DEFAULT_MARGIN.bottom : mk.bottom;
|
|
216
|
+
m.left = mk.left === undefined ? DEFAULT_MARGIN.left : mk.left;
|
|
217
|
+
|
|
218
|
+
var x0 = m.left, x1 = W - m.right;
|
|
219
|
+
var yTop = m.top, yBot = H - m.bottom;
|
|
220
|
+
|
|
221
|
+
var xs = makeScale(spec.x, [x0, x1]);
|
|
222
|
+
/* y 轴像素方向朝上 ⇒ range 反过来给 */
|
|
223
|
+
var ys = makeScale(spec.y, [yBot, yTop]);
|
|
224
|
+
|
|
225
|
+
var parts = [];
|
|
226
|
+
var label = [];
|
|
227
|
+
|
|
228
|
+
/* ---------- 网格 ---------- */
|
|
229
|
+
var xt = xs.type === 'log' ? logTicks(xs.domain[0], xs.domain[1])
|
|
230
|
+
: linearTicks(xs.domain[0], xs.domain[1]);
|
|
231
|
+
var yt = ys.type === 'log' ? logTicks(ys.domain[0], ys.domain[1])
|
|
232
|
+
: linearTicks(ys.domain[0], ys.domain[1]);
|
|
233
|
+
|
|
234
|
+
if (spec.grid !== false) {
|
|
235
|
+
xt.forEach(function (v) {
|
|
236
|
+
var px = xs.to(v); if (px == null) return;
|
|
237
|
+
parts.push('<line class="splot__grid" x1="' + px + '" y1="' + yTop +
|
|
238
|
+
'" x2="' + px + '" y2="' + yBot + '"/>');
|
|
239
|
+
});
|
|
240
|
+
yt.forEach(function (v) {
|
|
241
|
+
var py = ys.to(v); if (py == null) return;
|
|
242
|
+
parts.push('<line class="splot__grid" x1="' + x0 + '" y1="' + py +
|
|
243
|
+
'" x2="' + x1 + '" y2="' + py + '"/>');
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ---------- 轴 ---------- */
|
|
248
|
+
parts.push('<line class="splot__axis" x1="' + x0 + '" y1="' + yBot +
|
|
249
|
+
'" x2="' + x1 + '" y2="' + yBot + '"/>');
|
|
250
|
+
parts.push('<line class="splot__axis" x1="' + x0 + '" y1="' + yTop +
|
|
251
|
+
'" x2="' + x0 + '" y2="' + yBot + '"/>');
|
|
252
|
+
|
|
253
|
+
/* ---------- 刻度与标签 ---------- */
|
|
254
|
+
xt.forEach(function (v) {
|
|
255
|
+
var px = xs.to(v); if (px == null) return;
|
|
256
|
+
parts.push('<line class="splot__tick" x1="' + px + '" y1="' + yBot +
|
|
257
|
+
'" x2="' + px + '" y2="' + (yBot + 5) + '"/>');
|
|
258
|
+
parts.push('<text class="splot__tick-label" x="' + px + '" y="' + (yBot + 20) +
|
|
259
|
+
'" text-anchor="middle">' + esc(fmtTick(v, xs.type)) + '</text>');
|
|
260
|
+
});
|
|
261
|
+
yt.forEach(function (v) {
|
|
262
|
+
var py = ys.to(v); if (py == null) return;
|
|
263
|
+
parts.push('<line class="splot__tick" x1="' + (x0 - 5) + '" y1="' + py +
|
|
264
|
+
'" x2="' + x0 + '" y2="' + py + '"/>');
|
|
265
|
+
parts.push('<text class="splot__tick-label" x="' + (x0 - 9) + '" y="' + (py + 4) +
|
|
266
|
+
'" text-anchor="end">' + esc(fmtTick(v, ys.type)) + '</text>');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
/* ---------- 轴名与单位(原则①) ---------- */
|
|
270
|
+
var xl = axisTitle(spec.x, 'x');
|
|
271
|
+
if (xl) {
|
|
272
|
+
parts.push('<text class="splot__axis-label" x="' + ((x0 + x1) / 2) + '" y="' +
|
|
273
|
+
(H - 12) + '" text-anchor="middle">' + esc(xl) + '</text>');
|
|
274
|
+
}
|
|
275
|
+
var yl = axisTitle(spec.y, 'y');
|
|
276
|
+
if (yl) {
|
|
277
|
+
parts.push('<text class="splot__axis-label" transform="translate(16,' +
|
|
278
|
+
((yTop + yBot) / 2) + ') rotate(-90)" text-anchor="middle">' +
|
|
279
|
+
esc(yl) + '</text>');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* ---------- 数据序列 ---------- */
|
|
283
|
+
var series = spec.series || [];
|
|
284
|
+
var dparts = []; /* 数据层单独收集 ⇒ 统一挂裁剪。见下方 clipId */
|
|
285
|
+
var tally = { n: 0 }; /* 画不出来的点数(对数轴非正 / 非有限值) */
|
|
286
|
+
|
|
287
|
+
/* 🔴🔴 系列样式身份(外部评审 VIZ-REPORT-01 · P0-2)
|
|
288
|
+
------------------------------------------------------------------
|
|
289
|
+
修复前每个系列都拿 `'splot__s' + (colorIdx % 5)`,而 `colorIdx` 从头到尾
|
|
290
|
+
是 0 ⇒ 六条 series 全是 s0,同色同线型,读者分不出哪条是哪条材料。
|
|
291
|
+
|
|
292
|
+
⚠️ 修法**不能**是"在循环末尾加一行 colorIdx++":
|
|
293
|
+
那样每条 *记录* 都会吃掉一个色号,同一材料的「区间带」和「曲线」
|
|
294
|
+
会变成两个颜色 —— 错得更隐蔽(读者会以为是两种材料)。
|
|
295
|
+
|
|
296
|
+
⇒ 正确做法是给**逻辑系列**发身份:
|
|
297
|
+
· 有 `id`(或 `name`)⇒ 身份就是它,**带和线共用同一个色号**
|
|
298
|
+
· 没有 ⇒ 这条记录自成一个系列(至少彼此能分辨)
|
|
299
|
+
这条规则同时也决定了图例怎么排(同一身份只出现一次)。 */
|
|
300
|
+
var styleIds = {}; /* 身份 → 色号 */
|
|
301
|
+
var styleSeq = 0; /* 已发出的最大色号 +1 */
|
|
302
|
+
function styleIndexOf(s) {
|
|
303
|
+
var key = (s.id != null && s.id !== '') ? s.id : s.name;
|
|
304
|
+
if (key == null || key === '') return styleSeq++;
|
|
305
|
+
var k = String(key);
|
|
306
|
+
if (!Object.prototype.hasOwnProperty.call(styleIds, k)) styleIds[k] = styleSeq++;
|
|
307
|
+
return styleIds[k];
|
|
308
|
+
}
|
|
309
|
+
var legendOrder = []; /* [{ key, index }] 按首次出现顺序 */
|
|
310
|
+
var drawn = 0; /* 画出来的系列条数(给 aria-label 与门禁用) */
|
|
311
|
+
/* 像素坐标是否可用:null(对数轴非正)与 NaN/Infinity 都不许进图形 */
|
|
312
|
+
function okPx(v) { return v != null && isFinite(v); }
|
|
313
|
+
|
|
314
|
+
series.forEach(function (s) {
|
|
315
|
+
var st = styleIndexOf(s);
|
|
316
|
+
var cls = 'splot__s' + (st % SERIES_CLASSES);
|
|
317
|
+
if (s.name != null && s.name !== '') {
|
|
318
|
+
var key = String(s.id != null && s.id !== '' ? s.id : s.name);
|
|
319
|
+
var seen = legendOrder.some(function (o) { return o.key === key; });
|
|
320
|
+
if (!seen) legendOrder.push({ key: key, name: String(s.name), index: st });
|
|
321
|
+
}
|
|
322
|
+
var dMin = s.samples || 200;
|
|
323
|
+
var data = s.type === 'function'
|
|
324
|
+
? sampleFunction(s.fn, (s.domain || xs.domain)[0], (s.domain || xs.domain)[1], dMin)
|
|
325
|
+
: (s.data || []);
|
|
326
|
+
|
|
327
|
+
if (s.type === 'band') {
|
|
328
|
+
/* 置信区间带:上下两条边 + 中间填充
|
|
329
|
+
🔴 与折线同一条纪律:**不能跨过画不出来的点**。
|
|
330
|
+
一路 push 到同一个 polygon ⇒ 遇到对数轴上 ≤0 的边界时,
|
|
331
|
+
这段"根本没有数据"的区间会被**连成一块完整的色带**,
|
|
332
|
+
等于向读者宣称那里有观测支撑。 ⇒ 按连续可用段切开。 */
|
|
333
|
+
var bRuns = [], bCur = [];
|
|
334
|
+
data.forEach(function (p) {
|
|
335
|
+
var px = xs.to(p[0]), hi = ys.to(p[2]), lo = ys.to(p[1]);
|
|
336
|
+
if (!okPx(px) || !okPx(hi) || !okPx(lo)) {
|
|
337
|
+
tally.n++;
|
|
338
|
+
if (bCur.length >= 2) bRuns.push(bCur);
|
|
339
|
+
bCur = [];
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
bCur.push([px, hi, lo]);
|
|
343
|
+
});
|
|
344
|
+
if (bCur.length >= 2) bRuns.push(bCur);
|
|
345
|
+
|
|
346
|
+
bRuns.forEach(function (run) {
|
|
347
|
+
var up = [], dn = [];
|
|
348
|
+
run.forEach(function (c) { up.push(c[0] + ',' + c[1]); });
|
|
349
|
+
for (var j = run.length - 1; j >= 0; j--) dn.push(run[j][0] + ',' + run[j][2]);
|
|
350
|
+
dparts.push('<polygon class="splot__band ' + cls + '" points="' +
|
|
351
|
+
up.join(' ') + ' ' + dn.join(' ') + '"/>');
|
|
352
|
+
});
|
|
353
|
+
if (bRuns.length) drawn++;
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (s.type === 'errorbar') {
|
|
358
|
+
/* 误差棒:只画调用方给的误差,**不计算** */
|
|
359
|
+
data.forEach(function (p) {
|
|
360
|
+
var px = xs.to(p[0]);
|
|
361
|
+
if (!okPx(px) || !isNum(p[1])) { tally.n++; return; }
|
|
362
|
+
var py = ys.to(p[1]); if (!okPx(py)) { tally.n++; return; }
|
|
363
|
+
/* 两种写法都支持:
|
|
364
|
+
[x, y, yerr] —— 对称误差,上下各延 yerr
|
|
365
|
+
[x, y, lo, hi] —— 非对称区间(调用方自己算好的边界)
|
|
366
|
+
🔴 本模块只画给定的边界,**不替调用方计算**标准误或置信区间。 */
|
|
367
|
+
var yLo = p.length > 3 ? p[2] : p[1] - (p[2] || 0);
|
|
368
|
+
var yHi = p.length > 3 ? p[3] : p[1] + (p[2] || 0);
|
|
369
|
+
var pa = ys.to(yLo), pb = ys.to(yHi);
|
|
370
|
+
if (!okPx(pa) || !okPx(pb)) { tally.n++; return; }
|
|
371
|
+
dparts.push('<line class="splot__err ' + cls + '" x1="' + px + '" y1="' + pa +
|
|
372
|
+
'" x2="' + px + '" y2="' + pb + '"/>');
|
|
373
|
+
dparts.push('<line class="splot__err ' + cls + '" x1="' + (px - 4) + '" y1="' + pa +
|
|
374
|
+
'" x2="' + (px + 4) + '" y2="' + pa + '"/>');
|
|
375
|
+
dparts.push('<line class="splot__err ' + cls + '" x1="' + (px - 4) + '" y1="' + pb +
|
|
376
|
+
'" x2="' + (px + 4) + '" y2="' + pb + '"/>');
|
|
377
|
+
});
|
|
378
|
+
drawn++;
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (s.type === 'line' || s.type === 'function') {
|
|
383
|
+
/* 🔴 ① 按缺失分段,**不跨 null 连线**(原则②)
|
|
384
|
+
② 段内再按"坐标算不算得出来"切开(对数轴 ≤0 / 非有限值) */
|
|
385
|
+
var segs = splitByGaps(data, function (p) { return p[0]; }, function (p) { return p[1]; });
|
|
386
|
+
segs.forEach(function (seg) {
|
|
387
|
+
toRuns(seg, xs, ys, tally).forEach(function (run) {
|
|
388
|
+
if (run.length < 2) {
|
|
389
|
+
/* 单个孤立点也要能看见,否则它就从图上消失了 */
|
|
390
|
+
dparts.push('<circle class="splot__pt ' + cls + '" cx="' + run[0][0] +
|
|
391
|
+
'" cy="' + run[0][1] + '" r="2.5"/>');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
var pts = run.map(function (c) { return c[0] + ',' + c[1]; }).join(' ');
|
|
395
|
+
dparts.push('<polyline class="splot__line ' + cls + '" points="' + pts +
|
|
396
|
+
'"' + dashAttr(st) + '/>');
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
drawn++;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (s.type === 'scatter') {
|
|
404
|
+
data.forEach(function (p) {
|
|
405
|
+
var px = xs.to(p[0]);
|
|
406
|
+
if (!okPx(px) || !isNum(p[1])) { tally.n++; return; }
|
|
407
|
+
var py = ys.to(p[1]); if (!okPx(py)) { tally.n++; return; }
|
|
408
|
+
dparts.push('<circle class="splot__pt ' + cls + '" cx="' + px + '" cy="' + py +
|
|
409
|
+
'" r="3"/>');
|
|
410
|
+
});
|
|
411
|
+
drawn++;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
/* ---------- 🔴 裁剪:数据不许画到坐标域之外 ----------
|
|
417
|
+
SVG 的 `overflow` 默认是 visible:一个超出 domain 的散点会直接落到
|
|
418
|
+
轴外侧、甚至压到相邻的卡片上(09-assets/bar 真机踩过同一个坑)。
|
|
419
|
+
⇒ 用 clipPath 把**数据层**锁在坐标矩形内;网格与轴不参与(它们本来就在边上)。
|
|
420
|
+
⚠️ id 必须每次渲染都换新的:同一页多张图共用 id ⇒ 后面的图会被前面的裁掉。 */
|
|
421
|
+
var clipId = 'splot-clip-' + (++plotUid);
|
|
422
|
+
parts.push('<defs><clipPath id="' + clipId + '"><rect x="' + x0 + '" y="' + yTop +
|
|
423
|
+
'" width="' + (x1 - x0) + '" height="' + (yBot - yTop) +
|
|
424
|
+
'"/></clipPath></defs>');
|
|
425
|
+
parts.push('<g clip-path="url(#' + clipId + ')">' + dparts.join('') + '</g>');
|
|
426
|
+
|
|
427
|
+
/* ---------- 参考线 ---------- */
|
|
428
|
+
(spec.refs || []).forEach(function (r) {
|
|
429
|
+
var isY = r.axis === 'y';
|
|
430
|
+
var p = isY ? ys.to(r.value) : xs.to(r.value);
|
|
431
|
+
if (p == null) return;
|
|
432
|
+
if (isY) {
|
|
433
|
+
parts.push('<line class="splot__ref" x1="' + x0 + '" y1="' + p +
|
|
434
|
+
'" x2="' + x1 + '" y2="' + p + '"/>');
|
|
435
|
+
if (r.label) {
|
|
436
|
+
parts.push('<text class="splot__ref-label" x="' + (x1 - 4) + '" y="' + (p - 5) +
|
|
437
|
+
'" text-anchor="end">' + esc(r.label) + '</text>');
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
parts.push('<line class="splot__ref" x1="' + p + '" y1="' + yTop +
|
|
441
|
+
'" x2="' + p + '" y2="' + yBot + '"/>');
|
|
442
|
+
if (r.label) {
|
|
443
|
+
parts.push('<text class="splot__ref-label" x="' + (p + 5) + '" y="' + (yTop + 12) +
|
|
444
|
+
'">' + esc(r.label) + '</text>');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
/* ---------- 点标注 ---------- */
|
|
450
|
+
(spec.annotations || []).forEach(function (a) {
|
|
451
|
+
var px = xs.to(a.x), py = ys.to(a.y);
|
|
452
|
+
if (px == null || py == null) return;
|
|
453
|
+
parts.push('<circle class="splot__anno-dot" cx="' + px + '" cy="' + py + '" r="3.5"/>');
|
|
454
|
+
parts.push('<text class="splot__anno" x="' + (px + 7) + '" y="' + (py - 6) + '">' +
|
|
455
|
+
esc(a.text) + '</text>');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
/* ---------- 截断提示(原则③) ---------- */
|
|
459
|
+
if (spec.truncated) {
|
|
460
|
+
parts.push('<text class="splot__note" x="' + x1 + '" y="' + (yTop - 4) +
|
|
461
|
+
'" text-anchor="end">⚠ 轴已截断,非从零起</text>');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* 🔴 有画不出来的点就**明说**(对数轴 ≤0 / 非有限值)
|
|
465
|
+
—— 静默丢弃会让读者以为"这条曲线整个就是这个形状" */
|
|
466
|
+
if (tally.n > 0) {
|
|
467
|
+
parts.push('<text class="splot__note" x="' + x0 + '" y="' + (yTop - 4) + '">⚠ ' +
|
|
468
|
+
tally.n + ' 个点不在数轴范围内,未绘制</text>');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* ---------- 文本等价信息(原则⑤) ---------- */
|
|
472
|
+
label.push(spec.title || '科学绘图');
|
|
473
|
+
if (xl) label.push('横轴:' + xl);
|
|
474
|
+
if (yl) label.push('纵轴:' + yl);
|
|
475
|
+
label.push('共 ' + drawn + ' 个数据系列');
|
|
476
|
+
/* 系列名要进 aria-label:只用颜色区分 ⇔ 读屏用户完全无从分辨 */
|
|
477
|
+
if (legendOrder.length) {
|
|
478
|
+
label.push('系列:' + legendOrder.map(function (o) { return o.name; }).join('、'));
|
|
479
|
+
}
|
|
480
|
+
if (tally.n > 0) label.push('有 ' + tally.n + ' 个点不在轴范围内未绘制');
|
|
481
|
+
if (spec.desc) label.push(spec.desc);
|
|
482
|
+
|
|
483
|
+
var svg = '<svg class="splot ' + esc(spec.className || '') + '" viewBox="0 0 ' + W + ' ' + H +
|
|
484
|
+
'" width="' + W + '" height="' + H + '" role="img" aria-label="' +
|
|
485
|
+
esc(label.join('。')) + '">' + parts.join('') + '</svg>';
|
|
486
|
+
|
|
487
|
+
/* ---------- 图例(B7:颜色之外必须有名字) ---------- *
|
|
488
|
+
* 🔴 什么时候出:**有两个以上有名字的系列**时自动出。
|
|
489
|
+
* 三条曲线的颜色不同、但没有任何文字告诉你谁是谁 ⇒ 这张图回答不了问题。
|
|
490
|
+
* ⇒ 想关就显式传 `legend: false`。
|
|
491
|
+
* ⚠️ 同一身份(带 + 线)只出现**一次**,并且色块取的就是它自己那条线的色号。 */
|
|
492
|
+
var legendHtml = '';
|
|
493
|
+
var wantLegend = spec.legend === undefined
|
|
494
|
+
? legendOrder.length >= 2
|
|
495
|
+
: !!spec.legend;
|
|
496
|
+
if (wantLegend && legendOrder.length) {
|
|
497
|
+
var items = legendOrder.map(function (o) {
|
|
498
|
+
var c = 'splot__s' + (o.index % SERIES_CLASSES);
|
|
499
|
+
/* 用一小段**真的线**做色标,`--splot-c` 由上面的系列类给
|
|
500
|
+
⇒ 图例的色和曲线/色带的色来自同一份声明,不可能对不上 */
|
|
501
|
+
return '<li class="splot__legend-item">' +
|
|
502
|
+
'<svg class="splot__legend-mark ' + c + '" width="20" height="10" ' +
|
|
503
|
+
'viewBox="0 0 20 10" aria-hidden="true" focusable="false">' +
|
|
504
|
+
'<line x1="1" y1="5" x2="19" y2="5" stroke-width="2" stroke-linecap="round"' +
|
|
505
|
+
dashAttr(o.index) + '/></svg>' +
|
|
506
|
+
'<span>' + esc(o.name) + '</span></li>';
|
|
507
|
+
});
|
|
508
|
+
legendHtml = '<ul class="splot__legend">' + items.join('') + '</ul>';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
el.innerHTML = svg + legendHtml;
|
|
512
|
+
return {
|
|
513
|
+
svg: el.firstChild,
|
|
514
|
+
x: xs, y: ys,
|
|
515
|
+
ticks: { x: xt, y: yt },
|
|
516
|
+
series: drawn,
|
|
517
|
+
dropped: tally.n,
|
|
518
|
+
legend: el.querySelector('.splot__legend'),
|
|
519
|
+
styles: legendOrder.map(function (o) { return o.index; }),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/* ------------------------------------------------------------- 小助手 */
|
|
524
|
+
|
|
525
|
+
function axisTitle(axis, fallback) {
|
|
526
|
+
if (!axis) return '';
|
|
527
|
+
var t = axis.label || axis.title || fallback;
|
|
528
|
+
if (axis.unit) t += ' / ' + axis.unit;
|
|
529
|
+
return t;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function fmtTick(v, type) {
|
|
533
|
+
if (type === 'log') {
|
|
534
|
+
if (v >= 1e4 || v < 1e-3) {
|
|
535
|
+
var e = Math.round(Math.log(v) / Math.LN10);
|
|
536
|
+
return '1e' + e;
|
|
537
|
+
}
|
|
538
|
+
return String(+v.toPrecision(4));
|
|
539
|
+
}
|
|
540
|
+
if (Math.abs(v) >= 1e5 || (v !== 0 && Math.abs(v) < 1e-3)) {
|
|
541
|
+
return (+v.toExponential(1)).toString();
|
|
542
|
+
}
|
|
543
|
+
return String(+v.toPrecision(6));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function dashAttr(idx) {
|
|
547
|
+
var d = DASHES[idx % DASHES.length];
|
|
548
|
+
if (!d || !d.length) return '';
|
|
549
|
+
return ' stroke-dasharray="' + d.join(' ') + '"';
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/* --------------------------------------------------------------- 导出 */
|
|
553
|
+
|
|
554
|
+
global.ScientificPlot = {
|
|
555
|
+
render: render,
|
|
556
|
+
/* 给门禁/单测用的真值入口 —— 判据要能直接调它,而不是靠读 DOM 反推 */
|
|
557
|
+
__internals: {
|
|
558
|
+
makeScale: makeScale,
|
|
559
|
+
linearTicks: linearTicks,
|
|
560
|
+
logTicks: logTicks,
|
|
561
|
+
splitByGaps: splitByGaps,
|
|
562
|
+
toRuns: toRuns,
|
|
563
|
+
sampleFunction: sampleFunction,
|
|
564
|
+
SERIES_CLASSES: SERIES_CLASSES
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
})(typeof window !== 'undefined' ? window : this);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.sparkline {display:block;min-width:0;width:100%;height:var(--spark-h, 32px);overflow:hidden;margin:var(--sp-1) 0;}.sparkline svg {display:block;width:100%;overflow:hidden;}.spark-line {stroke:var(--spark-c, var(--accent));fill:none;}.spark-area {stroke:none;fill:var(--spark-c, var(--accent));}.spark-bar {stroke:none;fill:var(--spark-c, var(--accent));}.spark-dot {stroke:none;fill:var(--spark-c, var(--accent));}.row > .sparkline, td > .sparkline, .spark {margin:0;}.spark-anim {stroke-dasharray:var(--spark-len, 1000);stroke-dashoffset:0;animation:spark-draw var(--dur-slow, 600ms) var(--ease-out) forwards;}@keyframes spark-draw {from {stroke-dashoffset:var(--spark-len, 1000);}to {stroke-dashoffset:0;}}@media (prefers-reduced-motion:reduce) {.spark-anim {animation:none;stroke-dashoffset:0;}}@media print {.spark-anim {animation:none;stroke-dashoffset:0;}}
|