base-paint 0.3.1 → 0.3.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/dist/blocks.d.ts +141 -4
- package/dist/blocks.d.ts.map +1 -1
- package/dist/blocks.js +1015 -25
- package/dist/blocks.js.map +1 -1
- package/dist/charts.d.ts +6 -4
- package/dist/charts.d.ts.map +1 -1
- package/dist/charts.js +378 -64
- package/dist/charts.js.map +1 -1
- package/dist/controls.d.ts +20 -5
- package/dist/controls.d.ts.map +1 -1
- package/dist/controls.js +246 -34
- package/dist/controls.js.map +1 -1
- package/dist/font.d.ts +3 -3
- package/dist/font.js +3 -3
- package/dist/help.d.ts +3 -3
- package/dist/help.js +12 -12
- package/dist/help.js.map +1 -1
- package/dist/helpShell.d.ts.map +1 -1
- package/dist/helpShell.js +2 -2
- package/dist/helpShell.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/injector.d.ts +1 -1
- package/dist/output/saveHtml.d.ts +37 -2
- package/dist/output/saveHtml.d.ts.map +1 -1
- package/dist/output/saveHtml.js +125 -15
- package/dist/output/saveHtml.js.map +1 -1
- package/dist/pageShapes.d.ts +117 -0
- package/dist/pageShapes.d.ts.map +1 -0
- package/dist/pageShapes.js +366 -0
- package/dist/pageShapes.js.map +1 -0
- package/dist/pageUi.d.ts +37 -0
- package/dist/pageUi.d.ts.map +1 -0
- package/dist/pageUi.js +237 -0
- package/dist/pageUi.js.map +1 -0
- package/dist/spec/charts.d.ts +12 -3
- package/dist/spec/charts.d.ts.map +1 -1
- package/dist/spec/charts.js +1 -1
- package/dist/spec/charts.js.map +1 -1
- package/dist/spec/controls.d.ts +30 -5
- package/dist/spec/controls.d.ts.map +1 -1
- package/dist/spec/controls.js +14 -3
- package/dist/spec/controls.js.map +1 -1
- package/dist/spec/help.d.ts +4 -4
- package/dist/spec/help.d.ts.map +1 -1
- package/dist/spec/help.js +2 -2
- package/dist/spec/index.d.ts +6 -3
- package/dist/spec/index.d.ts.map +1 -1
- package/dist/spec/index.js +27 -2
- package/dist/spec/index.js.map +1 -1
- package/dist/style.d.ts.map +1 -1
- package/dist/style.js +265 -47
- package/dist/style.js.map +1 -1
- package/package.json +1 -1
package/dist/charts.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** base-paint/charts:#78 图表层运行时(契约 §3.5/§6.5)。
|
|
2
2
|
*
|
|
3
|
-
*
|
|
3
|
+
* 实施冻结面里 #78 的三条运行时出口:`charts`(8 方法)/`buildChartsHelpersJs`;
|
|
4
4
|
* `ChartError` 与本文件**不**从 `src/index.ts` 导出(冻结面 44 条内无该运行时条目,
|
|
5
5
|
* 与 `TemplateError`/`ControlsError` 同口径:调用方按 `name === 'ChartError'` + `code` 判定)。
|
|
6
6
|
*
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* 图标 📊 与提示「有记录后自动生成图表」沿用旧基线(纯 UI 文案,非契约常量);
|
|
26
26
|
* - `dotStyle`/`line.dotSize`/`combo.tooltip` 旧版是死参数,按 R21 实现为**新能力**:
|
|
27
27
|
* `dotStyle` = 数据点内联 `style` 文本(逐点样式覆盖),`dotSize` = 点直径,`tooltip` = `data-tip` 数据属性;
|
|
28
|
-
* - `avgLine(n)` =
|
|
28
|
+
* - `avgLine(n)` = 旧语义的**均线序列**(窗口统一为 `3..items.length`,`old-baseline.md` §2.2),
|
|
29
29
|
* 不是水平线;`emptyText`/`compact`/`bar.grid` 旧版是死参数,按冻结类型补齐为有效选项;
|
|
30
30
|
* - `prefix` 覆盖只影响 `buildChartsHelpersJs` **产出的 CSS 文本**;`charts.*` 产出的 HTML 恒用
|
|
31
31
|
* `STYLE_PREFIX` 命名空间(与 `SharedHelpersInput.dataAttr` 覆盖口径同构,不一致由调用方自负)。
|
|
@@ -95,6 +95,9 @@ const MUTED_COLOR = 'var(--fg3,#86868b)';
|
|
|
95
95
|
const DOT_FACE_COLOR = 'var(--card,#ffffff)';
|
|
96
96
|
const DOT_DEFAULT_PX = 7;
|
|
97
97
|
const SCATTER_DOT_DEFAULT_PX = 9;
|
|
98
|
+
/** 折线数据点抽稀上限(#424):`showDots` **未显式给**且点数超过它时不逐点画圆——
|
|
99
|
+
* 逐点画必重叠成一团(90 点 / 580 单位宽 → 点距 6.1,圆径 7)。显式 `showDots` 一律照办。 */
|
|
100
|
+
const DOT_STRIDE_MAX = 30;
|
|
98
101
|
/** scatter 缺省 Y 轴刻度条数(旧 `charts.js:854` `yTicks:4`;冻结 `ScatterChartOptions` 无该字段,
|
|
99
102
|
* 故按**缺省行为**渲染 4 条、不新增可关闭开关——登记为契约缺口,归后续票,R2-G1 裁定)。 */
|
|
100
103
|
const SCATTER_Y_TICKS = 4;
|
|
@@ -102,6 +105,7 @@ const EMPTY_ICON = '📊';
|
|
|
102
105
|
const EMPTY_HINT = '有记录后自动生成图表';
|
|
103
106
|
const DONUT_ZERO_HINT = '合计为零, 无环形数据';
|
|
104
107
|
const OWN_SCALE_NOTE = '各指标独立刻度';
|
|
108
|
+
/** 水平网格线**无 Y 轴标注时**的条数(#512:有标注时改按标注条数画,见 `gridSvg`)。 */
|
|
105
109
|
const GRID_LINES = 3;
|
|
106
110
|
/** 稳定 32 位散列(FNV-1a)——渐变 `<linearGradient id>` 由**渐变参数**派生,
|
|
107
111
|
* 同一输入恒得同一 id、不同参数得不同 id,且**不跨调用共享可变状态**(R1-A1/FX-78-A1b-11)。 */
|
|
@@ -173,7 +177,7 @@ function normalizeItems(raw, kind, allowNull, mode = 'value') {
|
|
|
173
177
|
return out;
|
|
174
178
|
});
|
|
175
179
|
}
|
|
176
|
-
/** `pct` 非数 → `pct-invalid
|
|
180
|
+
/** `pct` 非数 → `pct-invalid`;超界统一为 0~100(旧 §6.5 逐字行为)。 */
|
|
177
181
|
function normalizePct(raw, kind) {
|
|
178
182
|
if (!isNum(raw))
|
|
179
183
|
badPct('charts.' + kind + ': pct 无效: ' + String(raw));
|
|
@@ -194,7 +198,7 @@ function resolveCommon(raw, def) {
|
|
|
194
198
|
: undefined,
|
|
195
199
|
format: o !== undefined && typeof o.format === 'function' ? o.format : undefined,
|
|
196
200
|
labels: labels === 'all' || labels === 'none' || labels === 'select' || labels === 'edge' ? labels : def.labels,
|
|
197
|
-
showValues: showValues === true || showValues === false || showValues === 'edge' ? showValues : def.showValues,
|
|
201
|
+
showValues: showValues === true || showValues === false || showValues === 'edge' || showValues === 'last' ? showValues : def.showValues,
|
|
198
202
|
labelRotate: numOr(o === undefined ? undefined : o.labelRotate, 0),
|
|
199
203
|
yMin: o !== undefined && isNum(o.yMin) ? o.yMin : undefined,
|
|
200
204
|
yMax: o !== undefined && isNum(o.yMax) ? o.yMax : undefined,
|
|
@@ -229,13 +233,22 @@ function actionAttrs(common, index) {
|
|
|
229
233
|
function tipAttrs(common, makeText) {
|
|
230
234
|
return common.tooltip ? ' data-tip="' + esc(makeText()) + '"' : '';
|
|
231
235
|
}
|
|
232
|
-
/**
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
|
|
236
|
+
/** 八个 kind **一律** `preserveAspectRatio="xMidYMid meet"`(等比,不拉伸文字)。
|
|
237
|
+
*
|
|
238
|
+
* #507(2026-09-15 公共层视觉机制返工)撤掉了折线/组合/散点三族的 `none` 满宽拉伸:
|
|
239
|
+
* 这三族当年取 `none` 的**唯一理由是**移动端 CSS 把 svg 盒高钉成 `lineHeightMobilePx`(150px),
|
|
240
|
+
* 非等比压低后 `meet` 会等比缩到 228.6px 宽、左右各留 ~73px 空白(R2-N9)。那条钉高在
|
|
241
|
+
* #424 返工时就撤了(改成 `height:auto` 按 viewBox 长宽比派生高度,见 `chartsCss` 的 ≤720px 段),
|
|
242
|
+
* **盒宽高比自此恒等于 viewBox 长宽比** ⇒ `none` 与 `meet` 逐像素同解,`none` 只剩两个隐患:
|
|
243
|
+
* ① 将来任一处把盒高钉回固定 px,图内文字立刻被横向抻宽(1000px 宽容器下 1.6×,字脸变扁);
|
|
244
|
+
* ② 读代码的人会以为本族真的在拉伸。
|
|
245
|
+
* **本条只改属性,不改任何坐标**:坐标唯一性走 `CHART_COORD_RULE = 'viewBox-only'`(映射函数
|
|
246
|
+
* 按 `common.width/height` 算,`meet` 与 `none` 下同一份坐标同解)。描边的
|
|
247
|
+
* `vector-effect="non-scaling-stroke"` 照旧保留(等比缩放下同样有用:描边不随缩放变粗)。 */
|
|
248
|
+
function svgOpen(common, extra) {
|
|
236
249
|
return '<svg class="' + STYLE_PREFIX + 'charts-svg' + (extra === undefined ? '' : ' ' + extra) + '"'
|
|
237
250
|
+ ' viewBox="0 0 ' + n1(common.width) + ' ' + n1(common.height) + '"'
|
|
238
|
-
+ ' preserveAspectRatio="
|
|
251
|
+
+ ' preserveAspectRatio="xMidYMid meet" role="img" focusable="false">';
|
|
239
252
|
}
|
|
240
253
|
/** 空态联动(`CHART_EMPTY_RULE = 'emptyState'`):`empty:true`/`points:0`,不抛错。 */
|
|
241
254
|
function emptyOutput(kind, raw, hint) {
|
|
@@ -257,17 +270,99 @@ function makeFrame(common, insets) {
|
|
|
257
270
|
const y1 = Math.max(y0 + 1, common.height - insets.bottom);
|
|
258
271
|
return { x0, x1, y0, y1, w: x1 - x0, h: y1 - y0 };
|
|
259
272
|
}
|
|
260
|
-
/** 留白进 viewBox(容器零 padding):左留白含刻度文字宽度。
|
|
273
|
+
/** 留白进 viewBox(容器零 padding):左留白含刻度文字宽度。
|
|
274
|
+
*
|
|
275
|
+
* `minLeft`/`minBottom`(#424 返工):折线的留白必须能容下**移动端**那一档字号——媒体查询把
|
|
276
|
+
* 折线文字放大到 17–18 用户单位(见 `chartsCss` 的 ≤720px 段与 `LINE_TEXT_MOBILE`),留白却是
|
|
277
|
+
* 双端共用的用户单位 → 只按桌面 9.5–10.5 估会让 390px 下的「70.5kg」顶出 viewBox 左沿。 */
|
|
261
278
|
function insetsFor(common, opts) {
|
|
262
279
|
const padX = common.compact ? 6 : 14;
|
|
263
280
|
const padY = common.compact ? 4 : 8;
|
|
264
281
|
return {
|
|
265
|
-
left: padX + opts.tickWidth,
|
|
282
|
+
left: Math.max(padX + opts.tickWidth, opts.minLeft ?? 0),
|
|
266
283
|
right: padX,
|
|
267
|
-
top: padY + (common.showValues === false ? 0 : opts.valueHeight),
|
|
268
|
-
bottom: padY + opts.labelHeight,
|
|
284
|
+
top: Math.max(padY + (common.showValues === false ? 0 : opts.valueHeight), opts.minTop ?? 0),
|
|
285
|
+
bottom: Math.max(padY + opts.labelHeight, opts.minBottom ?? 0),
|
|
269
286
|
};
|
|
270
287
|
}
|
|
288
|
+
/** #424 返工:移动端(≤720px)折线的字号(**用户单位**)。
|
|
289
|
+
* viewBox 580×260 在 390px 手机上按宽度缩到 scale≈0.57(`width:100%` + 按 viewBox 比例算高,
|
|
290
|
+
* 见 `chartsCss` 的 ≤720px 段)——桌面档 9.5–10.5 单位实渲只剩 5.4px,肉眼不可读。
|
|
291
|
+
* 这里按 1/0.57≈1.75 倍提字号,实渲回到 ≈11.3px(390px 档)。
|
|
292
|
+
*
|
|
293
|
+
* t512 收尾(复审 P1):这一档原取 20 单位(512 档盒宽 450 → 实渲 15.52px)。复审表里那句「512 档刻度
|
|
294
|
+
* **实渲** 20px = 正文 1.6 倍」是**读错了量纲** —— 同一列里 820 档写 12.4、1000/1440 档写 10.8,逐值
|
|
295
|
+
* 正是本文件的**用户单位**常量;实渲像素 = 用户单位 × 盒宽 ÷ 580,20 单位在 512 档实渲 15.52px,
|
|
296
|
+
* 约等于正文 12–13px 的 1.2 倍。复审要的实渲 14–15px 里只有 15.0px 这一点能落:往下压到 15 单位
|
|
297
|
+
* 就只剩 11.64px,比正文还小,并破 #512 的「四档实渲 ≥15px」断言(`test/charts.test.mjs` 的 H.t512)。
|
|
298
|
+
* 故取 15 ÷ (450/580) = 19.33 → **19.4 单位**(512 档实渲 15.05px,留 0.4% 浮点余量,贴住那条下沿)。
|
|
299
|
+
* 要真压到 14px,得先改 #512 的 15px 下沿(契约变更,另开票)。
|
|
300
|
+
* 同一份数字既进 `chartsCss` 的媒体查询,也用来估留白(留白双端共用 → 取更大的这一档)。 */
|
|
301
|
+
const LINE_TEXT_MOBILE = { tick: 19.4, xlabel: 19.4, value: 19.4, last: 19.4, mark: 19.4 };
|
|
302
|
+
/** t512:折线族字号的**档位表**(用户单位)。
|
|
303
|
+
*
|
|
304
|
+
* svg 文本的 `font-size` 写在**用户单位**上,会被 viewBox 等比缩放(`preserveAspectRatio="xMidYMid meet"`
|
|
305
|
+
* + `width:100%;height:auto` → 两轴同倍),实渲像素 = 用户单位 × 缩放比,而缩放比 = svg 盒宽 ÷ 580
|
|
306
|
+
* (`LINE_DEFAULT_WIDTH`)——盒宽随视口变,所以**同一个用户单位在不同视口实渲出不同像素**。
|
|
307
|
+
* 只留 ≤720px 移动档(当时 20 单位,t512 收尾收到 19.4)那一档、721px 起落回桌面档 9.5/10 单位,就会在这条断点上**非单调地跳**:
|
|
308
|
+
* 720px 档盒宽 652 → 实渲 22.5px,721px 档盒宽 651 只剩 10.7px,再往宽走才慢慢回到 15px。
|
|
309
|
+
*
|
|
310
|
+
* 逐档补偿的算式:**用户单位 = 目标像素 ÷ 该档实测缩放比**。
|
|
311
|
+
* 缩放比取自 `docs/base/base-render/` 下 t507 证据的 CDP 四档实测(盒宽 ÷ 580):
|
|
312
|
+
* 512px → 盒 450×201.72 → 0.7759;820px → 750×336.20 → 1.2931;1000px/1440px → 930×416.89 → 1.6034
|
|
313
|
+
* (930 是卡片宽度上限,故 1000px 与 1440px 同档、实渲同值)。
|
|
314
|
+
*
|
|
315
|
+
* 三档结果(`chartsCss` 的 ≤720px/721–875px/≥876px 三段逐字用这三个数):
|
|
316
|
+
* 19.4 单位 → 15.05px(512px,t512 收尾前是 20 单位 → 15.5px);12.4 单位 → 16.0px(820px);
|
|
317
|
+
* 10.8 单位 → 17.3px(1000px/1440px)—— 四档**单调不降且 ≥15px**,不再随盒子宽度来回跳。
|
|
318
|
+
*
|
|
319
|
+
* **不变式(别越)**:任何档位的用户单位都不得超过 `LINE_TEXT_MOBILE.tick`(19.4)。
|
|
320
|
+
* `insetsFor` 的左留白是按**移动档那一档(19.4 单位)**估的(`charts.ts:843`,留白是双端共用的用户单位),
|
|
321
|
+
* 抬高它会让「70.5kg」这类刻度文字在**每个**视口都顶出 viewBox 左沿。故本表只抬中间档与宽档。
|
|
322
|
+
*
|
|
323
|
+
* **已知余量(如实记账)**:媒体查询只能按**视口**分档,而缩放比按**盒宽**走,同一档内仍是变化的
|
|
324
|
+
* —— 721–875px 档的下沿(721px)实渲 14.0px、上沿(875px)17.2px;这是视口分档的固有锯齿,
|
|
325
|
+
* 比改前同一段的 10.7–13.9px 已经抬高,要再抹平得按容器宽分档(另一票的事)。 */
|
|
326
|
+
const LINE_TEXT_WIDE_UNITS = 12.4;
|
|
327
|
+
const LINE_TEXT_FULL_UNITS = 10.8;
|
|
328
|
+
/** ≥876px 档的媒体查询下沿:由 `LINE_TEXT_FULL_UNITS` 的 ≥15px 要求**反解**得到——
|
|
329
|
+
* 缩放比须 ≤ 15 ÷ 10.8 = 1.3889 → 盒宽 ≤ 1.3889 × 580 = 805.6 → 视口 ≤ 875.6,取下沿 876。 */
|
|
330
|
+
const LINE_TEXT_WIDE_MAX_PX = 875;
|
|
331
|
+
/** 折线 X 标签行相对绘图区底边(`frame.y1`)的间距(用户单位)。最低那条刻度文字已抬到轴线上方
|
|
332
|
+
* (见 `ticksSvg` 的 `labelDy`),其文字盒下沿到 y1 附近;X 标签(19.4 单位)文字盒上沿顶在
|
|
333
|
+
* y1+gap−21 附近 —— gap ≥ 33 才不相撞(本条按 20 单位算出的下限,t512 收尾改小后仍富余;
|
|
334
|
+
* 实测 gap 34 时 390px 下余 ≈4.5 用户单位)。 */
|
|
335
|
+
const LINE_LABEL_GAP = 34;
|
|
336
|
+
/** 折线绘图区底留白下限:容下 `LINE_LABEL_GAP` + X 标签降部(19.4×0.25 = 4.9)。 */
|
|
337
|
+
const LINE_BOTTOM_MIN = 40;
|
|
338
|
+
/** 末值标签相对末点的抬升(用户单位):文字盒高随字号走(移动端 19.4 单位那一档),抬 6 单位时
|
|
339
|
+
* 390px 下标签盒底与数据线只余 0.4px、15 个折线顶点落在盒内 —— 按 0.7×字号 抬开(0.7×19.4 = 13.6,
|
|
340
|
+
* 取值 14 仍在 0.7× 之上,改字号后一字未动)。 */
|
|
341
|
+
const LINE_LAST_LABEL_LIFT = 14;
|
|
342
|
+
/** t512(最后一公里)· 折线族顶上那两条硬账。
|
|
343
|
+
*
|
|
344
|
+
* ① **顶端刻度标注得在框里**:刻度文字的落点是 `y = ty + labelDy`(基线),实渲文字盒高约 1.33em、
|
|
345
|
+
* 基线以上约占 1.04em —— 顶端那条刻度(`i = count−1`,`ty = frame.y0`)的盒子因此探出 viewBox
|
|
346
|
+
* 上沿被裁(复审实测 1000/1440 档 `relTop = −0.36px`、512 档 −7.47px)。留白旧口径只认
|
|
347
|
+
* `padY + valueHeight`(`showValues:false` 时仅 8 单位),而字号在 ≤720px 档是 19.4 单位(原 20)
|
|
348
|
+
* ⇒ 需要的上沿就是那 1.04em。留白是双端共用的**用户单位**,故按**最大那一档字号**
|
|
349
|
+
* (`LINE_TEXT_MOBILE.tick = 19.4`)算:ceil(1.04 × 19.4) = ceil(20.18) = 21 单位
|
|
350
|
+
* (20 单位时同为 21),四档一律够。
|
|
351
|
+
* ② **峰值与顶端刻度线的余量**:派生域旧口径两端各外扩 6%(`domainOf`)⇒ 峰值只离顶端刻度线
|
|
352
|
+
* 5.36% 的绘图区高,顶端那条线读成「标题下划线」(复审 §三-2:图高 416.89px、顶线在 12.83px、
|
|
353
|
+
* 512 档峰值墨顶离顶线只剩 6.13px)。`LINE_PEAK_HEADROOM` 把**上端**补到绘图区高的 10%
|
|
354
|
+
* (复审给的判据是 8–12%),下端 6% 不动。显式 `yMax` 是调用方的语义(「画到这儿」),不补。 */
|
|
355
|
+
const LINE_TICK_TOP_MIN = 21;
|
|
356
|
+
const LINE_PEAK_HEADROOM = 0.1;
|
|
357
|
+
/** 文字宽度估值(用户单位):ASCII ≈0.62em、CJK 全角 ≈1em。刻度留白按它算——留白必须容下
|
|
358
|
+
* **移动端那一档字号**,否则 390px 下「70.5kg」会顶出 viewBox 左沿(`overflow:visible` 也救不了,
|
|
359
|
+
* 卡片会裁)。 */
|
|
360
|
+
function textWidthUnits(text, size) {
|
|
361
|
+
let em = 0;
|
|
362
|
+
for (const ch of text)
|
|
363
|
+
em += (ch.codePointAt(0) ?? 0) > 0x2e7f ? 1 : 0.62;
|
|
364
|
+
return em * size;
|
|
365
|
+
}
|
|
271
366
|
function xAt(frame, index, count) {
|
|
272
367
|
return count <= 1 ? frame.x0 + frame.w / 2 : frame.x0 + (frame.w * index) / (count - 1);
|
|
273
368
|
}
|
|
@@ -313,17 +408,41 @@ function domainOf(values, yMin, yMax, includeZero, padRatio = 0.06) {
|
|
|
313
408
|
hi = lo + 1;
|
|
314
409
|
return [lo, hi];
|
|
315
410
|
}
|
|
316
|
-
/** `yTicks`
|
|
411
|
+
/** `yTicks` 统一为 2-6(旧 `charts.js:498`);`false`/非数 → 0 条。 */
|
|
317
412
|
function tickCount(value) {
|
|
318
413
|
if (!isNum(value))
|
|
319
414
|
return 0;
|
|
320
415
|
return Math.max(2, Math.min(6, Math.round(value)));
|
|
321
416
|
}
|
|
417
|
+
/** t512:折线族的**共享派生域** —— `domainOf` 之上再保一条峰值余量(见 `LINE_PEAK_HEADROOM`)。
|
|
418
|
+
*
|
|
419
|
+
* 只动**派生**路径:显式 `yMin`/`yMax` 是调用方的语义(域的边界=「画到这儿」),原样返回。
|
|
420
|
+
* 上端的求法:解 `(hi′ − max) / (hi′ − lo) = r` → `hi′ = (max − r·lo) / (1 − r)`;取下端不动
|
|
421
|
+
* (`lo` 仍是 6% 外扩后的值),故上端比下端厚,峰值不会再压在顶端那条刻度线上。
|
|
422
|
+
* 单值/全等数据(`domainOf` 内部已把 `hi` 抬到 `lo + 1`)时 `want` 不会超过原 `hi`,`Math.max` 兜住。 */
|
|
423
|
+
function lineDomainOf(values, yMin, yMax) {
|
|
424
|
+
const range = domainOf(values, yMin, yMax, false);
|
|
425
|
+
if (yMax !== undefined)
|
|
426
|
+
return range;
|
|
427
|
+
let max = -Infinity;
|
|
428
|
+
for (const v of values)
|
|
429
|
+
if (v > max)
|
|
430
|
+
max = v;
|
|
431
|
+
if (max === -Infinity)
|
|
432
|
+
return range;
|
|
433
|
+
const want = (max - LINE_PEAK_HEADROOM * range[0]) / (1 - LINE_PEAK_HEADROOM);
|
|
434
|
+
return [range[0], Math.max(range[1], want)];
|
|
435
|
+
}
|
|
322
436
|
/* ── 共享 SVG 片段 ────────────────────────────────────────────────────── */
|
|
323
|
-
function gridSvg(frame) {
|
|
437
|
+
function gridSvg(frame, count = 0) {
|
|
438
|
+
/** 对齐口径(#512):线条数取**实际标注条数**,位置与 `ticksSvg` 同一条均匀标度公式
|
|
439
|
+
* (`y1 - h*i/(n-1)` ⇔ `i` 刻度值映射到 `yAt`),故每条线都压着自己那条刻度。
|
|
440
|
+
* `count < 2`(`yTicks:false`/非数 → `tickCount` 给 0)时回退旧口径 `GRID_LINES` 三条:
|
|
441
|
+
* bar/combo 与未开 `yTicks` 的 line 本就无标注,不得因此变成空白图。 */
|
|
442
|
+
const lines = count >= 2 ? count : GRID_LINES;
|
|
324
443
|
let out = '';
|
|
325
|
-
for (let g = 0; g <
|
|
326
|
-
const gy = frame.y1 - (frame.h * g) / (
|
|
444
|
+
for (let g = 0; g < lines; g += 1) {
|
|
445
|
+
const gy = frame.y1 - (frame.h * g) / (lines - 1);
|
|
327
446
|
out += '<line class="' + STYLE_PREFIX + 'charts-grid" x1="' + n1(frame.x0) + '" y1="' + n1(gy)
|
|
328
447
|
+ '" x2="' + n1(frame.x1) + '" y2="' + n1(gy) + '" stroke="' + GRID_COLOR
|
|
329
448
|
+ '" stroke-width="1" vector-effect="non-scaling-stroke"/>';
|
|
@@ -337,23 +456,44 @@ function ticksSvg(frame, lo, hi, count, format) {
|
|
|
337
456
|
for (let i = 0; i < count; i += 1) {
|
|
338
457
|
const tv = lo + ((hi - lo) * i) / (count - 1);
|
|
339
458
|
const ty = yAt(frame, tv, lo, hi);
|
|
459
|
+
/* #424 返工:最低那条刻度与绘图区底边同高(`lo` 就是下界),它的文字盒会探进 X 标签那一行。
|
|
460
|
+
* 移动端字号提到 20 单位(t512 收尾收到 19.4)后两者只余 0.3px(实测)——最低那条改到轴线上方 1 单位起基线,
|
|
461
|
+
* 其余刻度保持旧口径 `ty + 3`(旧 `charts.js` 字面值不动)。 */
|
|
462
|
+
const labelDy = i === 0 ? -1 : 3;
|
|
340
463
|
out += '<line class="' + STYLE_PREFIX + 'charts-ytick" x1="' + n1(frame.x0 - 5) + '" y1="' + n1(ty)
|
|
341
464
|
+ '" x2="' + n1(frame.x0) + '" y2="' + n1(ty) + '" stroke="' + GRID_COLOR
|
|
342
465
|
+ '" stroke-width="1" vector-effect="non-scaling-stroke"/>'
|
|
343
|
-
+ '<text class="' + STYLE_PREFIX + 'charts-tick" x="' + n1(frame.x0 - 7) + '" y="' + n1(ty +
|
|
466
|
+
+ '<text class="' + STYLE_PREFIX + 'charts-tick" x="' + n1(frame.x0 - 7) + '" y="' + n1(ty + labelDy)
|
|
344
467
|
+ '" text-anchor="end" fill="' + MUTED_COLOR + '">' + esc(fmtValue(round2(tv), format)) + '</text>';
|
|
345
468
|
}
|
|
346
469
|
return out;
|
|
347
470
|
}
|
|
348
471
|
/** X 轴标签集合:`'all'` 全量/`'edge'` 首尾/`'select'` 首+峰值+尾(line 口径)
|
|
349
472
|
* /`'none'` 无。`select='edge'` 时 `'select'` 退化为首尾——**bar 口径**:
|
|
350
|
-
* 旧 bar 的 `'select'` 只显示首尾(`charts.js:385-388`),与 line 的「首+峰值+尾」不同(R2-N7)。
|
|
351
|
-
|
|
473
|
+
* 旧 bar 的 `'select'` 只显示首尾(`charts.js:385-388`),与 line 的「首+峰值+尾」不同(R2-N7)。
|
|
474
|
+
*
|
|
475
|
+
* #424 补一条下限(只对 line 的 `'select'`):**峰值落在端点时首+峰+尾去重后只剩 2 个标签**
|
|
476
|
+
* (单调下降的体重曲线必然如此,峰值就是首日),横轴中段又变成没有时间参照。故去重后不足 3 个、
|
|
477
|
+
* 且点数 ≥3 时,补一个**离首点最远的中间点**——仍满足「首+峰+尾」,顺带给出中段参照。
|
|
478
|
+
*
|
|
479
|
+
* #567 追加 `every`(只对 `'all'`):每 k 点取一枚(含末点),给「点全画、标签抽稀」用;
|
|
480
|
+
* 非 `'all'` 或非法 k(非 ≥2 有限数)时忽略(默认行为不变)。 */
|
|
481
|
+
function labelIndexes(mode, items, select = 'peak', every) {
|
|
352
482
|
const n = items.length;
|
|
353
483
|
if (mode === 'none' || n === 0)
|
|
354
484
|
return [];
|
|
355
|
-
if (mode === 'all')
|
|
485
|
+
if (mode === 'all') {
|
|
486
|
+
if (every !== undefined && Number.isFinite(every) && Math.floor(every) >= 2) {
|
|
487
|
+
const k = Math.floor(every);
|
|
488
|
+
const picked = [];
|
|
489
|
+
for (let i = 0; i < n; i += k)
|
|
490
|
+
picked.push(i);
|
|
491
|
+
if (picked[picked.length - 1] !== n - 1)
|
|
492
|
+
picked.push(n - 1);
|
|
493
|
+
return picked;
|
|
494
|
+
}
|
|
356
495
|
return items.map((_, i) => i);
|
|
496
|
+
}
|
|
357
497
|
if (mode === 'edge' || (mode === 'select' && select === 'edge'))
|
|
358
498
|
return n === 1 ? [0] : [0, n - 1];
|
|
359
499
|
let peak = 0;
|
|
@@ -364,23 +504,31 @@ function labelIndexes(mode, items, select = 'peak') {
|
|
|
364
504
|
peak = i;
|
|
365
505
|
}
|
|
366
506
|
});
|
|
367
|
-
|
|
507
|
+
const picked = Array.from(new Set([0, peak, n - 1])).sort((a, b) => a - b);
|
|
508
|
+
if (picked.length < 3 && n >= 3) {
|
|
509
|
+
const mid = Math.round((n - 1) / 2);
|
|
510
|
+
const extra = [mid, n - 1 - mid, 1, n - 2].find((i) => i > 0 && i < n - 1 && !picked.includes(i));
|
|
511
|
+
if (extra !== undefined)
|
|
512
|
+
picked.splice(1, 0, extra);
|
|
513
|
+
}
|
|
514
|
+
return picked;
|
|
368
515
|
}
|
|
369
516
|
/* t-chartfix:X 标签的 x 标度缺省与折线点同一 `xAt` 点标度;柱族(bar/combo)
|
|
370
517
|
* 的柱列中心走 band 标度 `x0 + slot*(i+0.5)`——点标度会把首尾标签钉在绘图区
|
|
371
518
|
* 左右边缘(3 柱时偏差 48.7 单位),与柱错位。调用方按自家几何传入。 */
|
|
372
|
-
function xLabelsSvg(common, frame, items, select = 'peak', xOf = (i, n) => xAt(frame, i, n)) {
|
|
519
|
+
function xLabelsSvg(common, frame, items, select = 'peak', xOf = (i, n) => xAt(frame, i, n), gap, every) {
|
|
373
520
|
if (common.labels === 'none')
|
|
374
521
|
return '';
|
|
375
|
-
const baseY = frame.y1 + (common.compact ? 9 : 12);
|
|
376
|
-
return labelIndexes(common.labels, items, select).map((i) => {
|
|
522
|
+
const baseY = frame.y1 + (gap ?? (common.compact ? 9 : 12));
|
|
523
|
+
return labelIndexes(common.labels, items, select, every).map((i) => {
|
|
377
524
|
const x = xOf(i, items.length);
|
|
378
525
|
const rotate = common.labelRotate === 0 ? '' : ' transform="rotate(' + common.labelRotate + ' ' + n1(x) + ' ' + n1(baseY) + ')"';
|
|
379
526
|
return '<text class="' + STYLE_PREFIX + 'charts-xlabel" x="' + n1(x) + '" y="' + n1(baseY)
|
|
380
527
|
+ '" text-anchor="middle"' + rotate + ' fill="' + MUTED_COLOR + '">' + esc(items[i].label) + '</text>';
|
|
381
528
|
}).join('');
|
|
382
529
|
}
|
|
383
|
-
/** `showValues` 标签集合:`true` 全量(相邻中心距 < 26 单位跳过)/`'edge'`
|
|
530
|
+
/** `showValues` 标签集合:`true` 全量(相邻中心距 < 26 单位跳过)/`'edge'` 首尾有效点/
|
|
531
|
+
* `'last'` 只标最后一个有效点(#567)/`false` 无。 */
|
|
384
532
|
function valueIndexes(mode, frame, items) {
|
|
385
533
|
if (mode === false)
|
|
386
534
|
return [];
|
|
@@ -391,6 +539,8 @@ function valueIndexes(mode, frame, items) {
|
|
|
391
539
|
});
|
|
392
540
|
if (valid.length === 0)
|
|
393
541
|
return [];
|
|
542
|
+
if (mode === 'last')
|
|
543
|
+
return [valid[valid.length - 1]];
|
|
394
544
|
if (mode === 'edge')
|
|
395
545
|
return valid.length === 1 ? [valid[0]] : [valid[0], valid[valid.length - 1]];
|
|
396
546
|
const out = [];
|
|
@@ -426,6 +576,7 @@ function resolveLineSeries(raw, mainItems, line) {
|
|
|
426
576
|
smooth: line.smooth,
|
|
427
577
|
area: line.area,
|
|
428
578
|
ownScale: false,
|
|
579
|
+
avg: false,
|
|
429
580
|
}];
|
|
430
581
|
}
|
|
431
582
|
return list.map((entry, i) => {
|
|
@@ -441,6 +592,7 @@ function resolveLineSeries(raw, mainItems, line) {
|
|
|
441
592
|
smooth: series.smooth === undefined ? line.smooth : series.smooth === true,
|
|
442
593
|
area: series.area === true,
|
|
443
594
|
ownScale: series.ownScale === true,
|
|
595
|
+
avg: false,
|
|
444
596
|
};
|
|
445
597
|
});
|
|
446
598
|
}
|
|
@@ -582,9 +734,27 @@ function resolveMarkPoint(raw) {
|
|
|
582
734
|
return raw;
|
|
583
735
|
return undefined;
|
|
584
736
|
}
|
|
737
|
+
/** 折线默认 viewBox:320×210 → 580×260(#424)。
|
|
738
|
+
*
|
|
739
|
+
* (#507 起本族不再走 `preserveAspectRatio="none"`,见 `svgOpen`;下面的量级关系一字不变——
|
|
740
|
+
* 真正决定实渲字号的是「盒宽 ÷ viewBox 宽」,与 `none`/`meet` 无关。)SVG 的**盒尺寸**仍按 viewBox 长宽比算
|
|
741
|
+
* (`width:100%` + `height:auto`),所以 320×210 塞进 930px 卡片时会被拉到 2.91 倍
|
|
742
|
+
* ——10px 图内文字渲染 29px、点径 20px(t-chartfix #160 的实测)。#160 用
|
|
743
|
+
* `max-width:480px` 压回 1.5 倍,代价是图只占卡片一半宽(930 里居中的 480,左右各空 225)。
|
|
744
|
+
*
|
|
745
|
+
* #424 改从源头修:viewBox 放到真实卡片量级(580×260 ≈ 930×417 的 0.62 相似形)。
|
|
746
|
+
* 卡片越宽、图越大、字越大:930px 卡 → scale 1.6 → 图内 9.5–10px 文字渲染 15.2–16px。
|
|
747
|
+
* 于是 `max-width` 上限不再需要(见 `chartsCss`,桌面规则已撤)。 */
|
|
748
|
+
const LINE_DEFAULT_WIDTH = 580;
|
|
749
|
+
const LINE_DEFAULT_HEIGHT = 260;
|
|
585
750
|
function renderLine(raw) {
|
|
586
751
|
const input = requireObject(raw, 'charts.line: input');
|
|
587
|
-
const common = resolveCommon(input.options, {
|
|
752
|
+
const common = resolveCommon(input.options, {
|
|
753
|
+
width: LINE_DEFAULT_WIDTH,
|
|
754
|
+
height: LINE_DEFAULT_HEIGHT,
|
|
755
|
+
labels: 'edge',
|
|
756
|
+
showValues: false,
|
|
757
|
+
});
|
|
588
758
|
const opts = isPlainObject(input.options) ? input.options : undefined;
|
|
589
759
|
const line = {
|
|
590
760
|
...common,
|
|
@@ -593,6 +763,7 @@ function renderLine(raw) {
|
|
|
593
763
|
smooth: opts !== undefined && opts.smooth === true,
|
|
594
764
|
step: opts !== undefined && opts.step === true,
|
|
595
765
|
showDots: opts === undefined || opts.showDots !== false,
|
|
766
|
+
showDotsExplicit: opts !== undefined && opts.showDots !== undefined,
|
|
596
767
|
dotSize: numOr(opts === undefined ? undefined : opts.dotSize, DOT_DEFAULT_PX),
|
|
597
768
|
dotStyle: opts !== undefined && typeof opts.dotStyle === 'string' && opts.dotStyle !== '' ? opts.dotStyle : undefined,
|
|
598
769
|
area: opts !== undefined && opts.area === true,
|
|
@@ -610,10 +781,19 @@ function renderLine(raw) {
|
|
|
610
781
|
? opts.highlightPoints
|
|
611
782
|
: undefined,
|
|
612
783
|
series: opts !== undefined && Array.isArray(opts.series) ? opts.series : undefined,
|
|
784
|
+
labelEvery: opts !== undefined && isNum(opts.labelEvery) && Math.floor(opts.labelEvery) >= 2
|
|
785
|
+
? Math.floor(opts.labelEvery)
|
|
786
|
+
: undefined,
|
|
613
787
|
};
|
|
614
788
|
const items = normalizeItems(input.items, 'line', true);
|
|
615
789
|
if (items.length === 0)
|
|
616
790
|
return emptyOutput('line', input.options);
|
|
791
|
+
/* 点密度(#424):`showDots` 未显式给且点数超上限 → 隔 k 个画一个(n=90 → k=3 → 30 个)。
|
|
792
|
+
* 末点**只按 stride 命中**时才画:`highlightLast` 会另外补一个实心末点圈(同 x,r 大 1),
|
|
793
|
+
* 两条路都无条件画会叠出「双圈」——所以末点改由 `highlightLast` 单独负责(调用方开它)。 */
|
|
794
|
+
const dotStride = line.showDotsExplicit || line.dotSize <= 0 || items.length <= DOT_STRIDE_MAX
|
|
795
|
+
? 1
|
|
796
|
+
: Math.ceil(items.length / DOT_STRIDE_MAX);
|
|
617
797
|
const series = resolveLineSeries(input.options, items, line);
|
|
618
798
|
const main = series[0];
|
|
619
799
|
/* band:与主序列 items 等长,否则抛错;hi/lo 值并入共享域 */
|
|
@@ -646,12 +826,23 @@ function renderLine(raw) {
|
|
|
646
826
|
if (v !== null)
|
|
647
827
|
sharedValues.push(v);
|
|
648
828
|
}
|
|
649
|
-
const [lo, hi] =
|
|
829
|
+
const [lo, hi] = lineDomainOf(sharedValues, line.yMin, line.yMax);
|
|
650
830
|
const tickN = tickCount(line.yTicks);
|
|
831
|
+
/* 刻度留白按**移动端字号**估(`LINE_TEXT_MOBILE.tick`):留白是双端共用的用户单位,桌面档估算
|
|
832
|
+
* 在 390px 下会漏 —— 见 `insetsFor` 的 `minLeft`。上沿同理(`LINE_TICK_TOP_MIN`):顶端那条
|
|
833
|
+
* 标注的基线以上要 1.04em,≤720px 档字号 19.4 单位最大(原 20),按它留才不会在四档里被裁。 */
|
|
834
|
+
let tickTextW = 0;
|
|
835
|
+
for (let i = 0; i < tickN; i += 1) {
|
|
836
|
+
const tv = lo + ((hi - lo) * i) / (tickN - 1);
|
|
837
|
+
tickTextW = Math.max(tickTextW, textWidthUnits(fmtValue(round2(tv), line.format), LINE_TEXT_MOBILE.tick));
|
|
838
|
+
}
|
|
651
839
|
const frame = makeFrame(line, insetsFor(line, {
|
|
652
840
|
tickWidth: tickN === 0 ? 0 : (line.compact ? 16 : 22),
|
|
653
841
|
labelHeight: line.labels === 'none' ? 0 : (line.compact ? 10 : 14),
|
|
654
842
|
valueHeight: line.compact ? 9 : 12,
|
|
843
|
+
minLeft: tickN === 0 ? 0 : Math.ceil(tickTextW) + 9,
|
|
844
|
+
minTop: tickN === 0 ? 0 : LINE_TICK_TOP_MIN,
|
|
845
|
+
minBottom: line.labels === 'none' ? 0 : LINE_BOTTOM_MIN,
|
|
655
846
|
}));
|
|
656
847
|
const ownDoms = series.map((s) => {
|
|
657
848
|
if (!s.ownScale)
|
|
@@ -671,9 +862,12 @@ function renderLine(raw) {
|
|
|
671
862
|
]);
|
|
672
863
|
};
|
|
673
864
|
const seriesPts = series.map((s, si) => ptsOf(s, si));
|
|
674
|
-
/* avgLine
|
|
675
|
-
*
|
|
676
|
-
|
|
865
|
+
/* avgLine:均线序列(窗口统一为 3..items.length)。旧版 `charts.js:414` 是 `if(opt.avgLine&&!opt.series)`
|
|
866
|
+
* ——传了 `series` 就不叠加(R2-N6,调用方自己管序列)。#424 放开这条互斥:`series` 同时用于给**主序列**
|
|
867
|
+
* 起图例名(旧help模板 `chart-multi-v2` 的图例写「每次称重/7 天均线」,均线是引擎现算的、调用方拿不到它的
|
|
868
|
+
* 数据)——两种用法合并,叠加均线仍由 `avgLine` 决定。主序列恒 `series[0]`、均线恒追加在末尾,
|
|
869
|
+
* 「只看第一条」的逻辑(`highlightLast`/`markPoint`)不受影响。 */
|
|
870
|
+
const avgWindow = line.avgLine === undefined
|
|
677
871
|
? undefined
|
|
678
872
|
: Math.max(3, Math.min(items.length, Math.round(line.avgLine || 7)));
|
|
679
873
|
if (avgWindow !== undefined && items.length >= 2) {
|
|
@@ -691,7 +885,18 @@ function renderLine(raw) {
|
|
|
691
885
|
}
|
|
692
886
|
return { label: item.label, value: n === 0 ? null : round2(sum / n) };
|
|
693
887
|
});
|
|
694
|
-
series.push({
|
|
888
|
+
series.push({
|
|
889
|
+
/* #424 返工:条目名按**实际窗口**生成(调用点传的是变量窗口,如 `reviewDocs.ts:52` 的
|
|
890
|
+
* `avgLine: win`)——写死「7 天均线」在窗口 ≠7 时是错的。 */
|
|
891
|
+
name: avgWindow + ' 天均线',
|
|
892
|
+
items: avgItems,
|
|
893
|
+
color: MUTED_COLOR,
|
|
894
|
+
dashed: true,
|
|
895
|
+
smooth: line.smooth,
|
|
896
|
+
area: false,
|
|
897
|
+
ownScale: false,
|
|
898
|
+
avg: true,
|
|
899
|
+
});
|
|
695
900
|
seriesPts.push(avgItems.map((item, i) => [
|
|
696
901
|
xAt(frame, i, avgItems.length),
|
|
697
902
|
item.value === null ? 0 : yAt(frame, item.value, lo, hi),
|
|
@@ -750,7 +955,9 @@ function renderLine(raw) {
|
|
|
750
955
|
if (s.ownScale)
|
|
751
956
|
anyOwnScale = true;
|
|
752
957
|
const pts = seriesPts[si];
|
|
753
|
-
|
|
958
|
+
/* #424 返工:均线身份**只看显式标记**(注入时置 `avg: true`),不再靠「虚线+非独立刻度+末位」
|
|
959
|
+
* 猜——调用方自己的虚线末位序列从此不再被误判。 */
|
|
960
|
+
const isAvg = s.avg;
|
|
754
961
|
const color = s.color ?? line.color ?? CHART_PALETTE[si % CHART_PALETTE.length];
|
|
755
962
|
if (s.area && !isAvg) {
|
|
756
963
|
const d = areaPath(pts, line.connectNulls, frame.y1, s.smooth && !line.step, line.step);
|
|
@@ -766,14 +973,19 @@ function renderLine(raw) {
|
|
|
766
973
|
+ '" stroke-width="' + line.lineWidth + '" stroke-linejoin="round" stroke-linecap="round"'
|
|
767
974
|
+ (s.dashed ? ' stroke-dasharray="6 5"' : '') + ' vector-effect="non-scaling-stroke"/>';
|
|
768
975
|
}
|
|
769
|
-
|
|
976
|
+
/* 图例条目(#424 起均线条目也算一条):均线是引擎注入的序列(`avg: true`),名字按窗口生成、
|
|
977
|
+
* 进图例;调用方自己的**虚线序列**(配对页 cross 轴那条)不进图例——那是调用方图例的事。 */
|
|
978
|
+
if (line.legend && s.name !== '' && (s.avg || !(avgWindow !== undefined && s.dashed))) {
|
|
770
979
|
legendEntries.push({ name: s.name, color });
|
|
980
|
+
}
|
|
771
981
|
if (isAvg)
|
|
772
982
|
return;
|
|
773
983
|
if (line.showDots) {
|
|
774
984
|
pts.forEach((p, i) => {
|
|
775
985
|
if (p[2])
|
|
776
986
|
return;
|
|
987
|
+
if (dotStride > 1 && (i % dotStride) !== 0)
|
|
988
|
+
return;
|
|
777
989
|
const item = s.items[i];
|
|
778
990
|
const anomaly = item.anomaly === true;
|
|
779
991
|
const dotClass = STYLE_PREFIX + 'charts-dot' + (anomaly ? ' ' + STYLE_PREFIX + 'charts-dot-anomaly' : '')
|
|
@@ -849,10 +1061,17 @@ function renderLine(raw) {
|
|
|
849
1061
|
marksSvg += '<circle class="' + STYLE_PREFIX + 'charts-dot-last" data-i="' + last + '" cx="' + n1(p[0])
|
|
850
1062
|
+ '" cy="' + n1(p[1]) + '" r="' + (line.dotSize / 2 + 1) + '" fill="' + esc(color) + '" stroke="' + esc(color)
|
|
851
1063
|
+ '" stroke-width="1.5" vector-effect="non-scaling-stroke"/>';
|
|
852
|
-
/* 末值文本受 `showValues`/`labels:'select'` 门控(旧 `charts.js:619-621`);高亮圈无条件。
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1064
|
+
/* 末值文本受 `showValues`/`labels:'select'` 门控(旧 `charts.js:619-621`);高亮圈无条件。
|
|
1065
|
+
* #424:末值常落在绘图区**右边缘**(末点就在 `frame.x1` 上),`middle` 锚会让文字一半探出图外
|
|
1066
|
+
* (实测「70.6kg」右半截贴到卡片边)。距边不足 6% 宽时改 `end`/`start` 锚并收进 4 单位。
|
|
1067
|
+
* #567:`showValues:'last'` 时末值已由上面的数值标签画出(恰一枚),此处只留圈、不再追加文本
|
|
1068
|
+
* (否则同一末值印两遍)。 */
|
|
1069
|
+
if ((line.showValues !== false && line.showValues !== 'last') || line.labels === 'select') {
|
|
1070
|
+
const edgeRatio = (p[0] - frame.x0) / frame.w;
|
|
1071
|
+
const lastAnchor = edgeRatio > 0.94 ? 'end' : edgeRatio < 0.06 ? 'start' : 'middle';
|
|
1072
|
+
const lastX = edgeRatio > 0.94 ? p[0] - 4 : edgeRatio < 0.06 ? p[0] + 4 : p[0];
|
|
1073
|
+
valuesSvg += '<text class="' + STYLE_PREFIX + 'charts-value ' + STYLE_PREFIX + 'charts-value-last" x="' + n1(lastX)
|
|
1074
|
+
+ '" y="' + n1(p[1] - LINE_LAST_LABEL_LIFT) + '" text-anchor="' + lastAnchor + '" fill="var(--fg,#1d1d1f)">'
|
|
856
1075
|
+ esc(fmtValue(main.items[last].value, line.format)) + '</text>';
|
|
857
1076
|
}
|
|
858
1077
|
}
|
|
@@ -868,7 +1087,9 @@ function renderLine(raw) {
|
|
|
868
1087
|
+ '" x2="' + n1(frame.x1) + '" y2="' + n1(my) + '" stroke="' + esc(color)
|
|
869
1088
|
+ '" stroke-width="1.5" stroke-dasharray="5 4" vector-effect="non-scaling-stroke"/>'
|
|
870
1089
|
+ '<text class="' + STYLE_PREFIX + 'charts-marktext" x="' + n1(frame.x1 - 2) + '" y="' + n1(my - 4)
|
|
871
|
-
|
|
1090
|
+
/* t-chartfix #160:标签恒用静音灰 `--fg3,#86868b`(老help模板 `公共组件/assets/charts.js:87`
|
|
1091
|
+
* 同款),**线**仍跟序列色/`mark.color`——线色由 3 条测试锁,标签色不锁。 */
|
|
1092
|
+
+ '" text-anchor="end" fill="' + MUTED_COLOR + '">'
|
|
872
1093
|
+ esc(mark.label !== undefined && mark.label !== null ? String(mark.label) : String(mark.value)) + '</text>';
|
|
873
1094
|
}
|
|
874
1095
|
if (mark.xValue !== undefined && mark.xValue !== null) {
|
|
@@ -897,7 +1118,8 @@ function renderLine(raw) {
|
|
|
897
1118
|
+ '" x2="' + n1(mx) + '" y2="' + n1(frame.y1) + '" stroke="' + esc(color)
|
|
898
1119
|
+ '" stroke-width="1.5" stroke-dasharray="5 4" vector-effect="non-scaling-stroke"/>'
|
|
899
1120
|
+ '<text class="' + STYLE_PREFIX + 'charts-marktext-v" x="' + n1(labelX) + '" y="' + n1(frame.y0 - 4)
|
|
900
|
-
|
|
1121
|
+
/* t-chartfix #160:同上——垂直线标签也走 `MUTED_COLOR`,线 stroke 不变。 */
|
|
1122
|
+
+ '" text-anchor="' + anchor + '" fill="' + MUTED_COLOR + '">'
|
|
901
1123
|
+ esc(mark.label !== undefined && mark.label !== null ? String(mark.label) : items[idx].label) + '</text>';
|
|
902
1124
|
}
|
|
903
1125
|
}
|
|
@@ -943,15 +1165,11 @@ function renderLine(raw) {
|
|
|
943
1165
|
+ '</text>';
|
|
944
1166
|
}
|
|
945
1167
|
}
|
|
946
|
-
const points = series.reduce((sum, s
|
|
947
|
-
if (avgWindow !== undefined && si === series.length - 1)
|
|
948
|
-
return sum;
|
|
949
|
-
return sum + s.items.filter((item) => item.value !== null).length;
|
|
950
|
-
}, 0);
|
|
1168
|
+
const points = series.reduce((sum, s) => (s.avg ? sum : sum + s.items.filter((item) => item.value !== null).length), 0);
|
|
951
1169
|
const html = containerOpen('line', line, STYLE_PREFIX + 'charts-line')
|
|
952
1170
|
+ legendHtml(legendEntries)
|
|
953
|
-
+ svgOpen(line
|
|
954
|
-
+ (line.grid ? gridSvg(frame) : '')
|
|
1171
|
+
+ svgOpen(line)
|
|
1172
|
+
+ (line.grid ? gridSvg(frame, tickN) : '')
|
|
955
1173
|
+ bandSvg
|
|
956
1174
|
+ fillSvg
|
|
957
1175
|
+ ticksSvg(frame, lo, hi, tickN, line.format)
|
|
@@ -960,7 +1178,7 @@ function renderLine(raw) {
|
|
|
960
1178
|
+ marksSvg
|
|
961
1179
|
+ valuesSvg
|
|
962
1180
|
+ markPointSvg
|
|
963
|
-
+ xLabelsSvg(line, frame, items)
|
|
1181
|
+
+ xLabelsSvg(line, frame, items, 'peak', undefined, LINE_LABEL_GAP, line.labelEvery)
|
|
964
1182
|
+ '</svg></div>';
|
|
965
1183
|
return { kind: 'line', html, empty: points === 0, points };
|
|
966
1184
|
}
|
|
@@ -1016,12 +1234,12 @@ function renderBar(raw) {
|
|
|
1016
1234
|
return total > best ? total : best;
|
|
1017
1235
|
}, 0)
|
|
1018
1236
|
: 0;
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1237
|
+
/* #567 柱状 Y 轴刻度(默认不开:`yTicks` 缺省 → 0 条,行为与改前逐字同);
|
|
1238
|
+
* 数值标签抽稀步长(`valueThin`,用户单位;非法值=全标)。 */
|
|
1239
|
+
const barTicks = tickCount(opts === undefined ? undefined : opts.yTicks);
|
|
1240
|
+
const valueThin = opts !== undefined && isNum(opts.valueThin) && opts.valueThin > 0
|
|
1241
|
+
? opts.valueThin
|
|
1242
|
+
: undefined;
|
|
1025
1243
|
let lo = 0;
|
|
1026
1244
|
let hi = 1;
|
|
1027
1245
|
if (stacked && stackMode === 'percent') {
|
|
@@ -1049,6 +1267,22 @@ function renderBar(raw) {
|
|
|
1049
1267
|
lo = dlo;
|
|
1050
1268
|
hi = dhi;
|
|
1051
1269
|
}
|
|
1270
|
+
/* 刻度留白按移动端字号估(与折线同口径 `LINE_TEXT_MOBILE.tick`);不开刻度时三项全 0,
|
|
1271
|
+
* 绘图区几何与改前逐字同(`tickN = 0` 旧口径)。 */
|
|
1272
|
+
let tickTextW = 0;
|
|
1273
|
+
if (barTicks >= 2) {
|
|
1274
|
+
for (let i = 0; i < barTicks; i += 1) {
|
|
1275
|
+
const tv = lo + ((hi - lo) * i) / (barTicks - 1);
|
|
1276
|
+
tickTextW = Math.max(tickTextW, textWidthUnits(fmtValue(round2(tv), common.format), LINE_TEXT_MOBILE.tick));
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
const frame = makeFrame(common, insetsFor(common, {
|
|
1280
|
+
tickWidth: barTicks === 0 ? 0 : (common.compact ? 16 : 22),
|
|
1281
|
+
labelHeight: common.labels === 'none' ? 0 : (common.compact ? 10 : 14),
|
|
1282
|
+
valueHeight: common.compact ? 9 : 12,
|
|
1283
|
+
minLeft: barTicks === 0 ? 0 : Math.ceil(tickTextW) + 9,
|
|
1284
|
+
minTop: barTicks === 0 ? 0 : LINE_TICK_TOP_MIN,
|
|
1285
|
+
}));
|
|
1052
1286
|
const yZero = yAt(frame, 0, lo, hi);
|
|
1053
1287
|
const slot = frame.w / items.length;
|
|
1054
1288
|
const barW = Math.min(slot * (common.compact ? 0.82 : 0.62), 34);
|
|
@@ -1103,10 +1337,45 @@ function renderBar(raw) {
|
|
|
1103
1337
|
});
|
|
1104
1338
|
barsSvg += group + '</g>';
|
|
1105
1339
|
});
|
|
1106
|
-
/* 数值标签:单柱 = 值;stacked = 柱顶合计;grouped =
|
|
1340
|
+
/* 数值标签:单柱 = 值;stacked = 柱顶合计;grouped = 每子柱顶部。
|
|
1341
|
+
* #567:`false` 无;`true` 全量(`valueThin` 给正数时按柱中心距抽稀,默认不变);
|
|
1342
|
+
* `'edge'` 只标首尾有效点(改前落进全开那支——59 柱 59 枚压字即此 bug);
|
|
1343
|
+
* `'last'` 只标末值。抽稀只作用于柱组一级(grouped 整组取舍,不拆组内子柱)。 */
|
|
1344
|
+
const labelSet = (() => {
|
|
1345
|
+
if (common.showValues === false)
|
|
1346
|
+
return new Set();
|
|
1347
|
+
if (common.showValues === true && valueThin !== undefined) {
|
|
1348
|
+
const keep = new Set();
|
|
1349
|
+
let lastX = -Infinity;
|
|
1350
|
+
items.forEach((item, i) => {
|
|
1351
|
+
if (item.value === null && !multi)
|
|
1352
|
+
return;
|
|
1353
|
+
const cx = frame.x0 + slot * (i + 0.5);
|
|
1354
|
+
if (cx - lastX < valueThin)
|
|
1355
|
+
return;
|
|
1356
|
+
keep.add(i);
|
|
1357
|
+
lastX = cx;
|
|
1358
|
+
});
|
|
1359
|
+
return keep;
|
|
1360
|
+
}
|
|
1361
|
+
if (common.showValues === true)
|
|
1362
|
+
return new Set(items.map((_, i) => i));
|
|
1363
|
+
/* 多值模式(stacked/grouped)的 `item.value` 为空,`valueIndexes` 按值取会落空——
|
|
1364
|
+
* 按柱组取首末(`'edge'` 首尾组、`'last'` 末组),标签内容仍走各组既有口径(合计/子柱值)。 */
|
|
1365
|
+
if (multi) {
|
|
1366
|
+
if (items.length === 0)
|
|
1367
|
+
return new Set();
|
|
1368
|
+
if (common.showValues === 'last')
|
|
1369
|
+
return new Set([items.length - 1]);
|
|
1370
|
+
return new Set(items.length === 1 ? [0] : [0, items.length - 1]);
|
|
1371
|
+
}
|
|
1372
|
+
return new Set(valueIndexes(common.showValues, frame, items));
|
|
1373
|
+
})();
|
|
1107
1374
|
let valuesSvg = '';
|
|
1108
|
-
if (
|
|
1375
|
+
if (labelSet.size > 0) {
|
|
1109
1376
|
items.forEach((item, i) => {
|
|
1377
|
+
if (!labelSet.has(i))
|
|
1378
|
+
return;
|
|
1110
1379
|
const cx = frame.x0 + slot * (i + 0.5);
|
|
1111
1380
|
if (!multi) {
|
|
1112
1381
|
const value = item.value;
|
|
@@ -1143,9 +1412,12 @@ function renderBar(raw) {
|
|
|
1143
1412
|
const html = containerOpen('bar', common, STYLE_PREFIX + 'charts-bar')
|
|
1144
1413
|
+ legendHtml(legendEntries)
|
|
1145
1414
|
+ svgOpen(common)
|
|
1146
|
-
|
|
1415
|
+
/* #567:开 `yTicks` 时网格线与刻度线同条数同位置(折线 #512 口径);不开时 `tickN = 0`
|
|
1416
|
+
* 回退旧三线口径,几何与改前逐字同。 */
|
|
1417
|
+
+ (common.grid ? gridSvg(frame, barTicks) : '')
|
|
1147
1418
|
+ barsSvg
|
|
1148
1419
|
+ valuesSvg
|
|
1420
|
+
+ ticksSvg(frame, lo, hi, barTicks, common.format)
|
|
1149
1421
|
/* t-chartfix:柱标签取柱列中心(与 `barsSvg` 同一 band 标度),不得用点标度。 */
|
|
1150
1422
|
+ xLabelsSvg(common, frame, items, 'edge', (i, n) => frame.x0 + (frame.w / n) * (i + 0.5))
|
|
1151
1423
|
+ '</svg></div>';
|
|
@@ -1351,7 +1623,7 @@ function renderCombo(raw) {
|
|
|
1351
1623
|
const points = bars.length + lines.length;
|
|
1352
1624
|
const html = containerOpen('combo', common, STYLE_PREFIX + 'charts-combo')
|
|
1353
1625
|
+ legend
|
|
1354
|
-
+ svgOpen(common
|
|
1626
|
+
+ svgOpen(common)
|
|
1355
1627
|
+ (common.grid ? gridSvg(frame) : '')
|
|
1356
1628
|
+ barsSvg
|
|
1357
1629
|
+ lineSvg
|
|
@@ -1528,8 +1800,8 @@ function renderScatter(raw) {
|
|
|
1528
1800
|
}
|
|
1529
1801
|
const points = items.length;
|
|
1530
1802
|
const html = containerOpen('scatter', common, STYLE_PREFIX + 'charts-scatter')
|
|
1531
|
-
+ svgOpen(common
|
|
1532
|
-
+ (common.grid ? gridSvg(frame) : '')
|
|
1803
|
+
+ svgOpen(common)
|
|
1804
|
+
+ (common.grid ? gridSvg(frame, SCATTER_Y_TICKS) : '')
|
|
1533
1805
|
+ ticksSvg(frame, ylo, yhi, SCATTER_Y_TICKS, common.format)
|
|
1534
1806
|
+ regressionSvg
|
|
1535
1807
|
+ dotsSvg
|
|
@@ -1562,8 +1834,20 @@ export const charts = new Proxy(CHART_DISPATCH, {
|
|
|
1562
1834
|
},
|
|
1563
1835
|
});
|
|
1564
1836
|
/* ── 图表 CSS 文本(R12:唯一产出者 = 本文件内部常量/函数;#75 复用同一份) ── */
|
|
1837
|
+
/** t512:折线族六个文本类的一组字号规则(**用户单位**,逐档补偿后的值——算式见 `LINE_TEXT_WIDE_UNITS`)。
|
|
1838
|
+
* 类名与字形族与 ≤720px 段的六条逐字同款,只换数字;抽成一函数免得三档各抄一遍走散。 */
|
|
1839
|
+
function lineFontRules(p, units) {
|
|
1840
|
+
return '.' + p + 'charts-line .' + p + 'charts-tick{font-size:' + units + 'px}'
|
|
1841
|
+
+ '.' + p + 'charts-line .' + p + 'charts-xlabel{font-size:' + units + 'px}'
|
|
1842
|
+
+ '.' + p + 'charts-line .' + p + 'charts-value{font-size:' + units + 'px}'
|
|
1843
|
+
+ '.' + p + 'charts-line .' + p + 'charts-value-last{font-size:' + units + 'px}'
|
|
1844
|
+
+ '.' + p + 'charts-line .' + p + 'charts-marktext{font-size:' + units + 'px}'
|
|
1845
|
+
+ '.' + p + 'charts-line .' + p + 'charts-marktext-v{font-size:' + units + 'px}';
|
|
1846
|
+
}
|
|
1565
1847
|
/** 图表样式文本(**唯一一份**):类名走 `prefix` 命名空间;容器零 padding;
|
|
1566
|
-
* 断点数值逐值取 `CHART_BREAKPOINTS`(`mobileMaxPx`/`dotSizeMobilePx
|
|
1848
|
+
* 断点数值逐值取 `CHART_BREAKPOINTS`(`mobileMaxPx`/`dotSizeMobilePx`)。
|
|
1849
|
+
* (`lineHeightMobilePx` 自 #424 返工起不再进 CSS:折线在移动端改按 viewBox 长宽比派生高度,
|
|
1850
|
+
* 见下方 ≤720px 段的注释;该常量仍留在冻结的 `CHART_BREAKPOINTS` 里不动。)
|
|
1567
1851
|
*
|
|
1568
1852
|
* **#75 复用点**:`buildStyleSheet()` 的 `charts` 样式区直接引用本函数产出,
|
|
1569
1853
|
* **不得**在别处重述图表 CSS 文本(#78 结论,违反即 S1)。
|
|
@@ -1573,7 +1857,6 @@ export function chartsCss(prefix) {
|
|
|
1573
1857
|
const p = prefix;
|
|
1574
1858
|
const mobile = CHART_BREAKPOINTS.mobileMaxPx;
|
|
1575
1859
|
const dotMobile = CHART_BREAKPOINTS.dotSizeMobilePx;
|
|
1576
|
-
const lineHeightMobile = CHART_BREAKPOINTS.lineHeightMobilePx;
|
|
1577
1860
|
return [
|
|
1578
1861
|
'.' + p + 'charts{position:relative;margin:0;padding:0;box-sizing:border-box;font-family:inherit;color:var(--fg,#1d1d1f);--' + p + 'charts-dot:' + SCATTER_DOT_DEFAULT_PX + 'px}',
|
|
1579
1862
|
'.' + p + 'charts *{box-sizing:border-box}',
|
|
@@ -1608,6 +1891,10 @@ export function chartsCss(prefix) {
|
|
|
1608
1891
|
'.' + p + 'charts-xlabel{font-size:10px}',
|
|
1609
1892
|
'.' + p + 'charts-value{font-size:10px}',
|
|
1610
1893
|
'.' + p + 'charts-value-last{font-size:10.5px;font-weight:700}',
|
|
1894
|
+
/* #424 返工:末值标签与末点同高、常与折线/均线交叠(390px 下实测线从标签盒里穿过)。
|
|
1895
|
+
* 给折线族的末值加白色描边(`paint-order:stroke` 先描边后填充),压在线上也读得清;
|
|
1896
|
+
* 不新增变量名——`--bg` 是本仓既有 token,缺省回退白。 */
|
|
1897
|
+
'.' + p + 'charts-line .' + p + 'charts-value-last{paint-order:stroke;stroke:var(--bg,#fff);stroke-width:3px;stroke-linejoin:round}',
|
|
1611
1898
|
'.' + p + 'charts-marktext{font-size:10px}',
|
|
1612
1899
|
'.' + p + 'charts-marktext-v{font-size:10px}',
|
|
1613
1900
|
'.' + p + 'charts-mptext{font-size:10.5px;font-weight:700}',
|
|
@@ -1620,12 +1907,39 @@ export function chartsCss(prefix) {
|
|
|
1620
1907
|
'.' + p + 'charts-center-value{font-size:13px;font-weight:700}',
|
|
1621
1908
|
'.' + p + 'charts-gauge-value{font-size:26px;font-weight:800}',
|
|
1622
1909
|
'.' + p + 'charts-gauge-label{font-size:11px}',
|
|
1910
|
+
/* t-chartfix #160 的桌面等比上限 `max-width:480px`(=320×1.5)在 #424 撤销:
|
|
1911
|
+
* 折线 viewBox 已放大到 580×260(`LINE_DEFAULT_WIDTH/HEIGHT`),930px 卡片下 scale≈1.6
|
|
1912
|
+
* ——图内 9.5–10px 文字渲染 15.2–16px,靠 viewBox 自身就压住了字号,不再需要砍宽度。
|
|
1913
|
+
* 留着它反而把图钉死在卡片中缝(930 里 480,左右各空 225px)。 */
|
|
1623
1914
|
'@media (max-width:' + mobile + 'px){'
|
|
1624
1915
|
+ '.' + p + 'charts{--' + p + 'charts-dot:' + dotMobile + 'px}'
|
|
1625
|
-
|
|
1916
|
+
/* #424 返工:原来这里把折线 svg 盒高钉成 `lineHeightMobilePx`(150px),而折线是
|
|
1917
|
+
* `preserveAspectRatio="none"` 的满宽拉伸族 —— 580×260/580×300/580×180 三种 viewBox
|
|
1918
|
+
* 被压进同一个 150px 高,横向 0.57、纵向 0.50/0.83 → 圆点变椭圆、字被压扁(实测失真比
|
|
1919
|
+
* 1.13/0.68)。改成按 viewBox 长宽比派生高度(`height:auto`,与 `.charts-svg` 基规则同款),
|
|
1920
|
+
* 三种 viewBox 一律等比缩放;字号补偿见 `LINE_TEXT_MOBILE`。 */
|
|
1921
|
+
+ '.' + p + 'charts-line .' + p + 'charts-svg{height:auto}'
|
|
1626
1922
|
+ '.' + p + 'charts-bar .' + p + 'charts-xlabel{font-size:9.5px}'
|
|
1627
1923
|
+ '.' + p + 'charts-legend{font-size:11.5px}'
|
|
1924
|
+
/* #424 返工(移动端折线文字不可读):上面那条把折线 svg 盒高钉成 150px,580×260 的 viewBox
|
|
1925
|
+
* 在 390px 手机上被压到 scale≈0.58(`preserveAspectRatio="none"`)——图内 9.5/10 单位实测
|
|
1926
|
+
* 只有 5.4/5.7px。这里只对**折线族**按 1/0.58≈1.75 倍提字号,把实渲拉回 ~10px;字号常量与
|
|
1927
|
+
* 留白估算同源(`LINE_TEXT_MOBILE`),留白已按这一档放大,文字不会再顶出 viewBox。 */
|
|
1928
|
+
+ '.' + p + 'charts-line .' + p + 'charts-tick{font-size:' + LINE_TEXT_MOBILE.tick + 'px}'
|
|
1929
|
+
+ '.' + p + 'charts-line .' + p + 'charts-xlabel{font-size:' + LINE_TEXT_MOBILE.xlabel + 'px}'
|
|
1930
|
+
+ '.' + p + 'charts-line .' + p + 'charts-value{font-size:' + LINE_TEXT_MOBILE.value + 'px}'
|
|
1931
|
+
+ '.' + p + 'charts-line .' + p + 'charts-value-last{font-size:' + LINE_TEXT_MOBILE.last + 'px}'
|
|
1932
|
+
+ '.' + p + 'charts-line .' + p + 'charts-marktext{font-size:' + LINE_TEXT_MOBILE.mark + 'px}'
|
|
1933
|
+
+ '.' + p + 'charts-line .' + p + 'charts-marktext-v{font-size:' + LINE_TEXT_MOBILE.mark + 'px}'
|
|
1628
1934
|
+ '}',
|
|
1935
|
+
/* t512(字号随盒宽乱跳的根因):上面那一段只按 ≤720px 给了一档(原 20 用户单位,t512 收尾收到 19.4),721px 起落回桌面档,
|
|
1936
|
+
* 断点两侧实渲 22.5px → 10.7px。这里补两档,用户单位 = 目标像素 ÷ 该档实测缩放比
|
|
1937
|
+
* (算式与四档实测读数见 `LINE_TEXT_WIDE_UNITS` 的注释)——只给**折线族**六条文本类,
|
|
1938
|
+
* 杆/散点/环/量表各族沿用基规则 9.5/10 单位不动(它们没有被报「字号乱跳」)。 */
|
|
1939
|
+
'@media (min-width:' + (mobile + 1) + 'px) and (max-width:' + LINE_TEXT_WIDE_MAX_PX + 'px){'
|
|
1940
|
+
+ lineFontRules(p, LINE_TEXT_WIDE_UNITS) + '}',
|
|
1941
|
+
'@media (min-width:' + (LINE_TEXT_WIDE_MAX_PX + 1) + 'px){'
|
|
1942
|
+
+ lineFontRules(p, LINE_TEXT_FULL_UNITS) + '}',
|
|
1629
1943
|
].join(LF);
|
|
1630
1944
|
}
|
|
1631
1945
|
/* ── 图表 helpers JS(唯一产出者,FX-22;首步幂等自注入 `<style>`) ────── */
|
|
@@ -1645,7 +1959,7 @@ function helpersStyleId(input) {
|
|
|
1645
1959
|
* 不向 `window.<id>`/`globalThis.<id>` 赋值/不引 `node:`。
|
|
1646
1960
|
*
|
|
1647
1961
|
* 首步即幂等自注入 `<style id="{styleId ?? CHARTS_STYLE_ID}">`(R3/R12);图表 CSS 文本
|
|
1648
|
-
* **只此一处产出**(`chartsCss`),#75
|
|
1962
|
+
* **只此一处产出**(`chartsCss`),#75 实施时复用同一份文本,不得重述。 */
|
|
1649
1963
|
export const buildChartsHelpersJs = (input) => {
|
|
1650
1964
|
const prefix = helpersPrefix(input);
|
|
1651
1965
|
const styleId = helpersStyleId(input);
|