ccus-cli 0.1.19 → 0.1.22
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/README.md +8 -4
- package/dist/cli.js +7 -1
- package/dist/lib/aggregate-dashboard.js +100 -166
- package/dist/lib/aggregate.js +250 -115
- package/dist/lib/chart-assets.js +485 -0
- package/dist/lib/dashboard.js +74 -138
- package/dist/vendor/uplot.LICENSE +21 -0
- package/dist/vendor/uplot.iife.min.js +2 -0
- package/dist/vendor/uplot.min.css +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getUplotJs = getUplotJs;
|
|
7
|
+
exports.getUplotCss = getUplotCss;
|
|
8
|
+
exports.serializeChartPayload = serializeChartPayload;
|
|
9
|
+
exports.renderUplotChart = renderUplotChart;
|
|
10
|
+
exports.uplotHeadAssets = uplotHeadAssets;
|
|
11
|
+
exports.uplotBodyScripts = uplotBodyScripts;
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
/**
|
|
15
|
+
* uPlot 资源接入与「容器 + 内联数据 + 客户端初始化」渲染辅助。
|
|
16
|
+
*
|
|
17
|
+
* 看板的折线与纵向柱状图都改用 uPlot 渲染:服务端只输出一个容器 `<div>` 加一段
|
|
18
|
+
* `<script type="application/json">` 内联的图表配置 + 数据,页面底部再一次性内联
|
|
19
|
+
* uPlot 库与一段 bootstrap 脚本,由浏览器端 `new uPlot` 真正绘制。这样既拿到 uPlot 原生
|
|
20
|
+
* 的「十字线 + legend 跟随读数」,又保证 `dashboard build` 产物用 `file://` 离线打开仍可渲染。
|
|
21
|
+
*
|
|
22
|
+
* vendored 资源放在 `src/vendor`,build 时拷到 `dist/vendor`;运行时用 `__dirname` + `../vendor`
|
|
23
|
+
* 定位(dist 与 tsx 直跑源码都成立,照搬 `src/lib/version.ts` 读 package.json 的相对定位套路)。
|
|
24
|
+
*/
|
|
25
|
+
let cachedJs = null;
|
|
26
|
+
let cachedCss = null;
|
|
27
|
+
function readVendorFile(fileName) {
|
|
28
|
+
// dist/lib/chart-assets.js 与 src/lib/chart-assets.ts 距离 vendor 目录都是 `../vendor`。
|
|
29
|
+
const assetPath = node_path_1.default.join(__dirname, "..", "vendor", fileName);
|
|
30
|
+
return node_fs_1.default.readFileSync(assetPath, "utf8");
|
|
31
|
+
}
|
|
32
|
+
/** 读取 vendored uPlot 库源码(IIFE 压缩版),内联进页面。 */
|
|
33
|
+
function getUplotJs() {
|
|
34
|
+
if (cachedJs === null) {
|
|
35
|
+
cachedJs = readVendorFile("uplot.iife.min.js");
|
|
36
|
+
}
|
|
37
|
+
return cachedJs;
|
|
38
|
+
}
|
|
39
|
+
/** 读取 vendored uPlot 官方 CSS,内联进页面。 */
|
|
40
|
+
function getUplotCss() {
|
|
41
|
+
if (cachedCss === null) {
|
|
42
|
+
cachedCss = readVendorFile("uplot.min.css");
|
|
43
|
+
}
|
|
44
|
+
return cachedCss;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 把图表配置 + 数据安全序列化进 `<script type="application/json">`。
|
|
48
|
+
*
|
|
49
|
+
* 关键是规避 `</script>` 注入:把 `<`/`>`/`&` 转成 `\uXXXX` 转义。数据落在
|
|
50
|
+
* `<script type="application/json">` 内、由 `JSON.parse(textContent)` 读取,转义后既不破坏
|
|
51
|
+
* JSON 语义,又保证浏览器不会把内联数据误当成标签或脚本结束。
|
|
52
|
+
*/
|
|
53
|
+
function serializeChartPayload(payload) {
|
|
54
|
+
return JSON.stringify(payload)
|
|
55
|
+
.replace(/</g, "\\u003c")
|
|
56
|
+
.replace(/>/g, "\\u003e")
|
|
57
|
+
.replace(/&/g, "\\u0026");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 渲染单张 uPlot 图表的容器 + 内联数据。
|
|
61
|
+
*
|
|
62
|
+
* 只产出「容器 + 数据」两段,页面级 uPlot 库与 bootstrap 由 `uplotBodyScripts` 一次性注入。
|
|
63
|
+
* `target` 必须是页面内唯一 id(多图共存时各自定位)。
|
|
64
|
+
*/
|
|
65
|
+
function renderUplotChart(target, spec, data) {
|
|
66
|
+
const payload = serializeChartPayload({ spec, data });
|
|
67
|
+
return `<div class="uplot-host" id="${target}"></div>
|
|
68
|
+
<script type="application/json" class="ccus-chart" data-target="${target}">${payload}</script>`;
|
|
69
|
+
}
|
|
70
|
+
/** uPlot 官方 CSS + 暗色主题适配,内联进 `<head>`。 */
|
|
71
|
+
function uplotHeadAssets() {
|
|
72
|
+
return `<style>
|
|
73
|
+
${getUplotCss()}
|
|
74
|
+
/* 暗色看板适配:uPlot 默认浅色,这里覆盖宽度与十字线配色。 */
|
|
75
|
+
.uplot { width: 100%; font-family: Georgia, "Times New Roman", serif; }
|
|
76
|
+
.uplot-host { width: 100%; padding: 8px 12px 4px; }
|
|
77
|
+
.u-hz .u-cursor-x, .u-vt .u-cursor-y { border-right-color: rgba(145, 160, 184, 0.55); }
|
|
78
|
+
.u-hz .u-cursor-y, .u-vt .u-cursor-x { border-bottom-color: rgba(145, 160, 184, 0.55); }
|
|
79
|
+
/* 自定义图例:每人/每条只一项,点击同时切换该项下所有 series 显隐;人名按原样不强制大写。 */
|
|
80
|
+
.ccus-legend {
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-wrap: wrap;
|
|
83
|
+
gap: 8px 16px;
|
|
84
|
+
padding: 10px 20px 4px;
|
|
85
|
+
color: var(--muted);
|
|
86
|
+
font-size: 13px;
|
|
87
|
+
}
|
|
88
|
+
.ccus-legend-item {
|
|
89
|
+
display: inline-flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 6px;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
user-select: none;
|
|
94
|
+
text-transform: none;
|
|
95
|
+
letter-spacing: normal;
|
|
96
|
+
transition: opacity 0.15s ease;
|
|
97
|
+
}
|
|
98
|
+
.ccus-legend-item:hover { color: var(--text); }
|
|
99
|
+
.ccus-legend-item.is-off { opacity: 0.4; }
|
|
100
|
+
.ccus-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }
|
|
101
|
+
/* 跟随鼠标的 tooltip:集中显示当前位置各 series 的读数(替代底部铺值)。 */
|
|
102
|
+
.ccus-tooltip {
|
|
103
|
+
position: absolute;
|
|
104
|
+
z-index: 10;
|
|
105
|
+
pointer-events: none;
|
|
106
|
+
background: rgba(10, 13, 18, 0.92);
|
|
107
|
+
border: 1px solid rgba(120, 141, 173, 0.3);
|
|
108
|
+
border-radius: 10px;
|
|
109
|
+
padding: 8px 10px;
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
color: var(--text);
|
|
112
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
|
|
113
|
+
white-space: nowrap;
|
|
114
|
+
max-width: 320px;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
}
|
|
117
|
+
.ccus-tip-head { color: var(--muted); margin-bottom: 4px; font-size: 11px; }
|
|
118
|
+
.ccus-tip-row { display: flex; align-items: center; gap: 6px; line-height: 1.5; }
|
|
119
|
+
.ccus-tip-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
|
|
120
|
+
.ccus-tip-label { flex: 1; margin-right: 12px; }
|
|
121
|
+
.ccus-tip-val { font-variant-numeric: tabular-nums; }
|
|
122
|
+
</style>`;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 页面底部一次性注入:内联 uPlot 库 + bootstrap 脚本。
|
|
126
|
+
*
|
|
127
|
+
* bootstrap 扫描页面里所有 `script.ccus-chart`,按 data-target 找容器、解析 spec/data、
|
|
128
|
+
* 构造 uPlot opts(含 legend 读数格式化、纵向柱 bars 路径与柱顶数值标签、缺失填 null 等
|
|
129
|
+
* 无法 JSON 序列化的部分),再 `new uPlot` 绘制;并监听 resize 自适应宽度。
|
|
130
|
+
*/
|
|
131
|
+
function uplotBodyScripts() {
|
|
132
|
+
return `<script>${getUplotJs()}</script>
|
|
133
|
+
<script>
|
|
134
|
+
(function () {
|
|
135
|
+
var THEME = { axis: "#91a0b8", grid: "rgba(145, 160, 184, 0.15)" };
|
|
136
|
+
|
|
137
|
+
function round1(v) { return Math.round(v * 10) / 10; }
|
|
138
|
+
|
|
139
|
+
function pad2(n) { return (n < 10 ? "0" : "") + n; }
|
|
140
|
+
|
|
141
|
+
function makeValueFmt(unit) {
|
|
142
|
+
return function (u, v) { return v == null ? "--" : round1(v) + unit; };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 时间轴刻度标签:uPlot 默认是 12 小时制 + am/pm,这里统一改成 24 小时制 HH:mm。
|
|
146
|
+
// 跨天的 tick 第二行补上「月/日」,复刻原来「边界显示日期、其余只显示时间」的观感;
|
|
147
|
+
// tick 间隔已到天级及以上时主标签直接显示日期。splits 为秒(uPlot time scale 单位)。
|
|
148
|
+
function timeAxisVals(u, splits, axisIdx, foundSpace, foundIncr) {
|
|
149
|
+
var DAY = 86400;
|
|
150
|
+
var incr = foundIncr || 0;
|
|
151
|
+
var prevDay = null;
|
|
152
|
+
return splits.map(function (sec) {
|
|
153
|
+
var d = new Date(sec * 1000);
|
|
154
|
+
var dayKey = d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate();
|
|
155
|
+
var datePart = (d.getMonth() + 1) + "/" + d.getDate();
|
|
156
|
+
var label;
|
|
157
|
+
if (incr >= DAY) {
|
|
158
|
+
label = datePart;
|
|
159
|
+
} else {
|
|
160
|
+
var t = pad2(d.getHours()) + ":" + pad2(d.getMinutes());
|
|
161
|
+
label = dayKey !== prevDay ? t + "\\n" + datePart : t;
|
|
162
|
+
}
|
|
163
|
+
prevDay = dayKey;
|
|
164
|
+
return label;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 纵向柱状图:在每根柱顶绘制其数值。canvas 坐标按 devicePixelRatio 放大,
|
|
169
|
+
// 所以取 valToPos(..., true) 的画布像素,并用同比放大的字号绘制。
|
|
170
|
+
function barLabelHook(unit) {
|
|
171
|
+
return function (u) {
|
|
172
|
+
var ser = u.series[1];
|
|
173
|
+
if (!ser || ser.show === false) return;
|
|
174
|
+
var xs = u.data[0];
|
|
175
|
+
var ys = u.data[1];
|
|
176
|
+
var pxr = window.devicePixelRatio || 1;
|
|
177
|
+
var ctx = u.ctx;
|
|
178
|
+
ctx.save();
|
|
179
|
+
ctx.fillStyle = THEME.axis;
|
|
180
|
+
ctx.font = Math.round(11 * pxr) + "px Georgia, serif";
|
|
181
|
+
ctx.textAlign = "center";
|
|
182
|
+
ctx.textBaseline = "bottom";
|
|
183
|
+
for (var i = 0; i < ys.length; i++) {
|
|
184
|
+
var val = ys[i];
|
|
185
|
+
if (val == null || val === 0) continue;
|
|
186
|
+
var x = u.valToPos(xs[i], "x", true);
|
|
187
|
+
var y = u.valToPos(val, "y", true);
|
|
188
|
+
ctx.fillText(String(val) + unit, x, y - 4 * pxr);
|
|
189
|
+
}
|
|
190
|
+
ctx.restore();
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function escapeText(s) {
|
|
195
|
+
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 每条 series 跳过 null,找离 hoveredIdx 最近的有值索引(uPlot nearest-non-null 标准做法)。
|
|
199
|
+
// 多人并集对齐后绝大多数点位只有一个人有值,靠它让每条线在任意 x 都吸附高亮到自己最近的真实点。
|
|
200
|
+
function nearestNonNull(u, seriesIdx, hoveredIdx) {
|
|
201
|
+
var d = u.data[seriesIdx];
|
|
202
|
+
if (d[hoveredIdx] != null) return hoveredIdx;
|
|
203
|
+
var len = d.length;
|
|
204
|
+
var lo = hoveredIdx;
|
|
205
|
+
var hi = hoveredIdx;
|
|
206
|
+
while (lo >= 0 || hi < len) {
|
|
207
|
+
lo--;
|
|
208
|
+
hi++;
|
|
209
|
+
if (lo >= 0 && d[lo] != null) return lo;
|
|
210
|
+
if (hi < len && d[hi] != null) return hi;
|
|
211
|
+
}
|
|
212
|
+
return hoveredIdx;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 2D 吸附:横向极窄的尖峰里,多个数据点 x 几乎重合,只按 x 最近会永远吸到靠下的点、读不到峰顶。
|
|
216
|
+
// 这里在 hoveredIdx 左右一个很窄的 x 像素窗口内,改取离鼠标「2D(x+y)最近」的真实点:
|
|
217
|
+
// 鼠标移到竖线顶端就能命中峰值点。窗口外(稀疏区)只剩 hoveredIdx 一个候选,行为与默认一致。
|
|
218
|
+
// 窗口内无有效点 / 无鼠标像素时退回 nearestNonNull。
|
|
219
|
+
function nearestPointIdx(u, seriesIdx, hoveredIdx) {
|
|
220
|
+
var d = u.data[seriesIdx];
|
|
221
|
+
var cl = u.cursor.left;
|
|
222
|
+
var ct = u.cursor.top;
|
|
223
|
+
if (cl == null || ct == null || cl < 0 || ct < 0) {
|
|
224
|
+
return nearestNonNull(u, seriesIdx, hoveredIdx);
|
|
225
|
+
}
|
|
226
|
+
var xs = u.data[0];
|
|
227
|
+
var scaleKey = u.series[seriesIdx].scale || "y";
|
|
228
|
+
var XWIN = 16;
|
|
229
|
+
var best = -1;
|
|
230
|
+
var bestDist = Infinity;
|
|
231
|
+
for (var i = hoveredIdx; i >= 0; i--) {
|
|
232
|
+
var xl = u.valToPos(xs[i], "x");
|
|
233
|
+
if (i !== hoveredIdx && Math.abs(xl - cl) > XWIN) break;
|
|
234
|
+
if (d[i] == null) continue;
|
|
235
|
+
var yl = u.valToPos(d[i], scaleKey);
|
|
236
|
+
var ax = xl - cl;
|
|
237
|
+
var ay = yl - ct;
|
|
238
|
+
var dl = ax * ax + ay * ay;
|
|
239
|
+
if (dl < bestDist) { bestDist = dl; best = i; }
|
|
240
|
+
}
|
|
241
|
+
for (var j = hoveredIdx + 1; j < d.length; j++) {
|
|
242
|
+
var xr = u.valToPos(xs[j], "x");
|
|
243
|
+
if (Math.abs(xr - cl) > XWIN) break;
|
|
244
|
+
if (d[j] == null) continue;
|
|
245
|
+
var yr = u.valToPos(d[j], scaleKey);
|
|
246
|
+
var bx = xr - cl;
|
|
247
|
+
var by = yr - ct;
|
|
248
|
+
var dr = bx * bx + by * by;
|
|
249
|
+
if (dr < bestDist) { bestDist = dr; best = j; }
|
|
250
|
+
}
|
|
251
|
+
return best >= 0 ? best : nearestNonNull(u, seriesIdx, hoveredIdx);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// tooltip 顶部的 x 标签:time 图显示本地 MM-DD HH:mm,category 图显示对应文本标签。
|
|
255
|
+
function formatXHead(spec, u, idx) {
|
|
256
|
+
var xv = u.data[0][idx];
|
|
257
|
+
if (spec.xType === "category") {
|
|
258
|
+
var li = Math.round(xv);
|
|
259
|
+
return spec.xLabels && spec.xLabels[li] != null ? spec.xLabels[li] : "";
|
|
260
|
+
}
|
|
261
|
+
var d = new Date(xv * 1000);
|
|
262
|
+
function p(n) { return (n < 10 ? "0" : "") + n; }
|
|
263
|
+
return p(d.getMonth() + 1) + "-" + p(d.getDate()) + " " + p(d.getHours()) + ":" + p(d.getMinutes());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 跟随鼠标的 tooltip:ready 时建浮层、setCursor 时按 cursor.idxs(每条 series 经 dataIdx 吸附后的索引)
|
|
267
|
+
// 汇总当前各 series 读数。被 legend toggle 隐藏或该处无值的 series 跳过,不占行。
|
|
268
|
+
function makeTooltipHooks(spec, seriesSpecs) {
|
|
269
|
+
var unit = spec.yUnit || "";
|
|
270
|
+
var create = function (u) {
|
|
271
|
+
var tt = document.createElement("div");
|
|
272
|
+
tt.className = "ccus-tooltip";
|
|
273
|
+
tt.style.display = "none";
|
|
274
|
+
u.over.appendChild(tt);
|
|
275
|
+
u._ccusTip = tt;
|
|
276
|
+
};
|
|
277
|
+
var update = function (u) {
|
|
278
|
+
var tt = u._ccusTip;
|
|
279
|
+
if (!tt) return;
|
|
280
|
+
var idx = u.cursor.idx;
|
|
281
|
+
if (idx == null) { tt.style.display = "none"; return; }
|
|
282
|
+
var idxs = u.cursor.idxs || [];
|
|
283
|
+
var rows = "";
|
|
284
|
+
for (var i = 1; i < u.series.length; i++) {
|
|
285
|
+
if (u.series[i].show === false) continue;
|
|
286
|
+
var di = idxs[i];
|
|
287
|
+
if (di == null) continue;
|
|
288
|
+
var val = u.data[i][di];
|
|
289
|
+
if (val == null) continue;
|
|
290
|
+
var spc = seriesSpecs[i - 1] || {};
|
|
291
|
+
rows += '<div class="ccus-tip-row"><span class="ccus-tip-dot" style="background:' + spc.stroke + '"></span>'
|
|
292
|
+
+ '<span class="ccus-tip-label">' + escapeText(spc.label) + '</span>'
|
|
293
|
+
+ '<span class="ccus-tip-val">' + round1(val) + unit + '</span></div>';
|
|
294
|
+
}
|
|
295
|
+
if (!rows) { tt.style.display = "none"; return; }
|
|
296
|
+
tt.innerHTML = '<div class="ccus-tip-head">' + escapeText(formatXHead(spec, u, idx)) + '</div>' + rows;
|
|
297
|
+
tt.style.display = "block";
|
|
298
|
+
var left = u.cursor.left;
|
|
299
|
+
var top = u.cursor.top;
|
|
300
|
+
var x = left + 14;
|
|
301
|
+
var y = top + 14;
|
|
302
|
+
if (x + tt.offsetWidth > u.over.clientWidth) x = left - tt.offsetWidth - 14;
|
|
303
|
+
if (x < 0) x = 4;
|
|
304
|
+
if (y + tt.offsetHeight > u.over.clientHeight) y = u.over.clientHeight - tt.offsetHeight - 4;
|
|
305
|
+
if (y < 0) y = 4;
|
|
306
|
+
tt.style.left = x + "px";
|
|
307
|
+
tt.style.top = y + "px";
|
|
308
|
+
};
|
|
309
|
+
return { ready: [create], setCursor: [update] };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildOpts(spec, el) {
|
|
313
|
+
var unit = spec.yUnit || "";
|
|
314
|
+
var valueFmt = makeValueFmt(unit);
|
|
315
|
+
|
|
316
|
+
var series = [{}];
|
|
317
|
+
spec.series.forEach(function (s) {
|
|
318
|
+
var ser = {
|
|
319
|
+
label: s.label,
|
|
320
|
+
stroke: s.stroke,
|
|
321
|
+
width: s.width || 2,
|
|
322
|
+
value: valueFmt,
|
|
323
|
+
spanGaps: true,
|
|
324
|
+
};
|
|
325
|
+
if (s.fill) ser.fill = s.fill;
|
|
326
|
+
if (s.dash) ser.dash = s.dash;
|
|
327
|
+
if (s.scale) ser.scale = s.scale;
|
|
328
|
+
if (spec.bars) {
|
|
329
|
+
ser.paths = uPlot.paths.bars({ size: [0.62, 48], align: 0 });
|
|
330
|
+
ser.points = { show: false };
|
|
331
|
+
}
|
|
332
|
+
series.push(ser);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
var xAxis = {
|
|
336
|
+
stroke: THEME.axis,
|
|
337
|
+
grid: { stroke: THEME.grid, width: 1 },
|
|
338
|
+
ticks: { stroke: THEME.grid, width: 1 },
|
|
339
|
+
};
|
|
340
|
+
if (spec.xType === "category") {
|
|
341
|
+
xAxis.splits = function () {
|
|
342
|
+
var n = spec.xLabels ? spec.xLabels.length : 0;
|
|
343
|
+
return Array.from({ length: n }, function (_, i) { return i; });
|
|
344
|
+
};
|
|
345
|
+
xAxis.values = function (u, splits) {
|
|
346
|
+
return splits.map(function (v) {
|
|
347
|
+
if (Math.abs(v - Math.round(v)) > 0.01) return null;
|
|
348
|
+
var i = Math.round(v);
|
|
349
|
+
return spec.xLabels && spec.xLabels[i] != null ? spec.xLabels[i] : "";
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
} else {
|
|
353
|
+
// time 轴:用 24 小时制刻度(替换 uPlot 默认 12h + am/pm)。
|
|
354
|
+
xAxis.values = timeAxisVals;
|
|
355
|
+
}
|
|
356
|
+
var yAxis = {
|
|
357
|
+
stroke: THEME.axis,
|
|
358
|
+
grid: { stroke: THEME.grid, width: 1 },
|
|
359
|
+
ticks: { stroke: THEME.grid, width: 1 },
|
|
360
|
+
values: function (u, splits) {
|
|
361
|
+
return splits.map(function (v) { return round1(v) + unit; });
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
var scales = { x: { time: spec.xType === "time" } };
|
|
366
|
+
if (spec.xType === "category") {
|
|
367
|
+
scales.x = {
|
|
368
|
+
time: false,
|
|
369
|
+
range: function (u, min, max) { return [Math.floor(min) - 0.5, Math.ceil(max) + 0.5]; },
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
if (spec.yRange) {
|
|
373
|
+
scales.y = { range: spec.yRange };
|
|
374
|
+
} else if (spec.yMin0) {
|
|
375
|
+
scales.y = {
|
|
376
|
+
range: function (u, min, max) {
|
|
377
|
+
var hi = max == null || max <= 0 ? 1 : max;
|
|
378
|
+
return [0, hi];
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
var axes = [xAxis, yAxis];
|
|
384
|
+
if (spec.y2) {
|
|
385
|
+
var y2unit = spec.y2.unit || "";
|
|
386
|
+
scales[spec.y2.scale] = spec.y2.range
|
|
387
|
+
? { range: spec.y2.range }
|
|
388
|
+
: (spec.y2.min0
|
|
389
|
+
? { range: function (u, min, max) { var hi = max == null || max <= 0 ? 1 : max; return [0, hi]; } }
|
|
390
|
+
: {});
|
|
391
|
+
axes.push({
|
|
392
|
+
scale: spec.y2.scale,
|
|
393
|
+
side: 1,
|
|
394
|
+
stroke: THEME.axis,
|
|
395
|
+
grid: { show: false },
|
|
396
|
+
ticks: { stroke: THEME.grid, width: 1 },
|
|
397
|
+
values: function (u, splits) { return splits.map(function (v) { return round1(v) + y2unit; }); },
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
var opts = {
|
|
402
|
+
width: el.clientWidth || 920,
|
|
403
|
+
height: spec.height || 280,
|
|
404
|
+
scales: scales,
|
|
405
|
+
series: series,
|
|
406
|
+
axes: axes,
|
|
407
|
+
// 关掉 uPlot 原生底部 legend(每条 series 一行),改用 buildLegend 自绘「每人一项」图例。
|
|
408
|
+
legend: { show: false },
|
|
409
|
+
// dataIdx 2D 吸附:横向重合的尖峰按鼠标高度命中峰顶,稀疏区/缺失退回最近真实点。
|
|
410
|
+
cursor: { show: true, dataIdx: nearestPointIdx },
|
|
411
|
+
};
|
|
412
|
+
var hooks = makeTooltipHooks(spec, spec.series);
|
|
413
|
+
if (spec.bars) {
|
|
414
|
+
hooks.draw = [barLabelHook(unit)];
|
|
415
|
+
}
|
|
416
|
+
opts.hooks = hooks;
|
|
417
|
+
return opts;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 自绘图例:每个 legendGroups 项渲染成一个「名字」chip,点击同时切换该项下所有 series 显隐
|
|
421
|
+
// (多人时一个人名联动 5h + 7d 两条线)。人名按 spec 原样(小写),不强制大写。
|
|
422
|
+
function buildLegend(u, spec, host) {
|
|
423
|
+
var groups = spec.legendGroups;
|
|
424
|
+
if (!groups || !groups.length || !host.parentNode) return;
|
|
425
|
+
var wrap = document.createElement("div");
|
|
426
|
+
wrap.className = "ccus-legend";
|
|
427
|
+
groups.forEach(function (g) {
|
|
428
|
+
var item = document.createElement("span");
|
|
429
|
+
item.className = "ccus-legend-item";
|
|
430
|
+
var dot = document.createElement("span");
|
|
431
|
+
dot.className = "ccus-legend-dot";
|
|
432
|
+
dot.style.background = g.color;
|
|
433
|
+
var label = document.createElement("span");
|
|
434
|
+
label.textContent = g.label;
|
|
435
|
+
item.appendChild(dot);
|
|
436
|
+
item.appendChild(label);
|
|
437
|
+
item.addEventListener("click", function () {
|
|
438
|
+
var first = g.seriesIdx[0];
|
|
439
|
+
var shown = u.series[first] ? u.series[first].show !== false : true;
|
|
440
|
+
var next = !shown;
|
|
441
|
+
g.seriesIdx.forEach(function (si) { u.setSeries(si, { show: next }); });
|
|
442
|
+
item.classList.toggle("is-off", !next);
|
|
443
|
+
});
|
|
444
|
+
wrap.appendChild(item);
|
|
445
|
+
});
|
|
446
|
+
host.parentNode.insertBefore(wrap, host.nextSibling);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
var instances = [];
|
|
450
|
+
function initOne(node) {
|
|
451
|
+
var id = node.getAttribute("data-target");
|
|
452
|
+
var el = id ? document.getElementById(id) : null;
|
|
453
|
+
if (!el) return;
|
|
454
|
+
var payload;
|
|
455
|
+
try { payload = JSON.parse(node.textContent); } catch (e) { return; }
|
|
456
|
+
var opts = buildOpts(payload.spec, el);
|
|
457
|
+
var u = new uPlot(opts, payload.data, el);
|
|
458
|
+
buildLegend(u, payload.spec, el);
|
|
459
|
+
instances.push({ u: u, el: el, height: payload.spec.height || 280 });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function initAll() {
|
|
463
|
+
var nodes = document.querySelectorAll("script.ccus-chart");
|
|
464
|
+
for (var i = 0; i < nodes.length; i++) initOne(nodes[i]);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
var resizeTimer = null;
|
|
468
|
+
window.addEventListener("resize", function () {
|
|
469
|
+
if (resizeTimer) clearTimeout(resizeTimer);
|
|
470
|
+
resizeTimer = setTimeout(function () {
|
|
471
|
+
instances.forEach(function (inst) {
|
|
472
|
+
inst.u.setSize({ width: inst.el.clientWidth || 920, height: inst.height });
|
|
473
|
+
});
|
|
474
|
+
}, 120);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
if (document.readyState === "loading") {
|
|
478
|
+
document.addEventListener("DOMContentLoaded", initAll);
|
|
479
|
+
} else {
|
|
480
|
+
initAll();
|
|
481
|
+
}
|
|
482
|
+
})();
|
|
483
|
+
</script>`;
|
|
484
|
+
}
|
|
485
|
+
//# sourceMappingURL=chart-assets.js.map
|