agentcss 0.0.0-stage → 0.11.3
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 +3138 -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 +497 -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 +311 -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,94 @@
|
|
|
1
|
+
# bar · 柱状图
|
|
2
|
+
|
|
3
|
+
**charter 13 图表规范**的四条铁律全部落地。ES5、零依赖、纯 SVG。
|
|
4
|
+
|
|
5
|
+
## 🔴 四条铁律
|
|
6
|
+
|
|
7
|
+
| # | 铁律 | 落地在哪 |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| 一 | 超量程必须被**容器裁剪** | CSS 侧 `overflow: hidden`(JS 不参与)|
|
|
10
|
+
| 二 | **标注不得覆盖数据**(≤ 4 个)| `markLabel()` 只标关键点 |
|
|
11
|
+
| 三 | **量程必须显式声明** | `max` 是**必填参数**,无默认值 |
|
|
12
|
+
| 铁 | 门禁要能证伪 | `05-audit/chart-check.js` 覆盖本组件 |
|
|
13
|
+
|
|
14
|
+
### 铁律一:为什么**两道**裁剪都不能删
|
|
15
|
+
|
|
16
|
+
```css
|
|
17
|
+
.bar { overflow: hidden; } /* 第二道:防穿透到别的卡片 */
|
|
18
|
+
.bar__svg { overflow: hidden; } /* 根因:裁掉超出 viewBox 的部分 */
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
关键在 `overflow` 的默认值:**SVG 的 `overflow` 默认是 `visible`,不是 `hidden`**
|
|
22
|
+
(`canvas` 才是默认 hidden)—— 与 HTML 的直觉相反。
|
|
23
|
+
一根高度算出 400px、viewBox 只有 200px 的柱子会**照原样画到外面**。
|
|
24
|
+
|
|
25
|
+
⚠️ 但**只裁剪是止损不是解决**:被裁的那根看起来像"到顶了"。
|
|
26
|
+
所以 JS 侧同时给出 `data-clipped` 标记 + `.bar__note` 文字说明。
|
|
27
|
+
|
|
28
|
+
### 铁律三:`max` 必填,因为**自动适配是最隐蔽的谎报**
|
|
29
|
+
|
|
30
|
+
一个 `400` 的异常值 + 30 个 `0.35` 的常态值,若 Y 轴自适应到 400
|
|
31
|
+
⇒ 所有常态值被压成**一条贴底的直线**
|
|
32
|
+
⇒ 读者会得出「这些天完全没有变化」的**错误结论**。
|
|
33
|
+
|
|
34
|
+
所以 `opt.max` **没有默认值**——不声明就是 bug,构造函数直接要求你回答。
|
|
35
|
+
|
|
36
|
+
### 铁律二:只标关键点
|
|
37
|
+
|
|
38
|
+
标数字的位置会压在柱子上、彼此重叠(右下角糊成一团)。
|
|
39
|
+
所以默认**只标 4 个**,其余靠 Y 轴刻度读。
|
|
40
|
+
|
|
41
|
+
## 启用
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
const chart = Bar.draw({
|
|
45
|
+
host: 元素, // 会自动清空
|
|
46
|
+
series: [{ name: '成交', values: [120, 90, 150] },
|
|
47
|
+
{ name: '挂单', values: [30, 40, 20] }],
|
|
48
|
+
max: 200, // 🔴 必填
|
|
49
|
+
mode: 'stack', // 'stack'(默认)| 'group'
|
|
50
|
+
labels: ['09-01', '09-02', '09-03'],
|
|
51
|
+
threshold: 100, // 可选:画一条虚线
|
|
52
|
+
height: 180,
|
|
53
|
+
note: '…',
|
|
54
|
+
legend: true, // 默认:系列 ≥2 且都有名字时自动出
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
返回 `{ el, clipped, mode, tops, legend, destroy() }`
|
|
59
|
+
—— `clipped` 是被裁的类目下标,`tops` 是每列合计/最高值。
|
|
60
|
+
|
|
61
|
+
## 🔴 堆叠 vs 分组:语义必须显式选择
|
|
62
|
+
|
|
63
|
+
(外部评审 VIZ-REPORT-01 · P0-1 的教训)
|
|
64
|
+
|
|
65
|
+
原实现的 `base` 数组在**每个系列开始时**被重置,且所有系列的柱用同一横坐标
|
|
66
|
+
与同一柱宽 ⇒ 多系列不是堆叠,是**后画的盖住先画的**。它自称堆叠,实际重叠。
|
|
67
|
+
|
|
68
|
+
现在:`base` 提到系列循环外累计;堆叠宽度与分组宽度分开算。
|
|
69
|
+
判据直接断言**几何关系**:多段柱首尾相接、重叠 0。
|
|
70
|
+
|
|
71
|
+
## 类名
|
|
72
|
+
|
|
73
|
+
| 类| 用途 |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `.bar` / `.bar__svg` | 根 / SVG |
|
|
76
|
+
| `.bar__bar` + `.bar__s0`…`.bar__s4` | 柱体 + 5 个分类色 |
|
|
77
|
+
| `.bar__grid` / `.bar__ytick` / `.bar__xlabel` | 网格 / Y 刻度 / X 标签 |
|
|
78
|
+
| `.bar__label` / `--max` / `--clip` | 标注(铁律二)|
|
|
79
|
+
| `.bar__threshold` | 阈值线 |
|
|
80
|
+
| `.bar__legend` / `__legend-item` / `__legend-swatch` | 图例 |
|
|
81
|
+
| `.bar__note` | 脚注(说明截断了什么)|
|
|
82
|
+
|
|
83
|
+
**超过 5 个系列请自己给 `series[i].cls`** —— 颜色循环会造成误读。
|
|
84
|
+
|
|
85
|
+
## 依赖
|
|
86
|
+
|
|
87
|
+
`01-tokens/tokens.css`(颜色全走令牌,不写死)。
|
|
88
|
+
|
|
89
|
+
## 成熟度:stable(无行为债)
|
|
90
|
+
|
|
91
|
+
## 门禁
|
|
92
|
+
|
|
93
|
+
`05-audit/chart-check.js` —— 覆盖堆叠几何、`tops` 口径、柱高与合计相符、
|
|
94
|
+
超量程裁剪与写明、图例同色五条。
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
bar.css — 柱状图样式(charter 13 图表规范)
|
|
3
|
+
============================================================================
|
|
4
|
+
⭐ 铁律一的关键就写在下面这两行,**不能删**:
|
|
5
|
+
|
|
6
|
+
.bar { overflow: hidden; }
|
|
7
|
+
.bar__svg { overflow: hidden; }
|
|
8
|
+
|
|
9
|
+
为什么两道都要:
|
|
10
|
+
· `.bar__svg` 裁掉**超出 viewBox** 的部分(这才是根因)
|
|
11
|
+
—— SVG 的 `overflow` 默认值是 `visible`,**不是 hidden**,
|
|
12
|
+
与 HTML 的直觉相反(`canvas` 才是默认 hidden)。
|
|
13
|
+
一根高度算出 400px、viewBox 只有 200px 的柱子会**照原样画到外面**。
|
|
14
|
+
· `.bar` 裁掉溢出**父容器**的部分(防穿透到别的卡片)
|
|
15
|
+
|
|
16
|
+
⚠️ 只裁剪是**止损**不是**解决**:被裁的那根会显得像"到顶了",
|
|
17
|
+
所以 JS 侧必须同时给出 `data-clipped` 标记 + `.bar__note` 说明。
|
|
18
|
+
============================================================================ */
|
|
19
|
+
|
|
20
|
+
.bar {
|
|
21
|
+
position: relative;
|
|
22
|
+
overflow: hidden; /* 🔴 铁律一:第二道 */
|
|
23
|
+
background: var(--surface, #fff);
|
|
24
|
+
border: 1px solid var(--border-decor);
|
|
25
|
+
border-radius: var(--r-md, 8px);
|
|
26
|
+
padding: var(--sp-3);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.bar__svg {
|
|
30
|
+
display: block;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: auto;
|
|
33
|
+
overflow: hidden; /* 🔴 铁律一:真正的根因在这 */
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ---------- 网格与刻度 ---------- */
|
|
37
|
+
.bar__grid {
|
|
38
|
+
stroke: var(--border-decor);
|
|
39
|
+
stroke-width: 1;
|
|
40
|
+
/* 虚线比实线轻:网格是背景,不该跟数据抢注意力 */
|
|
41
|
+
stroke-dasharray: 2 3;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.bar__ytick,
|
|
45
|
+
.bar__xlabel {
|
|
46
|
+
fill: var(--text-tertiary);
|
|
47
|
+
font-size: 10px;
|
|
48
|
+
font-family: var(--mono);
|
|
49
|
+
/* 数字要对齐 ⇒ 等宽 */
|
|
50
|
+
font-variant-numeric: tabular-nums;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* ---------- 柱子 ---------- */
|
|
54
|
+
/* 🔴 系列身份:一个类给一套颜色变量,rect 与图例色块**共用同一份**声明。
|
|
55
|
+
⇒ 图例画的色,和读者在柱子上看到的色,不可能是两个东西。
|
|
56
|
+
(旧的 --stack-N 写法做不到这一点,图例是后加的,那时也没有图例。) */
|
|
57
|
+
.bar__s0 { --bar-c: var(--accent); }
|
|
58
|
+
.bar__s1 { --bar-c: var(--text-tertiary); }
|
|
59
|
+
.bar__s2 { --bar-c: var(--success); }
|
|
60
|
+
.bar__s3 { --bar-c: var(--warning); }
|
|
61
|
+
.bar__s4 { --bar-c: var(--info); }
|
|
62
|
+
|
|
63
|
+
.bar__bar {
|
|
64
|
+
fill: var(--bar-c, var(--accent));
|
|
65
|
+
/* 悬停变深(只对真能悬停的设备生效,见下方 @media) */
|
|
66
|
+
transition: fill 120ms var(--ease-out, ease);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* 🔴 被截断的那一根:加一道顶线,明确"这是被裁的"
|
|
70
|
+
(放在系列色之后 ⇒ 特异性更高,能盖住 series 给的 fill) */
|
|
71
|
+
.bar__bar[data-clipped="1"] {
|
|
72
|
+
fill: var(--danger, #c0392b);
|
|
73
|
+
stroke: var(--danger, #c0392b);
|
|
74
|
+
stroke-width: 1;
|
|
75
|
+
stroke-dasharray: 2 2;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ---------- 图例 ---------- */
|
|
79
|
+
.bar__legend {
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-wrap: wrap;
|
|
82
|
+
gap: var(--sp-2) var(--sp-4, 1rem);
|
|
83
|
+
margin: var(--sp-2) 0 0;
|
|
84
|
+
padding: 0;
|
|
85
|
+
list-style: none;
|
|
86
|
+
font-size: var(--fs-xs, 12px);
|
|
87
|
+
color: var(--text-secondary);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.bar__legend-item {
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: var(--sp-1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* 色块与系列共用 --bar-c ⇒ 颜色不会对不上 */
|
|
97
|
+
.bar__legend-swatch {
|
|
98
|
+
display: inline-block;
|
|
99
|
+
width: 10px;
|
|
100
|
+
height: 10px;
|
|
101
|
+
border-radius: 2px;
|
|
102
|
+
background: var(--bar-c, var(--accent));
|
|
103
|
+
flex: 0 0 auto;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ---------- 阈值线 ---------- */
|
|
107
|
+
.bar__threshold {
|
|
108
|
+
stroke: var(--text-tertiary);
|
|
109
|
+
stroke-width: 1;
|
|
110
|
+
stroke-dasharray: 4 3;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ---------- 🔴 铁律二:标注样式 ---------- */
|
|
114
|
+
.bar__label {
|
|
115
|
+
fill: var(--text-secondary);
|
|
116
|
+
font-size: 10px;
|
|
117
|
+
font-family: var(--mono);
|
|
118
|
+
font-variant-numeric: tabular-nums;
|
|
119
|
+
/* ⭐ 描边画在字**下面**(不糊住笔画)——
|
|
120
|
+
有了它,标注落在深色柱子上也看得清。
|
|
121
|
+
这是 charter 13 铁律二的三条硬性要求之一。 */
|
|
122
|
+
paint-order: stroke fill;
|
|
123
|
+
stroke: var(--surface, #fff);
|
|
124
|
+
stroke-width: 3px;
|
|
125
|
+
stroke-linejoin: round;
|
|
126
|
+
/* 标注本身不接收指针事件,否则会挡住柱子 */
|
|
127
|
+
pointer-events: none;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.bar__label--max { fill: var(--text-primary); font-weight: 700; }
|
|
131
|
+
.bar__label--clip { fill: var(--danger, #c0392b); font-weight: 700; }
|
|
132
|
+
|
|
133
|
+
/* ---------- 越界说明 ---------- */
|
|
134
|
+
.bar__note {
|
|
135
|
+
margin: var(--sp-2) 0 0;
|
|
136
|
+
font-size: var(--fs-xs, 12px);
|
|
137
|
+
line-height: 1.5;
|
|
138
|
+
color: var(--text-secondary);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ---------- 悬停反馈:只对真能悬停的设备生效 ----------
|
|
142
|
+
🔴 补(hover-gate 门禁要求):
|
|
143
|
+
`:hover` 在**触屏设备上会"粘住"** ——
|
|
144
|
+
点一下按钮,hover 状态一直保持,要点别处才消失
|
|
145
|
+
(移动端根本没有"移开鼠标"这个动作)。
|
|
146
|
+
⇒ 只对**真能悬停的设备**(鼠标 / 触控板)生效。 */
|
|
147
|
+
@media (hover: hover) and (pointer: fine) {
|
|
148
|
+
/* 🔴 悬停**不改系列色**:把柱子整块染成强调色 ⇒ 读者会把它读成另一个系列
|
|
149
|
+
(图例还在旁边写着本来的颜色,两者直接打架)。
|
|
150
|
+
⇒ 改成加一道描边:位置不变、归属颜色不变。 */
|
|
151
|
+
.bar__bar:hover {
|
|
152
|
+
stroke: var(--text-primary);
|
|
153
|
+
stroke-width: 1.5;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ---------- 暗色下容器要跟着换 ---------- */
|
|
158
|
+
@media (prefers-color-scheme: dark) {
|
|
159
|
+
.bar {
|
|
160
|
+
background: var(--surface, #1a1c1e);
|
|
161
|
+
border-color: var(--border-decor);
|
|
162
|
+
}
|
|
163
|
+
.bar__label {
|
|
164
|
+
stroke: var(--surface, #1a1c1e);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bar.js — 柱状图(charter 13 图表规范 · 四条铁律全部落地)
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* 设计依据:见 API.md「资产类组件」一节
|
|
5
|
+
* 铁律一 · 超量程必须被**容器裁剪** → CSS 侧 overflow:hidden(本文件不参与)
|
|
6
|
+
* 铁律二 · **标注不得覆盖数据**(≤4 个) → markLabel() 只标关键点
|
|
7
|
+
* 铁律三 · **量程必须显式声明** → max 是必填参数,无默认值
|
|
8
|
+
* 铁律四 · **门禁要能证伪** → 05-audit/chart-check.js 已覆盖本组件
|
|
9
|
+
*
|
|
10
|
+
* ⭐ 为什么量程不自动适配(charter 13 铁律三):
|
|
11
|
+
* 一个 400 的异常值 + 30 个 0.35 的常态值,若 Y 轴自适应到 400
|
|
12
|
+
* ⇒ 所有常态值被压成**一条贴底的直线**
|
|
13
|
+
* ⇒ 读者会得出「这些天完全没有变化」的**错误结论**。
|
|
14
|
+
* ⇒ **自动适配是最隐蔽的谎报。**
|
|
15
|
+
*
|
|
16
|
+
* 依赖:01-tokens/tokens.css(颜色全走令牌,不写死)
|
|
17
|
+
* 用法:ES5,无依赖,纯 SVG
|
|
18
|
+
* ============================================================================
|
|
19
|
+
*/
|
|
20
|
+
(function (global) {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
var NS = 'http://www.w3.org/2000/svg';
|
|
24
|
+
var uid = 0;
|
|
25
|
+
/* 分类色 5 个(bar.css 里的 .bar__s0..s4),与 scientific-plot 的 ramp 对齐。
|
|
26
|
+
超过 5 个系列请由调用方给 `series[i].cls` 自定义 —— 颜色循环会造成误读。 */
|
|
27
|
+
var SERIES_STYLES = 5;
|
|
28
|
+
|
|
29
|
+
/* ==================================================================== *
|
|
30
|
+
* 铁律二的核心:哪些点值得标数字
|
|
31
|
+
* ==================================================================== *
|
|
32
|
+
* ⭐ 只标 3 类点,**永远 ≤ 4 个**:
|
|
33
|
+
* 1) 最高点(峰值在哪)
|
|
34
|
+
* 2) 末位(现在到多少)
|
|
35
|
+
* 3) 越界点(如果有,告诉读者"这里被截断了")
|
|
36
|
+
* 30 根柱子配 30 个数字 ⇒ 必然重叠,且读者也读不完。
|
|
37
|
+
*
|
|
38
|
+
* 🔴 传入的是「每个 category 顶端代表的值」,不是某个系列的值:
|
|
39
|
+
* 堆叠模式下这一列顶端 = 该列合计;分组模式下 = 该列最高的那一根。
|
|
40
|
+
* 标的是一个系列的值、而读者看到的是另一根柱子的顶 ⇒ 那是谎报。
|
|
41
|
+
*/
|
|
42
|
+
function pickKeyPoints(values) {
|
|
43
|
+
var keys = {}, i, maxI = 0;
|
|
44
|
+
for (i = 0; i < values.length; i++) {
|
|
45
|
+
if (values[i] > values[maxI]) maxI = i;
|
|
46
|
+
}
|
|
47
|
+
keys[maxI] = 'max';
|
|
48
|
+
if (values.length) keys[values.length - 1] = 'last';
|
|
49
|
+
return keys;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function el(tag, attrs) {
|
|
53
|
+
var e = document.createElementNS(NS, tag), k;
|
|
54
|
+
for (k in attrs) {
|
|
55
|
+
if (Object.prototype.hasOwnProperty.call(attrs, k) && attrs[k] != null) {
|
|
56
|
+
e.setAttribute(k, String(attrs[k]));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return e;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ==================================================================== *
|
|
63
|
+
* Bar.draw
|
|
64
|
+
* ==================================================================== *
|
|
65
|
+
* @param {Object} opt
|
|
66
|
+
* @param {Element} opt.host 挂载点(会自动清空)
|
|
67
|
+
* @param {Array} opt.series [{ name, values:[Number], cls }]
|
|
68
|
+
* @param {string} [opt.mode] 'stack'(默认 · 堆叠)| 'group'(并列分组)
|
|
69
|
+
* @param {string} [opt.max] 🔴 **必填** —— Y 轴上限(铁律三)
|
|
70
|
+
* @param {string[]} [opt.labels] X 轴标签(可少于 values.length)
|
|
71
|
+
* @param {Number} [opt.threshold] 阈值线(画一条虚线)
|
|
72
|
+
* @param {Number} [opt.height] 画布高,默认 180
|
|
73
|
+
* @param {Number} [opt.width] 画布宽,默认 640。**窄容器必须传真实宽度**,
|
|
74
|
+
* 否则整张图被等比缩小,刻度字会缩到不可读
|
|
75
|
+
* (393 视口实测 10px → 3×6px)
|
|
76
|
+
* @param {string} [opt.note] 脚注(例如"09-01 已按 400 截断")
|
|
77
|
+
* @param {boolean}[opt.legend] 是否画图例(默认:系列数 ≥ 2 且都有名字时自动出)
|
|
78
|
+
* @returns {Object} { el, clipped, mode, tops, legend, destroy() }
|
|
79
|
+
*
|
|
80
|
+
* 🔴🔴 **堆叠 vs 分组,语义必须显式选择**(外部评审 VIZ-REPORT-01 · P0-1)
|
|
81
|
+
* --------------------------------------------------------------------
|
|
82
|
+
* 修复前的实现声称"堆叠",实际把 `base` 数组**在每个系列开头清零**,
|
|
83
|
+
* ⇒ 两个系列都从同一基线起算、**同横坐标同柱宽** ⇒ 后画的系列
|
|
84
|
+
* 直接覆盖前一个系列的底部视图上看就是一根柱子换了个色块,
|
|
85
|
+
* 读者会当成"总数",实际他看到的只是**最后一个系列**的值。
|
|
86
|
+
* ⇒ 这属于数据表达错误,而且**不会报错**(CI 全绿、截图也看不出来)。
|
|
87
|
+
*
|
|
88
|
+
* 修法不是把 `base` 挪出循环就完事,堆叠还剩三件事要交代清楚:
|
|
89
|
+
* ① **累计位置**:第 s 个系列的底部 = 前 s−1 个系列的高度和(现在真的算了)
|
|
90
|
+
* ② **合计超量程**:单独每一段都不超、加起来超了 —— 也要算"被截断",
|
|
91
|
+
* 并且只画(画布内的那一部分),不能画到坐标系外面去
|
|
92
|
+
* ③ **标注跟着堆叠顶端走**:标签写的是**该列合计**,不是第一个系列的值
|
|
93
|
+
*/
|
|
94
|
+
function draw(opt) {
|
|
95
|
+
opt = opt || {};
|
|
96
|
+
var host = opt.host;
|
|
97
|
+
if (!host) throw new Error('bar.draw: 缺少 host');
|
|
98
|
+
if (opt.max === undefined || opt.max === null) {
|
|
99
|
+
/* 🔴 铁律三:不替调用方决定量程。
|
|
100
|
+
宁可报错,也不要悄悄自适应(那会让 400 的异常值把常态值压成直线)。 */
|
|
101
|
+
throw new Error('bar.draw: max 是必填项(charter 13 铁律三:量程不得自动适配)');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var series = opt.series || [];
|
|
105
|
+
var max = +opt.max;
|
|
106
|
+
var H = opt.height || 180;
|
|
107
|
+
var nS = series.length;
|
|
108
|
+
var mode = opt.mode === 'group' ? 'group' : 'stack';
|
|
109
|
+
/* 🔴 viewBox 宽**必须是参数**,不能写死 640。
|
|
110
|
+
原来这里写的是「640;靠 CSS width:100% 自适应」—— 那句话是错的:
|
|
111
|
+
`width:100%` 只是把整张 SVG **等比缩放**,SVG 里的刻度字是绝对值(10px),
|
|
112
|
+
跟着一起缩。实测 393 视口下容器 334px ⇒ 缩放 0.52 ⇒ 10px 的字渲染成
|
|
113
|
+
**3×6 像素**,等于不可读。
|
|
114
|
+
⇒ 传真实容器宽,布局按容器重排(groupW/barW 都是从 plotW 算的,天然自适应),
|
|
115
|
+
字号保持 10px 不变。CSS 的 width:100% 此时只是"正好等于",不再缩放。
|
|
116
|
+
⚠️ 下限 240:比这更窄时五根柱子的标签必然重叠,图本身就不该在那宽度出现。 */
|
|
117
|
+
var W = Math.max(240, Math.round(+opt.width || 640));
|
|
118
|
+
var padT = 14, padB = 22, padL = 34, padR = 8;
|
|
119
|
+
var plotH = H - padT - padB;
|
|
120
|
+
var plotW = W - padL - padR;
|
|
121
|
+
var n = series.length ? series[0].values.length : 0;
|
|
122
|
+
if (!n) throw new Error('bar.draw: series 为空');
|
|
123
|
+
|
|
124
|
+
var id = 'bar' + (++uid);
|
|
125
|
+
var svg = el('svg', {
|
|
126
|
+
'viewBox': '0 0 ' + W + ' ' + H,
|
|
127
|
+
'class': 'bar__svg',
|
|
128
|
+
role: 'img',
|
|
129
|
+
'aria-label': (opt.ariaLabel || '柱状图'),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/* ---------- 网格 + Y 轴刻度 ---------- */
|
|
133
|
+
var ticks = 4, i, t, y;
|
|
134
|
+
for (i = 0; i <= ticks; i++) {
|
|
135
|
+
t = (max / ticks) * i;
|
|
136
|
+
y = padT + plotH - (t / max) * plotH;
|
|
137
|
+
svg.appendChild(el('line', {
|
|
138
|
+
'class': 'bar__grid', x1: padL, x2: W - padR, y1: y, y2: y,
|
|
139
|
+
}));
|
|
140
|
+
var tl = el('text', { 'class': 'bar__ytick', x: padL - 6, y: y + 3,
|
|
141
|
+
'text-anchor': 'end' });
|
|
142
|
+
tl.textContent = String(Math.round(t * 100) / 100);
|
|
143
|
+
svg.appendChild(tl);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ---------- 柱子 ---------- */
|
|
147
|
+
var groupW = plotW / n;
|
|
148
|
+
/* 单系列 / 堆叠:沿用历史的 0.62 宽度(不制造无关的外观漂移);
|
|
149
|
+
分组:把可用的 0.82 均分给 nS 个系列 */
|
|
150
|
+
var barW = mode === 'group'
|
|
151
|
+
? Math.max(2, (groupW * 0.82) / nS)
|
|
152
|
+
: Math.max(2, groupW * 0.62);
|
|
153
|
+
var clipped = [];
|
|
154
|
+
var x, h, yy, s, vv, bottom, top, vb, vt, rect, k0, k1;
|
|
155
|
+
|
|
156
|
+
/* 🔴🔴 累计高度的载体 —— **必须在系列循环之外**。
|
|
157
|
+
修复前它在每个系列开头被清零 ⇒ 每个系列都从基线起算 ⇒ 互相覆盖 */
|
|
158
|
+
var base = new Array(n);
|
|
159
|
+
for (k0 = 0; k0 < n; k0++) base[k0] = 0;
|
|
160
|
+
|
|
161
|
+
/* 每个类目「顶端代表的值」:堆叠 = 该列合计,分组 = 该列最高的那根。
|
|
162
|
+
⇒ 标注必须跟着它走,否则读者看到的高度和读到的数字不是一回事 */
|
|
163
|
+
var topVal = new Array(n);
|
|
164
|
+
for (k0 = 0; k0 < n; k0++) {
|
|
165
|
+
var sum = 0, mx = -Infinity;
|
|
166
|
+
for (k1 = 0; k1 < nS; k1++) {
|
|
167
|
+
vv = +series[k1].values[k0] || 0;
|
|
168
|
+
sum += vv;
|
|
169
|
+
if (vv > mx) mx = vv;
|
|
170
|
+
}
|
|
171
|
+
topVal[k0] = mode === 'stack' ? sum : mx;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (s = 0; s < nS; s++) {
|
|
175
|
+
for (i = 0; i < n; i++) {
|
|
176
|
+
vv = +series[s].values[i] || 0;
|
|
177
|
+
h = (vv / max) * plotH;
|
|
178
|
+
bottom = base[i];
|
|
179
|
+
top = bottom + h;
|
|
180
|
+
if (mode === 'stack') base[i] = top; /* ⭐ 累计:下一个系列的起点 */
|
|
181
|
+
|
|
182
|
+
/* 🔴 铁律一:**超量程必须被裁掉**,不能画到坐标系外。
|
|
183
|
+
画到 viewBox 之外 ⇒ 会穿透到页面其他内容上(真机遇到过)。
|
|
184
|
+
⇒ 堆叠时"单段都不超、合计超了"同样成立 ⇒ 一起进 clipped */
|
|
185
|
+
if (top > plotH && clipped.indexOf(i) < 0) clipped.push(i);
|
|
186
|
+
vb = Math.min(bottom, plotH); /* 可见段的底 */
|
|
187
|
+
vt = Math.min(top, plotH); /* 可见段的顶 */
|
|
188
|
+
if (vt - vb < 0.01) continue; /* 整段都在量程之上 ⇒ 不画 */
|
|
189
|
+
|
|
190
|
+
x = mode === 'group'
|
|
191
|
+
? padL + groupW * i + (groupW - barW * nS) / 2 + barW * s
|
|
192
|
+
: padL + groupW * i + (groupW - barW) / 2;
|
|
193
|
+
yy = padT + plotH - vt;
|
|
194
|
+
|
|
195
|
+
rect = el('rect', {
|
|
196
|
+
'class': 'bar__bar bar__s' + (s % SERIES_STYLES) +
|
|
197
|
+
(series[s].cls ? ' ' + series[s].cls : ''),
|
|
198
|
+
x: x, y: yy, width: barW, height: vt - vb,
|
|
199
|
+
});
|
|
200
|
+
/* 只有露在最顶端、且没被裁的那一段才倒圆角 ——
|
|
201
|
+
堆叠**中间**段倒角会露出接缝,看着像柱子断掉了 */
|
|
202
|
+
if (top <= plotH && vt === top) rect.setAttribute('rx', 1.5);
|
|
203
|
+
/* 被裁掉的那一段:明确标记,配合 bar.css 的样式告诉读者"这是截断的" */
|
|
204
|
+
if (vt < top) rect.setAttribute('data-clipped', '1');
|
|
205
|
+
svg.appendChild(rect);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* ---------- 阈值线 ---------- */
|
|
210
|
+
if (opt.threshold != null) {
|
|
211
|
+
var th = (+opt.threshold / max) * plotH;
|
|
212
|
+
y = padT + plotH - th;
|
|
213
|
+
svg.appendChild(el('line', {
|
|
214
|
+
'class': 'bar__threshold', x1: padL, x2: W - padR, y1: y, y2: y,
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ---------- X 轴标签(抽稀,避免挤成一团) ---------- */
|
|
219
|
+
var labels = opt.labels || [];
|
|
220
|
+
var every = Math.max(1, Math.ceil(n / 7));
|
|
221
|
+
for (i = 0; i < labels.length; i++) {
|
|
222
|
+
if (i % every !== 0 && i !== labels.length - 1) continue;
|
|
223
|
+
var xt = el('text', {
|
|
224
|
+
'class': 'bar__xlabel',
|
|
225
|
+
x: padL + groupW * i + groupW / 2,
|
|
226
|
+
y: H - 6, 'text-anchor': 'middle',
|
|
227
|
+
});
|
|
228
|
+
xt.textContent = labels[i];
|
|
229
|
+
svg.appendChild(xt);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* ---------- 🔴 铁律二:只标关键点(≤4 个),且不压数据 ---------- */
|
|
233
|
+
/* 注意:keys 来自 topVal(看到的那个顶端),不是 series[0] */
|
|
234
|
+
var keys = pickKeyPoints(topVal);
|
|
235
|
+
/* 越界点也标一个(告诉读者"这里被截断了")*/
|
|
236
|
+
clipped.forEach(function (ci) { keys[ci] = keys[ci] || 'clip'; });
|
|
237
|
+
|
|
238
|
+
var count = 0;
|
|
239
|
+
for (var k in keys) {
|
|
240
|
+
if (!Object.prototype.hasOwnProperty.call(keys, k)) continue;
|
|
241
|
+
if (count >= 4) break; /* ⭐ 硬上限 4 个 */
|
|
242
|
+
var i2 = +k;
|
|
243
|
+
var v2 = +topVal[i2] || 0;
|
|
244
|
+
var h2 = Math.min((v2 / max) * plotH, plotH);
|
|
245
|
+
var yy2 = padT + plotH - h2;
|
|
246
|
+
var lb = el('text', {
|
|
247
|
+
/* 🔴 charter 13 铁律二:标注画在柱顶**外侧**(yy - 5),
|
|
248
|
+
不画在柱子上;且顶外侧不够高时改画内侧底部(见下方 clamp)。
|
|
249
|
+
描边用 paint-order(在字下面),保证深色柱子上也看得清。 */
|
|
250
|
+
'class': 'bar__label bar__label--' + keys[k],
|
|
251
|
+
x: padL + groupW * i2 + groupW / 2,
|
|
252
|
+
y: yy2 - 5 < padT + 8 ? yy2 + 12 : yy2 - 5,
|
|
253
|
+
'text-anchor': 'middle',
|
|
254
|
+
});
|
|
255
|
+
lb.textContent = (keys[k] === 'clip' ? '▲ ' : '') +
|
|
256
|
+
(Math.round(v2 * 100) / 100);
|
|
257
|
+
svg.appendChild(lb);
|
|
258
|
+
count++;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
host.innerHTML = '';
|
|
262
|
+
host.appendChild(svg);
|
|
263
|
+
|
|
264
|
+
/* ---------- 图例:两个以上系列时,颜色必须能被"翻译"成名字 ---------- *
|
|
265
|
+
* 🔴 为什么默认就出(不再由调用方记得开):
|
|
266
|
+
* 两种颜色 + 没有图例 ⇒ 读者只能猜哪块是哪个系列;
|
|
267
|
+
* 在黑白打印 / 色觉障碍下这块就直接失读了。
|
|
268
|
+
* 这与 charter 13「重要结论不允许只存在于颜色里」是同一条。
|
|
269
|
+
* ⇒ 要关就显式传 `legend: false`,别指望"忘了传也没事"。 */
|
|
270
|
+
var hasNames = series.every(function (se) { return !!se.name; });
|
|
271
|
+
var wantLegend = opt.legend === undefined ? (nS >= 2 && hasNames) : !!opt.legend;
|
|
272
|
+
var legendEl = null;
|
|
273
|
+
if (wantLegend) {
|
|
274
|
+
legendEl = document.createElement('ul');
|
|
275
|
+
legendEl.className = 'bar__legend';
|
|
276
|
+
series.forEach(function (se, si) {
|
|
277
|
+
var li = document.createElement('li');
|
|
278
|
+
li.className = 'bar__legend-item';
|
|
279
|
+
var sw = document.createElement('span');
|
|
280
|
+
sw.className = 'bar__legend-swatch bar__s' + (si % SERIES_STYLES) +
|
|
281
|
+
(se.cls ? ' ' + se.cls : '');
|
|
282
|
+
/* 🔴 用 textContent,不用 innerHTML —— 系列名来自调用方的数据 */
|
|
283
|
+
li.appendChild(sw);
|
|
284
|
+
var tx = document.createElement('span');
|
|
285
|
+
tx.textContent = se.name || ('系列 ' + (si + 1));
|
|
286
|
+
li.appendChild(tx);
|
|
287
|
+
legendEl.appendChild(li);
|
|
288
|
+
});
|
|
289
|
+
host.appendChild(legendEl);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* ---------- 越界说明(必须有,否则读者会误读被裁掉的数据) ---------- */
|
|
293
|
+
if (clipped.length && opt.note) {
|
|
294
|
+
var p = document.createElement('p');
|
|
295
|
+
p.className = 'bar__note';
|
|
296
|
+
p.textContent = opt.note;
|
|
297
|
+
host.appendChild(p);
|
|
298
|
+
} else if (clipped.length) {
|
|
299
|
+
var p2 = document.createElement('p');
|
|
300
|
+
p2.className = 'bar__note';
|
|
301
|
+
p2.textContent = '有 ' + clipped.length + ' 个' +
|
|
302
|
+
(mode === 'stack' ? '类目的合计' : '数据') +
|
|
303
|
+
'超出量程(上限 ' + max + '),已按上限截断显示。';
|
|
304
|
+
host.appendChild(p2);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
el: svg,
|
|
309
|
+
clipped: clipped,
|
|
310
|
+
mode: mode,
|
|
311
|
+
/* 每个类目顶端的真实数值(堆叠=合计 / 分组=最高),
|
|
312
|
+
给门禁做几何断言用 —— 判据不该靠读 DOM 反推 */
|
|
313
|
+
tops: topVal,
|
|
314
|
+
legend: legendEl,
|
|
315
|
+
destroy: function () { host.innerHTML = ''; },
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
global.Bar = { draw: draw };
|
|
320
|
+
})(window);
|