@heycys/gallery 0.1.1 → 0.1.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/THIRD-PARTY-NOTICES.md +1 -1
- package/browser/editor/components/columns/page.mjs +4 -4
- package/browser/editor/components/embed/index.mjs +2 -2
- package/browser/editor/components/flowchart/add.mjs +7 -6
- package/browser/editor/components/flowchart/page.mjs +1 -1
- package/browser/editor/components/icons.mjs +8 -0
- package/browser/editor/components/image/crop.mjs +6 -9
- package/browser/editor/components/installed/index.mjs +11 -4
- package/browser/editor/components/installed/page.mjs +27 -16
- package/browser/editor/components/installed/served.mjs +6 -5
- package/browser/editor/components/instances.mjs +18 -0
- package/browser/editor/components/list/page.mjs +3 -6
- package/browser/editor/components/registry.mjs +13 -4
- package/browser/editor/components/table/index.mjs +59 -2
- package/browser/editor/components/table/page.mjs +1 -1
- package/browser/editor/page/annotate/card.mjs +3 -6
- package/browser/editor/page/autosave.mjs +310 -37
- package/browser/editor/page/board/client.mjs +57 -0
- package/browser/editor/page/board/hint.mjs +145 -0
- package/browser/editor/page/board/history.mjs +119 -0
- package/browser/editor/page/board/index.mjs +970 -0
- package/browser/editor/page/board/settings.mjs +209 -0
- package/browser/editor/page/caret.mjs +1 -1
- package/browser/editor/page/changes/bar.mjs +6 -14
- package/browser/editor/page/changes/index.mjs +2 -2
- package/browser/editor/page/clipboard/import/html.mjs +29 -6
- package/browser/editor/page/clipboard/import/inline.mjs +32 -0
- package/browser/editor/page/clipboard/import/markdown.mjs +58 -13
- package/browser/editor/page/clipboard/import/read.mjs +6 -1
- package/browser/editor/page/clipboard/import/to-blocks.mjs +44 -12
- package/browser/editor/page/clipboard/import/yuque.mjs +622 -0
- package/browser/editor/page/clipboard/index.mjs +124 -13
- package/browser/editor/page/clipboard/own/blocks.mjs +11 -11
- package/browser/editor/page/clipboard/own/chart-keys.mjs +41 -28
- package/browser/editor/page/clipboard/own/selection.mjs +1 -1
- package/browser/editor/page/columns/index.mjs +48 -46
- package/browser/editor/page/commit.mjs +80 -22
- package/browser/editor/page/conflicts.mjs +7 -7
- package/browser/editor/page/core.mjs +31 -15
- package/browser/editor/page/custom-page.mjs +106 -0
- package/browser/editor/page/editor.mjs +51 -14
- package/browser/editor/page/emoji-picker/styles.mjs +7 -12
- package/browser/editor/page/emoji-picker/upload.mjs +2 -2
- package/browser/editor/page/handle/index.mjs +5 -8
- package/browser/editor/page/history.mjs +24 -4
- package/browser/editor/page/links/index.mjs +5 -3
- package/browser/editor/page/links/pages.mjs +27 -9
- package/browser/editor/page/look.mjs +54 -3
- package/browser/editor/page/margin.mjs +18 -3
- package/browser/editor/page/media-menu/index.mjs +6 -7
- package/browser/editor/page/menu.mjs +18 -7
- package/browser/editor/page/notices.mjs +2 -2
- package/browser/editor/page/page-objects.mjs +13 -2
- package/browser/editor/page/panel.mjs +4 -9
- package/browser/editor/page/present-links.mjs +11 -2
- package/browser/editor/page/resize/index.mjs +42 -29
- package/browser/editor/page/served.mjs +14 -0
- package/browser/editor/page/session.mjs +48 -4
- package/browser/editor/page/slash-menu/index.mjs +3 -3
- package/browser/editor/page/table/cells.mjs +102 -46
- package/browser/editor/page/table/icons.mjs +2 -0
- package/browser/editor/page/table/index.mjs +29 -10
- package/browser/editor/page/table/pictures.mjs +299 -0
- package/browser/editor/page/table/views.mjs +150 -0
- package/browser/editor/page/text-select/index.mjs +26 -12
- package/browser/editor/page/todo-writes.mjs +75 -0
- package/browser/editor/page/typing/index.mjs +53 -3
- package/browser/editor/page/ui.mjs +13 -0
- package/browser/shared/block-packs.mjs +404 -0
- package/browser/shared/board-places.mjs +224 -0
- package/browser/shared/card-size.mjs +59 -0
- package/browser/shared/colors.mjs +3 -2
- package/browser/shared/component-catalog.mjs +323 -46
- package/browser/shared/component-source.mjs +37 -0
- package/browser/shared/components/index.mjs +12 -3
- package/browser/shared/components/installed.mjs +35 -3
- package/browser/shared/components/page.mjs +18 -8
- package/browser/shared/components/state.mjs +5 -1
- package/browser/shared/document-look.mjs +10 -5
- package/browser/shared/drafts.mjs +432 -0
- package/browser/shared/extension-sidebar.mjs +126 -0
- package/browser/shared/extensions.mjs +79 -0
- package/browser/shared/file-table.mjs +101 -0
- package/browser/shared/i18n/en.mjs +307 -27
- package/browser/shared/json-layout.mjs +47 -0
- package/browser/shared/net-cache.mjs +133 -0
- package/browser/shared/net-keys.mjs +32 -0
- package/browser/shared/net-policy.mjs +177 -0
- package/browser/shared/outline-shape.mjs +128 -0
- package/browser/shared/outline.mjs +331 -67
- package/browser/shared/pack-manifest.mjs +77 -0
- package/browser/shared/page-address.mjs +6 -3
- package/browser/shared/page-cards.mjs +308 -0
- package/browser/shared/page-links.mjs +69 -6
- package/browser/shared/pages.mjs +388 -0
- package/browser/shared/paths.mjs +6 -0
- package/browser/shared/preview-server.mjs +3 -7
- package/browser/shared/session.mjs +8 -1
- package/browser/shared/shipped-blocks.mjs +299 -0
- package/browser/shared/table-chart.mjs +33 -1
- package/browser/shared/todos.mjs +83 -0
- package/browser/shared/user-directory.mjs +24 -0
- package/browser/shared/versions.mjs +14 -0
- package/browser/shared/web-links.mjs +134 -0
- package/browser/shared/workspace-columns.mjs +120 -0
- package/browser/shared/workspace-name.mjs +154 -0
- package/browser/shared/workspace-query.mjs +125 -0
- package/browser/shared/workspace-registry.mjs +272 -0
- package/browser/shared/workspace.mjs +171 -20
- package/dist/chunk-2NRBY6AJ.mjs +2 -0
- package/dist/chunk-32ZUEFZ7.mjs +2 -0
- package/dist/chunk-35NQ4PLQ.mjs +2 -0
- package/dist/chunk-3KJHLHUO.mjs +2 -0
- package/dist/chunk-3NJACR4W.mjs +2 -0
- package/dist/chunk-4AV456RQ.mjs +2 -0
- package/dist/chunk-4OZWXZE5.mjs +5 -0
- package/dist/chunk-5XYHDBJ5.mjs +2 -0
- package/dist/chunk-5ZJA7CXT.mjs +2 -0
- package/dist/chunk-6A53MZYZ.mjs +2 -0
- package/dist/chunk-6EXH3ORZ.mjs +166 -0
- package/dist/chunk-6EZ7FNIK.mjs +3 -0
- package/dist/{chunk-V5JTW2LH.mjs → chunk-6QBHU4DF.mjs} +1 -1
- package/dist/{chunk-Y6RLRPG3.mjs → chunk-6VTU6KEO.mjs} +1 -1
- package/dist/chunk-B4P2CM2J.mjs +3 -0
- package/dist/{chunk-FDLXYYG4.mjs → chunk-BPL64HGA.mjs} +1 -1
- package/dist/chunk-BVEFGE4B.mjs +2 -0
- package/dist/{chunk-LS4F7JUM.mjs → chunk-BZ5RAF7L.mjs} +1 -1
- package/dist/chunk-CRJEG7MT.mjs +2 -0
- package/dist/{chunk-LCTHTZIL.mjs → chunk-D4CKRD3V.mjs} +91 -91
- package/dist/chunk-D4EURAX4.mjs +4 -0
- package/dist/chunk-DQCE7TJY.mjs +5 -0
- package/dist/chunk-DR73Y2UJ.mjs +3 -0
- package/dist/{chunk-TMYZB6AV.mjs → chunk-EKNHM3P4.mjs} +1 -1
- package/dist/{chunk-GB5SOYQJ.mjs → chunk-ERLAIBOA.mjs} +2 -2
- package/dist/chunk-EURLTOTZ.mjs +4 -0
- package/dist/chunk-FJYN7RTH.mjs +4 -0
- package/dist/chunk-FWNRPMCJ.mjs +12 -0
- package/dist/chunk-FWW4CSAZ.mjs +2 -0
- package/dist/{chunk-2B5E4CDJ.mjs → chunk-GPIWQ4X4.mjs} +1 -1
- package/dist/chunk-GY3FAUVM.mjs +2 -0
- package/dist/chunk-HLYV7RIH.mjs +2 -0
- package/dist/chunk-HMYTM3GT.mjs +15 -0
- package/dist/chunk-IA4PSIMD.mjs +2 -0
- package/dist/chunk-JEUK3ZO7.mjs +2 -0
- package/dist/chunk-KTKHRC6H.mjs +2 -0
- package/dist/chunk-L7UP52JO.mjs +2 -0
- package/dist/{chunk-SYGN7GLU.mjs → chunk-MHW7ZKOU.mjs} +1 -1
- package/dist/chunk-N76HWBA3.mjs +10 -0
- package/dist/chunk-N7AZZLGT.mjs +548 -0
- package/dist/chunk-NAMN3RAF.mjs +2 -0
- package/dist/{chunk-5ZKJ3H4P.mjs → chunk-NFMCP2HI.mjs} +3 -3
- package/dist/chunk-NG375ILQ.mjs +2 -0
- package/dist/chunk-NVRNLK25.mjs +6 -0
- package/dist/chunk-NWFBE2YA.mjs +2 -0
- package/dist/chunk-NWR2LT5B.mjs +2 -0
- package/dist/chunk-OJKMINIU.mjs +20 -0
- package/dist/{chunk-4FS7Z7WR.mjs → chunk-OUUP3WKT.mjs} +1 -1
- package/dist/{chunk-BGGDRNYQ.mjs → chunk-OW5NWPOJ.mjs} +1 -1
- package/dist/chunk-PAREWQGB.mjs +4 -0
- package/dist/chunk-POQSE4C2.mjs +2 -0
- package/dist/chunk-PUOQ65NC.mjs +2 -0
- package/dist/{chunk-XNROSH2T.mjs → chunk-PVER7LSC.mjs} +2 -2
- package/dist/chunk-PZDLJJTJ.mjs +3 -0
- package/dist/chunk-Q47PLKIJ.mjs +15 -0
- package/dist/chunk-Q4UEPUWF.mjs +2 -0
- package/dist/chunk-QKXMPUXC.mjs +2 -0
- package/dist/chunk-QOUEFUCH.mjs +16 -0
- package/dist/chunk-RFDZXFJF.mjs +13 -0
- package/dist/chunk-RKPWLGZ3.mjs +4 -0
- package/dist/chunk-RMECZA2K.mjs +2 -0
- package/dist/{chunk-7DTINW6U.mjs → chunk-SQ6Y2ZMA.mjs} +2 -2
- package/dist/chunk-SX4LJYT6.mjs +8 -0
- package/dist/chunk-SXHBYZST.mjs +16 -0
- package/dist/chunk-TANZRINB.mjs +2 -0
- package/dist/chunk-TEZAWDSQ.mjs +2 -0
- package/dist/chunk-UBBRTJ2R.mjs +24 -0
- package/dist/chunk-UFNYFFKO.mjs +20 -0
- package/dist/chunk-UGWYKHBL.mjs +170 -0
- package/dist/chunk-UKSHDZZ6.mjs +2 -0
- package/dist/chunk-UNOYVIYR.mjs +2 -0
- package/dist/{chunk-E7LWZRXY.mjs → chunk-V6RIYQZZ.mjs} +1 -1
- package/dist/chunk-VJPFVNNW.mjs +15 -0
- package/dist/chunk-VS3ZMVGX.mjs +5 -0
- package/dist/chunk-VTKNEYT3.mjs +6 -0
- package/dist/chunk-VXACTLO3.mjs +3 -0
- package/dist/chunk-YI43RG4P.mjs +2 -0
- package/dist/chunk-YQGT4AB2.mjs +6 -0
- package/dist/chunk-ZB4H7ZN4.mjs +2 -0
- package/dist/{chunk-IVDLMMBD.mjs → chunk-ZHEHPMBK.mjs} +1 -1
- package/dist/chunk-ZTHICSSF.mjs +2 -0
- package/dist/chunk-ZUNVAXLH.mjs +2 -0
- package/dist/chunk-ZWPMCK7P.mjs +2 -0
- package/dist/cli.mjs +1 -1
- package/dist/server.mjs +1 -1
- package/dist/{chunk-U6KNPJPR.mjs → update-fetch.mjs} +1 -1
- package/dist/worker.mjs +1 -1
- package/kit/board.css +95 -0
- package/kit/board.js +250 -0
- package/kit/charts.js +5 -71
- package/kit/code.js +14 -14
- package/kit/report.css +255 -17
- package/kit/runtime.js +1600 -125
- package/kit/state.js +10 -1
- package/kit/tables.js +128 -43
- package/kit/ui.js +77 -35
- package/package.json +2 -1
- package/packs/gallery/card/component.json +14 -0
- package/packs/gallery/card/example.html +10 -0
- package/packs/gallery/card/preview.svg +1 -0
- package/packs/gallery/card/style.css +6 -0
- package/packs/gallery/card/template.html +6 -0
- package/packs/gallery/card/writing.md +14 -0
- package/packs/gallery/chart-card/component.json +15 -0
- package/packs/gallery/chart-card/example.html +14 -0
- package/packs/gallery/chart-card/kit.js +130 -0
- package/packs/gallery/chart-card/preview.svg +1 -0
- package/packs/gallery/chart-card/style.css +42 -0
- package/packs/gallery/chart-card/template.html +13 -0
- package/packs/gallery/chart-card/writing.md +20 -0
- package/packs/gallery/chart-view/component.json +20 -0
- package/packs/gallery/chart-view/example.html +13 -0
- package/packs/gallery/chart-view/kit.js +355 -0
- package/packs/gallery/chart-view/preview.svg +1 -0
- package/packs/gallery/chart-view/style.css +94 -0
- package/packs/gallery/chart-view/template.html +3 -0
- package/packs/gallery/chart-view/writing.md +22 -0
- package/packs/gallery/countdown/component.json +18 -0
- package/packs/gallery/countdown/example.html +3 -0
- package/packs/gallery/countdown/kit.js +156 -0
- package/packs/gallery/countdown/preview.svg +1 -0
- package/packs/gallery/countdown/style.css +130 -0
- package/packs/gallery/countdown/template.html +3 -0
- package/packs/gallery/countdown/writing.md +22 -0
- package/packs/gallery/habit/component.json +14 -0
- package/packs/gallery/habit/example.html +3 -0
- package/packs/gallery/habit/kit.js +295 -0
- package/packs/gallery/habit/preview.svg +1 -0
- package/packs/gallery/habit/style.css +246 -0
- package/packs/gallery/habit/template.html +3 -0
- package/packs/gallery/habit/writing.md +21 -0
- package/packs/gallery/heatmap/component.json +18 -0
- package/packs/gallery/heatmap/example.html +3 -0
- package/packs/gallery/heatmap/kit.js +351 -0
- package/packs/gallery/heatmap/preview.svg +1 -0
- package/packs/gallery/heatmap/style.css +201 -0
- package/packs/gallery/heatmap/template.html +3 -0
- package/packs/gallery/heatmap/writing.md +21 -0
- package/packs/gallery/kanban/component.json +18 -0
- package/packs/gallery/kanban/example.html +12 -0
- package/packs/gallery/kanban/kit.js +540 -0
- package/packs/gallery/kanban/preview.svg +1 -0
- package/packs/gallery/kanban/style.css +212 -0
- package/packs/gallery/kanban/template.html +3 -0
- package/packs/gallery/kanban/writing.md +20 -0
- package/packs/gallery/metric/component.json +22 -0
- package/packs/gallery/metric/example.html +15 -0
- package/packs/gallery/metric/kit.js +248 -0
- package/packs/gallery/metric/preview.svg +1 -0
- package/packs/gallery/metric/style.css +92 -0
- package/packs/gallery/metric/template.html +3 -0
- package/packs/gallery/metric/writing.md +23 -0
- package/packs/gallery/note/component.json +14 -0
- package/packs/gallery/note/example.html +3 -0
- package/packs/gallery/note/kit.js +102 -0
- package/packs/gallery/note/preview.svg +1 -0
- package/packs/gallery/note/style.css +76 -0
- package/packs/gallery/note/template.html +3 -0
- package/packs/gallery/note/writing.md +18 -0
- package/packs/gallery/pack.json +8 -0
- package/packs/gallery/recent/component.json +13 -0
- package/packs/gallery/recent/example.html +1 -0
- package/packs/gallery/recent/kit.js +238 -0
- package/packs/gallery/recent/preview.svg +1 -0
- package/packs/gallery/recent/style.css +158 -0
- package/packs/gallery/recent/template.html +1 -0
- package/packs/gallery/recent/writing.md +15 -0
- package/packs/gallery/static/component.json +13 -0
- package/packs/gallery/static/example.html +11 -0
- package/packs/gallery/static/preview.svg +1 -0
- package/packs/gallery/static/style.css +138 -0
- package/packs/gallery/static/template.html +1 -0
- package/packs/gallery/static/writing.md +18 -0
- package/packs/gallery/stats/component.json +13 -0
- package/packs/gallery/stats/example.html +1 -0
- package/packs/gallery/stats/kit.js +166 -0
- package/packs/gallery/stats/preview.svg +1 -0
- package/packs/gallery/stats/style.css +151 -0
- package/packs/gallery/stats/template.html +1 -0
- package/packs/gallery/stats/writing.md +11 -0
- package/packs/gallery/today/component.json +12 -0
- package/packs/gallery/today/example.html +1 -0
- package/packs/gallery/today/kit.js +185 -0
- package/packs/gallery/today/preview.svg +1 -0
- package/packs/gallery/today/style.css +120 -0
- package/packs/gallery/today/template.html +1 -0
- package/packs/gallery/today/writing.md +14 -0
- package/packs/gallery/todos/component.json +19 -0
- package/packs/gallery/todos/example.html +3 -0
- package/packs/gallery/todos/kit.js +342 -0
- package/packs/gallery/todos/preview.svg +1 -0
- package/packs/gallery/todos/style.css +267 -0
- package/packs/gallery/todos/template.html +3 -0
- package/packs/gallery/todos/writing.md +30 -0
- package/packs/gallery/weather/component.json +19 -0
- package/packs/gallery/weather/example.html +3 -0
- package/packs/gallery/weather/kit.js +312 -0
- package/packs/gallery/weather/preview.svg +1 -0
- package/packs/gallery/weather/style.css +258 -0
- package/packs/gallery/weather/template.html +3 -0
- package/packs/gallery/weather/writing.md +26 -0
- package/packs/gallery-soft/pack.json +8 -0
- package/release.json +12 -10
- package/sdk/doc-kit.d.ts +396 -5
- package/sdk/doc-state.d.ts +2 -0
- package/skill/SKILL.md +56 -23
- package/skill/references/block-script.md +140 -0
- package/skill/references/building-blocks.md +257 -0
- package/skill/references/extensions.md +59 -0
- package/skill/references/pages-and-data.md +221 -0
- package/skill/references/writing-html.md +6 -4
- package/skill/sdk/doc-kit.d.ts +396 -5
- package/skill/sdk/doc-state.d.ts +2 -0
- package/web/assets/index-BK2e_07w.js +13 -0
- package/web/assets/index-CWFI83mF.css +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-3HIRPVLU.mjs +0 -2
- package/dist/chunk-5MHMDLZD.mjs +0 -4
- package/dist/chunk-64K7IPLO.mjs +0 -255
- package/dist/chunk-6QXGZM36.mjs +0 -8
- package/dist/chunk-6RGJOMJP.mjs +0 -16
- package/dist/chunk-AJ73XJF3.mjs +0 -2
- package/dist/chunk-AUZGSZJH.mjs +0 -13
- package/dist/chunk-BFE45AKL.mjs +0 -2
- package/dist/chunk-BXO2ZY5P.mjs +0 -2
- package/dist/chunk-CEH4MRCC.mjs +0 -2
- package/dist/chunk-CY2FNNAB.mjs +0 -16
- package/dist/chunk-DFUDV4LX.mjs +0 -2
- package/dist/chunk-DKXG4XDG.mjs +0 -2
- package/dist/chunk-EZQ34M3F.mjs +0 -124
- package/dist/chunk-EZY3WC6Z.mjs +0 -15
- package/dist/chunk-FUKQNOQS.mjs +0 -9
- package/dist/chunk-JPOSLGOC.mjs +0 -6
- package/dist/chunk-L3QKH4OK.mjs +0 -26
- package/dist/chunk-LAE5NQ4B.mjs +0 -2
- package/dist/chunk-LEF6DRCK.mjs +0 -59
- package/dist/chunk-NO5NJCHO.mjs +0 -2
- package/dist/chunk-NQPVAU63.mjs +0 -2
- package/dist/chunk-NSKUTQ5J.mjs +0 -5
- package/dist/chunk-O35FIXRB.mjs +0 -2
- package/dist/chunk-P2CAZPDP.mjs +0 -3
- package/dist/chunk-PL6GHLGD.mjs +0 -15
- package/dist/chunk-QJIR2BLP.mjs +0 -6
- package/dist/chunk-QUPLVPB2.mjs +0 -7
- package/dist/chunk-R7BNENLY.mjs +0 -4
- package/dist/chunk-S6O4UHLC.mjs +0 -2
- package/dist/chunk-SGJWQKEI.mjs +0 -2
- package/dist/chunk-TX6JH7TR.mjs +0 -2
- package/dist/chunk-TYK6VNAB.mjs +0 -10
- package/dist/chunk-UALRNVSE.mjs +0 -4
- package/dist/chunk-V3HXVQEM.mjs +0 -2
- package/dist/chunk-V5VXUJKG.mjs +0 -2
- package/dist/chunk-VBXMADMP.mjs +0 -2
- package/dist/chunk-VNV55RUA.mjs +0 -2
- package/dist/chunk-VPD2PS35.mjs +0 -2
- package/dist/chunk-W4VKN3GF.mjs +0 -7
- package/dist/chunk-W5BXXPDJ.mjs +0 -3
- package/dist/chunk-XUNHRCOG.mjs +0 -6
- package/skill/references/building-components.md +0 -367
- package/web/assets/index-Bh74DfsV.css +0 -1
- package/web/assets/index-UW0RyFFt.js +0 -13
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<div data-doc-component="gallery/chart-card">
|
|
2
|
+
<h3>各渠道的新增用户(千人)</h3>
|
|
3
|
+
<div data-doc-slot="table">
|
|
4
|
+
<table>
|
|
5
|
+
<thead><tr><th>渠道</th><th>上月</th><th>本月</th></tr></thead>
|
|
6
|
+
<tbody>
|
|
7
|
+
<tr><td>搜索</td><td>42</td><td>48</td></tr>
|
|
8
|
+
<tr><td>社交媒体</td><td>31</td><td>27</td></tr>
|
|
9
|
+
<tr><td>老用户推荐</td><td>18</td><td>24</td></tr>
|
|
10
|
+
<tr><td>线下活动</td><td>9</td><td>—</td></tr>
|
|
11
|
+
</tbody>
|
|
12
|
+
</table>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// gallery/chart-card: a card with a title, the bar chart this script draws from the table in its slot,
|
|
2
|
+
// and that table. The table is the document's, a plain table in the slot: the reader edits it as any
|
|
3
|
+
// table of the page, and each change of it (`source`) draws the bars again. Its first column names the
|
|
4
|
+
// bars; each column after it that holds a number is a series, its header the legend's words (bars one
|
|
5
|
+
// under another, a legend above them when there are two or more); a cell with no number has no bar.
|
|
6
|
+
// The chart is no chart card of the kit's: "/" makes what a template's slot holds as a paste of it
|
|
7
|
+
// makes blocks, and a paste makes a table and never a chart card, so the block draws the bars itself.
|
|
8
|
+
// They are laid out in percents and colored by the kit's variables, so that a width or a theme draws
|
|
9
|
+
// nothing again; the manifest's `runtime` names what is drawn, which the file never holds.
|
|
10
|
+
|
|
11
|
+
DocKit.component({
|
|
12
|
+
id: "gallery/chart-card",
|
|
13
|
+
on: ["source"],
|
|
14
|
+
mount(host, context) {
|
|
15
|
+
const drawn = document.createElement("div");
|
|
16
|
+
drawn.className = "gallery-chart-card-bars";
|
|
17
|
+
const slot = context.part("table");
|
|
18
|
+
host.insertBefore(drawn, slot && slot.parentElement === host ? slot : null);
|
|
19
|
+
const draw = () => drawBars(drawn, tableOf(context.part("table")));
|
|
20
|
+
draw();
|
|
21
|
+
return { update: draw };
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The first table in `slot` as words: the names of its columns (the last row of its <thead>, or its
|
|
27
|
+
* first row) and the rows after them, a <tfoot> left out. Null where the slot holds no table.
|
|
28
|
+
* @param {HTMLElement | null} slot
|
|
29
|
+
* @returns {{ columns: string[], rows: string[][] } | null}
|
|
30
|
+
*/
|
|
31
|
+
function tableOf(slot) {
|
|
32
|
+
const table = slot?.querySelector("table");
|
|
33
|
+
if (!(table instanceof HTMLTableElement)) return null;
|
|
34
|
+
/** @param {HTMLTableRowElement} row */
|
|
35
|
+
const words = (row) => [...row.cells].map((cell) => (cell.textContent ?? "").replace(/\s+/g, " ").trim());
|
|
36
|
+
const rows = [...table.rows].filter((row) => row.parentElement?.localName !== "tfoot");
|
|
37
|
+
const head = table.tHead?.rows[table.tHead.rows.length - 1] ?? rows[0];
|
|
38
|
+
if (!head) return null;
|
|
39
|
+
return { columns: words(head), rows: rows.filter((row) => row !== head && row.parentElement?.localName !== "thead").map(words) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A cell's number as a reader writes one (1,234 · 12.5% · −3), or null.
|
|
44
|
+
* @param {string} text
|
|
45
|
+
*/
|
|
46
|
+
function numberOf(text) {
|
|
47
|
+
const plain = text.replace(/[\s,,]/g, "").replace(/^[−–]/, "-").replace(/%$/, "");
|
|
48
|
+
return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[-+]?\d+)?$/i.test(plain) ? Number(plain) : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* An element of the drawing, of `kind` (its class) and with `words`.
|
|
53
|
+
* @param {string} kind
|
|
54
|
+
* @param {string} [words]
|
|
55
|
+
*/
|
|
56
|
+
function part(kind, words) {
|
|
57
|
+
const node = document.createElement("span");
|
|
58
|
+
node.className = `gallery-chart-card-${kind}`;
|
|
59
|
+
if (words !== undefined) node.textContent = words;
|
|
60
|
+
return node;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The bars of `table` in `drawn`, all it held before taken off.
|
|
65
|
+
* @param {HTMLElement} drawn
|
|
66
|
+
* @param {{ columns: string[], rows: string[][] } | null} table
|
|
67
|
+
*/
|
|
68
|
+
function drawBars(drawn, table) {
|
|
69
|
+
const chinese = /^zh/i.test(document.documentElement.lang || "zh");
|
|
70
|
+
const [name = "", ...rest] = table?.columns ?? [];
|
|
71
|
+
const rows = table?.rows ?? [];
|
|
72
|
+
const series = rest.map((label, index) => ({ label, column: index + 1 })).filter(({ column }) => rows.some((row) => numberOf(row[column] ?? "") !== null));
|
|
73
|
+
drawn.replaceChildren();
|
|
74
|
+
drawn.classList.remove("gallery-chart-card-below", "gallery-chart-card-many");
|
|
75
|
+
drawn.removeAttribute("role");
|
|
76
|
+
drawn.removeAttribute("aria-label");
|
|
77
|
+
if (series.length === 0) {
|
|
78
|
+
const empty = document.createElement("p");
|
|
79
|
+
empty.className = "gallery-chart-card-empty";
|
|
80
|
+
empty.textContent = chinese ? "表格里还没有数:第一列写名字,后面的列写数。" : "The table has no numbers yet: names in its first column, numbers in the columns after it.";
|
|
81
|
+
drawn.append(empty);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const values = rows.flatMap((row) => series.map(({ column }) => numberOf(row[column] ?? ""))).filter((value) => value !== null);
|
|
85
|
+
const low = Math.min(0, ...values);
|
|
86
|
+
const span = Math.max(0, ...values) - low || 1;
|
|
87
|
+
/** @param {number} value */
|
|
88
|
+
const at = (value) => ((value - low) / span) * 100;
|
|
89
|
+
const zero = at(0);
|
|
90
|
+
drawn.classList.toggle("gallery-chart-card-below", low < 0);
|
|
91
|
+
drawn.classList.toggle("gallery-chart-card-many", series.length > 1);
|
|
92
|
+
drawn.style.setProperty("--gallery-chart-card-zero", `${zero}%`);
|
|
93
|
+
drawn.setAttribute("role", "img");
|
|
94
|
+
drawn.setAttribute("aria-label", chinese ? `${series.map(({ label }) => label).join("、")},按${name}` : `${series.map(({ label }) => label).join(", ")} by ${name}`);
|
|
95
|
+
const color = (/** @type {number} */ index) => `var(--doc-series-${(index % 6) + 1})`;
|
|
96
|
+
if (series.length > 1) {
|
|
97
|
+
const legend = part("legend");
|
|
98
|
+
series.forEach(({ label }, index) => {
|
|
99
|
+
const key = part("key");
|
|
100
|
+
const swatch = part("swatch");
|
|
101
|
+
swatch.style.background = color(index);
|
|
102
|
+
key.append(swatch, label);
|
|
103
|
+
legend.append(key);
|
|
104
|
+
});
|
|
105
|
+
drawn.append(legend);
|
|
106
|
+
}
|
|
107
|
+
for (const row of rows) {
|
|
108
|
+
const line = part("row");
|
|
109
|
+
const lanes = part("lanes");
|
|
110
|
+
series.forEach(({ column }, index) => {
|
|
111
|
+
const lane = part("lane");
|
|
112
|
+
const value = numberOf(row[column] ?? "");
|
|
113
|
+
if (value !== null) {
|
|
114
|
+
const from = Math.min(zero, at(value));
|
|
115
|
+
const to = Math.max(zero, at(value));
|
|
116
|
+
const bar = part("bar");
|
|
117
|
+
bar.style.left = `${from}%`;
|
|
118
|
+
bar.style.width = `max(1px, ${to - from}%)`;
|
|
119
|
+
bar.style.background = color(index);
|
|
120
|
+
const said = part("value", row[column]);
|
|
121
|
+
if (value < 0) said.style.right = `calc(${100 - from}% + 4px)`;
|
|
122
|
+
else said.style.left = `calc(${to}% + 4px)`;
|
|
123
|
+
lane.append(bar, said);
|
|
124
|
+
}
|
|
125
|
+
lanes.append(lane);
|
|
126
|
+
});
|
|
127
|
+
line.append(part("label", row[0] ?? ""), lanes);
|
|
128
|
+
drawn.append(line);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 120"><rect width="240" height="120" rx="8" fill="#f2f4f7"/><rect x="16" y="14" width="208" height="92" rx="8" fill="#ffffff" stroke="#d0d5dd"/><rect x="30" y="26" width="40" height="8" rx="3" fill="#98a2b3"/><rect x="78" y="26" width="120" height="8" rx="3" fill="#2678d8"/><rect x="30" y="48" width="40" height="8" rx="3" fill="#98a2b3"/><rect x="78" y="48" width="84" height="8" rx="3" fill="#2678d8"/><rect x="30" y="70" width="40" height="8" rx="3" fill="#98a2b3"/><rect x="78" y="70" width="136" height="8" rx="3" fill="#2678d8"/><rect x="78" y="90" width="136" height="2" fill="#d0d5dd"/></svg>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Every selector starts at the instance and goes down a step at a time (>), so that nothing here
|
|
2
|
+
reaches the rest of the page or the table in the slot, which reads as the page's. The frame is
|
|
3
|
+
gallery/card's; the bars the script draws take the kit's series colors, which follow the theme, and
|
|
4
|
+
are laid out in percents of the card's width. */
|
|
5
|
+
[data-doc-component="gallery/chart-card"] {
|
|
6
|
+
margin: var(--doc-component-gap) 0;
|
|
7
|
+
padding: 20px 24px 8px;
|
|
8
|
+
border: 1px solid var(--doc-border);
|
|
9
|
+
border-radius: 10px;
|
|
10
|
+
background: var(--doc-surface);
|
|
11
|
+
> h3 { margin: 0 0 12px; font-size: var(--doc-card-title-font-size); line-height: var(--doc-card-title-line-height); font-weight: 600; letter-spacing: -.025em; }
|
|
12
|
+
> .gallery-chart-card-bars {
|
|
13
|
+
display: grid;
|
|
14
|
+
grid-template-columns: fit-content(40%) minmax(0, 1fr);
|
|
15
|
+
gap: 8px 12px;
|
|
16
|
+
align-items: center;
|
|
17
|
+
margin: 0 0 12px;
|
|
18
|
+
font-size: 13px;
|
|
19
|
+
line-height: 1.4;
|
|
20
|
+
color: var(--doc-text);
|
|
21
|
+
> .gallery-chart-card-legend {
|
|
22
|
+
grid-column: 1 / -1;
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-wrap: wrap;
|
|
25
|
+
gap: 4px 16px;
|
|
26
|
+
font-size: 12px;
|
|
27
|
+
color: var(--doc-muted);
|
|
28
|
+
> .gallery-chart-card-key { display: inline-flex; align-items: center; gap: 6px; }
|
|
29
|
+
> .gallery-chart-card-key > .gallery-chart-card-swatch { width: 10px; height: 10px; border-radius: 2px; }
|
|
30
|
+
}
|
|
31
|
+
> .gallery-chart-card-row { display: contents; }
|
|
32
|
+
> .gallery-chart-card-row > .gallery-chart-card-label { text-align: right; overflow-wrap: anywhere; }
|
|
33
|
+
> .gallery-chart-card-row > .gallery-chart-card-lanes { display: flex; flex-direction: column; gap: 2px; margin-right: 4em; }
|
|
34
|
+
> .gallery-chart-card-row > .gallery-chart-card-lanes > .gallery-chart-card-lane { position: relative; height: 18px; }
|
|
35
|
+
&.gallery-chart-card-many > .gallery-chart-card-row > .gallery-chart-card-lanes > .gallery-chart-card-lane { height: 13px; }
|
|
36
|
+
&.gallery-chart-card-below > .gallery-chart-card-row > .gallery-chart-card-lanes { margin-left: 4em; }
|
|
37
|
+
&.gallery-chart-card-below > .gallery-chart-card-row > .gallery-chart-card-lanes > .gallery-chart-card-lane { background: linear-gradient(var(--doc-grid), var(--doc-grid)) var(--gallery-chart-card-zero) 0 / 1px 100% no-repeat; }
|
|
38
|
+
> .gallery-chart-card-row > .gallery-chart-card-lanes > .gallery-chart-card-lane > .gallery-chart-card-bar { position: absolute; top: 0; bottom: 0; border-radius: 3px; }
|
|
39
|
+
> .gallery-chart-card-row > .gallery-chart-card-lanes > .gallery-chart-card-lane > .gallery-chart-card-value { position: absolute; top: 50%; transform: translateY(-50%); font-size: 12px; line-height: 1; color: var(--doc-muted); white-space: nowrap; }
|
|
40
|
+
> .gallery-chart-card-empty { grid-column: 1 / -1; margin: 0; font-size: 13px; color: var(--doc-muted); }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div data-doc-component="gallery/chart-card">
|
|
2
|
+
<h3>图表的标题</h3>
|
|
3
|
+
<div data-doc-slot="table">
|
|
4
|
+
<table>
|
|
5
|
+
<thead><tr><th>项目</th><th>数值</th></tr></thead>
|
|
6
|
+
<tbody>
|
|
7
|
+
<tr><td>项目 A</td><td>12</td></tr>
|
|
8
|
+
<tr><td>项目 B</td><td>18</td></tr>
|
|
9
|
+
<tr><td>项目 C</td><td>9</td></tr>
|
|
10
|
+
</tbody>
|
|
11
|
+
</table>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 表格图表卡片 (gallery/chart-card)
|
|
2
|
+
|
|
3
|
+
A card with a title, a table, and the bar chart the block draws from that table, with no script of the document's. Use it for a comparison of a few numbers by name (channels, teams, months) that the reader may change; for any other chart, write a chart of the document's own (references/writing-html.md, "Figures and charts").
|
|
4
|
+
|
|
5
|
+
## How to write it
|
|
6
|
+
|
|
7
|
+
Write the element as example.html does (`gallery block show gallery/chart-card` prints it):
|
|
8
|
+
|
|
9
|
+
- the title, its `h3`: what the numbers are, with their unit.
|
|
10
|
+
- the table, in its `<div data-doc-slot="table">`: a plain table of the document's, with no `data-doc-chart` and no `<caption>` (the card's title is its title):
|
|
11
|
+
- its header row: the names of the columns;
|
|
12
|
+
- the first column: the name of each bar;
|
|
13
|
+
- each column after it that holds numbers: a series, its header the legend's words (two or more draw their bars one under another, a legend above them);
|
|
14
|
+
- a cell with no number (empty, a dash, `n/a`): no bar. A number may have thousands separators and a `%`.
|
|
15
|
+
|
|
16
|
+
The reader edits the table as any table of the document's, and the bars draw again.
|
|
17
|
+
|
|
18
|
+
## How it looks
|
|
19
|
+
|
|
20
|
+
A framed card: the title, horizontal bars from zero with each value at its end, in the kit's series colors, which follow the theme, then the table. The bars are drawn by the block's script and are not in the file: where the block is not, and in a Markdown export, a reader has the title and the table.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "gallery/chart-view",
|
|
3
|
+
"apiVersion": 1,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"name": { "zh": "图表", "en": "Chart" },
|
|
6
|
+
"description": { "zh": "表格的视图:按一列分组的计数、合计或平均,或每一行的数,画成条形图,表格一改就重画", "en": "A view of a table: a count, sum or average by a column, or each row's number, drawn as bars that follow each edit of the table" },
|
|
7
|
+
"words": "tubiao tiaoxingtu chart bar bars graph 图表 条形图 柱状图 统计 视图 view",
|
|
8
|
+
"parts": {
|
|
9
|
+
"settings": { "at": ":scope > script", "edit": "data" }
|
|
10
|
+
},
|
|
11
|
+
"runtime": { "generated": [":scope > .gallery-chart-view-face"] },
|
|
12
|
+
"places": ["document"],
|
|
13
|
+
"settings": [
|
|
14
|
+
{ "key": "by", "label": { "zh": "分组依据", "en": "Group by" }, "type": "text", "default": "" },
|
|
15
|
+
{ "key": "op", "label": { "zh": "计算", "en": "Calculate" }, "type": "select", "options": ["count", "sum", "average"], "default": "count" },
|
|
16
|
+
{ "key": "value", "label": { "zh": "数值列", "en": "Numbers" }, "type": "text", "default": "" },
|
|
17
|
+
{ "key": "title", "label": { "zh": "标题", "en": "Title" }, "type": "text", "default": "" }
|
|
18
|
+
],
|
|
19
|
+
"needs": ["tables"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<table data-doc-chart-source="sales">
|
|
2
|
+
<thead><tr><th>客户</th><th>区域</th><th>金额</th><th>状态</th></tr></thead>
|
|
3
|
+
<tbody>
|
|
4
|
+
<tr><td>华东物流</td><td>华东</td><td>12,800</td><td>已成交</td></tr>
|
|
5
|
+
<tr><td>北方食品</td><td>华北</td><td>8,600</td><td>洽谈中</td></tr>
|
|
6
|
+
<tr><td>南方家居</td><td>华南</td><td>15,200</td><td>已成交</td></tr>
|
|
7
|
+
<tr><td>东海电子</td><td>华东</td><td>9,300</td><td>洽谈中</td></tr>
|
|
8
|
+
<tr><td>西部能源</td><td>西部</td><td>6,400</td><td>已成交</td></tr>
|
|
9
|
+
</tbody>
|
|
10
|
+
</table>
|
|
11
|
+
<div data-doc-component="gallery/chart-view" data-doc-from="sales">
|
|
12
|
+
<script type="application/json">{"by":"区域","op":"sum","value":"金额","title":"各区域的金额(元)"}</script>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// gallery/chart-view (图表): a view of a table of the document (its manifest's needs hold "tables"; the
|
|
2
|
+
// table's frame carries the key its data-doc-from names, which the table's 视图 › puts on both), drawn as
|
|
3
|
+
// horizontal bars from zero, each with its value at its end, under its title. Its settings (its data part
|
|
4
|
+
// `settings`, JSON) say what: `by` a column whose values are the bars (the rows grouped by it, in the order
|
|
5
|
+
// the rows first say them), each the `op` of its rows, "count" (how many), "sum" or "average" of the
|
|
6
|
+
// numbers of the column `value`; with no `by` a bar for each row, its first cell its name and its number
|
|
7
|
+
// in `value`. Left out: `by` the column that reads as a status or whose words repeat (none: a bar a row),
|
|
8
|
+
// `value` the first column of numbers, `op` "sum" where there is one, else "count"; `title` one made of
|
|
9
|
+
// the rest. At most MAX_BARS bars, a line saying how many more.
|
|
10
|
+
//
|
|
11
|
+
// It reads the table each time its rows change (the `data` signal), and draws only what changed: a bar
|
|
12
|
+
// is kept by its group's value (or its row's element) and its label, its value and its length written
|
|
13
|
+
// only where they changed, so that typing in a table of 500 rows moves one bar. Its ⚙, at its head while
|
|
14
|
+
// the page edits, opens its settings, each change one undo step of the document's (context.editJSON); it
|
|
15
|
+
// writes nothing else. The colors are the kit's series colors, which follow the theme. The words are the
|
|
16
|
+
// page's language.
|
|
17
|
+
|
|
18
|
+
const WORDS = {
|
|
19
|
+
zh: {
|
|
20
|
+
settings: "设置",
|
|
21
|
+
by: "分组依据",
|
|
22
|
+
each: "每行一条",
|
|
23
|
+
op: "计算",
|
|
24
|
+
ops: { count: "计数", sum: "合计", average: "平均" },
|
|
25
|
+
value: "数值列",
|
|
26
|
+
title: "标题",
|
|
27
|
+
none: "(空)",
|
|
28
|
+
rows: (/** @type {string} */ by) => `各${by}的行数`,
|
|
29
|
+
sum: (/** @type {string} */ by, /** @type {string} */ value) => `各${by}的${value}合计`,
|
|
30
|
+
average: (/** @type {string} */ by, /** @type {string} */ value) => `各${by}的${value}平均`,
|
|
31
|
+
each_: (/** @type {string} */ name, /** @type {string} */ value) => `各${name}的${value}`,
|
|
32
|
+
more: (/** @type {number} */ shown, /** @type {number} */ all) => `只画了前 ${shown} 条,共 ${all} 条:按一列分组可以都画出来。`,
|
|
33
|
+
unbound: "这张图还没有接到表格上:在表格的菜单里选“视图 › 图表”。",
|
|
34
|
+
unread: "读不到这张图的表格:它不在页面上了,或者有合并的单元格。",
|
|
35
|
+
missing: (/** @type {string} */ name) => `找不到列“${name}”:表格的列名改过了吗?`,
|
|
36
|
+
noNumbers: "表格里没有写数的列:每行一条、合计和平均要一列数。",
|
|
37
|
+
empty: "表格里还没有行。",
|
|
38
|
+
},
|
|
39
|
+
en: {
|
|
40
|
+
settings: "Settings",
|
|
41
|
+
by: "Group by",
|
|
42
|
+
each: "A bar a row",
|
|
43
|
+
op: "Calculate",
|
|
44
|
+
ops: { count: "Count", sum: "Sum", average: "Average" },
|
|
45
|
+
value: "Numbers",
|
|
46
|
+
title: "Title",
|
|
47
|
+
none: "(empty)",
|
|
48
|
+
rows: (/** @type {string} */ by) => `Rows by ${by}`,
|
|
49
|
+
sum: (/** @type {string} */ by, /** @type {string} */ value) => `Total ${value} by ${by}`,
|
|
50
|
+
average: (/** @type {string} */ by, /** @type {string} */ value) => `Average ${value} by ${by}`,
|
|
51
|
+
each_: (/** @type {string} */ name, /** @type {string} */ value) => `${value} by ${name}`,
|
|
52
|
+
more: (/** @type {number} */ shown, /** @type {number} */ all) => `The first ${shown} bars of ${all}: group by a column to draw them all.`,
|
|
53
|
+
unbound: "This chart reads no table yet: pick View › Chart in a table's menu.",
|
|
54
|
+
unread: "This chart's table cannot be read: it is not on the page, or it has merged cells.",
|
|
55
|
+
missing: (/** @type {string} */ name) => `No column “${name}”: was the table's column renamed?`,
|
|
56
|
+
noNumbers: "The table has no column of numbers: a bar a row, a sum and an average need one.",
|
|
57
|
+
empty: "The table has no rows yet.",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// The most bars it draws.
|
|
62
|
+
const MAX_BARS = 50;
|
|
63
|
+
|
|
64
|
+
// A column whose name says it is a status, the one it groups by when its settings name none.
|
|
65
|
+
const STATUS = /状态|進度|进度|阶段|階段|状況|状况|status|stage|state|phase/i;
|
|
66
|
+
|
|
67
|
+
const GEAR = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 4.5h6M12 4.5h1M3 11.5h1M7 11.5h6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><circle cx="10.5" cy="4.5" r="1.5" fill="none" stroke="currentColor" stroke-width="1.3"/><circle cx="5.5" cy="11.5" r="1.5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg>';
|
|
68
|
+
|
|
69
|
+
/** The language the page speaks: the UI's where Gallery says it (data-ge-language), else its lang's. */
|
|
70
|
+
function languageOf(/** @type {HTMLElement} */ root) {
|
|
71
|
+
const ui = root.getAttribute("data-ge-language");
|
|
72
|
+
return (ui !== null ? ui === "en" : /^en(?:-|$)/i.test(root.lang)) ? "en" : "zh";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Words as a cell shows them, its white space one space. */
|
|
76
|
+
const wordsOf = (/** @type {Node | null | undefined} */ node) => (node?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
77
|
+
|
|
78
|
+
/** A row's value of a column as a group: its words ("" for none). */
|
|
79
|
+
const keyOf = (/** @type {DocRow} */ row, /** @type {string} */ column) => {
|
|
80
|
+
const value = row.values[column];
|
|
81
|
+
return value === null || value === undefined ? "" : String(value).trim();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* What its settings ask (context.readJSON's; {} where none is set): "" where a setting is left out.
|
|
86
|
+
* @param {any} settings
|
|
87
|
+
* @returns {{ by: string, op: "" | "count" | "sum" | "average", value: string, title: string }}
|
|
88
|
+
*/
|
|
89
|
+
function askedOf(settings) {
|
|
90
|
+
const table = settings !== null && typeof settings === "object" && !Array.isArray(settings) ? settings : {};
|
|
91
|
+
const text = (/** @type {string} */ key) => (typeof table[key] === "string" ? table[key].trim() : "");
|
|
92
|
+
const op = table.op === "count" || table.op === "sum" || table.op === "average" ? table.op : "";
|
|
93
|
+
return { by: text("by"), op, value: text("value"), title: text("title") };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The columns of `table` holding numbers: a number in a cell at least, and no words. */
|
|
97
|
+
const numberColumns = (/** @type {DocTableView} */ table) => table.columns.filter((name) => table.rows.some((row) => typeof row.values[name] === "number") && table.rows.every((row) => row.values[name] === null || typeof row.values[name] === "number"));
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The column it groups by where its settings name none: one after the first whose name says it is a
|
|
101
|
+
* status, else the one after the first holding words whose values repeat most (fewer than its rows),
|
|
102
|
+
* else none: a bar a row.
|
|
103
|
+
* @param {DocTableView} table
|
|
104
|
+
*/
|
|
105
|
+
function groupColumnOf(table) {
|
|
106
|
+
const numbers = numberColumns(table);
|
|
107
|
+
const rest = table.columns.slice(1).filter((name) => !numbers.includes(name));
|
|
108
|
+
const named = rest.find((name) => STATUS.test(name));
|
|
109
|
+
if (named) return named;
|
|
110
|
+
/** @type {[string, number] | null} */
|
|
111
|
+
let best = null;
|
|
112
|
+
for (const name of rest) {
|
|
113
|
+
const kinds = new Set(table.rows.map((row) => keyOf(row, name))).size;
|
|
114
|
+
if (kinds < table.rows.length && (best === null || kinds < best[1])) best = [name, kinds];
|
|
115
|
+
}
|
|
116
|
+
return best ? best[0] : "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The chart `asked` says of `table`: { title, bars: [{ key, label, value }], all } or { line }.
|
|
121
|
+
* @param {DocTableView} table
|
|
122
|
+
* @param {ReturnType<typeof askedOf>} asked
|
|
123
|
+
* @param {typeof WORDS.zh} words
|
|
124
|
+
*/
|
|
125
|
+
function chartOf(table, asked, words) {
|
|
126
|
+
if (table.rows.length === 0) return { line: words.empty };
|
|
127
|
+
const by = asked.by || groupColumnOf(table);
|
|
128
|
+
if (by && !table.columns.includes(by)) return { line: words.missing(by) };
|
|
129
|
+
const value = asked.value || numberColumns(table)[0] || "";
|
|
130
|
+
const op = asked.op || (value ? "sum" : "count");
|
|
131
|
+
if (value && !table.columns.includes(value)) return { line: words.missing(value) };
|
|
132
|
+
if (!value && (!by || op !== "count")) return { line: words.noNumbers };
|
|
133
|
+
/** @type {{ key: unknown, label: string, value: number | null }[]} */
|
|
134
|
+
let bars;
|
|
135
|
+
let title;
|
|
136
|
+
if (by) {
|
|
137
|
+
/** @type {Map<string, { count: number, sum: number, numbers: number }>} */
|
|
138
|
+
const groups = new Map();
|
|
139
|
+
for (const row of table.rows) {
|
|
140
|
+
const group = keyOf(row, by);
|
|
141
|
+
const kept = groups.get(group) ?? { count: 0, sum: 0, numbers: 0 };
|
|
142
|
+
kept.count += 1;
|
|
143
|
+
const number = value ? row.values[value] : null;
|
|
144
|
+
if (typeof number === "number") {
|
|
145
|
+
kept.sum += number;
|
|
146
|
+
kept.numbers += 1;
|
|
147
|
+
}
|
|
148
|
+
groups.set(group, kept);
|
|
149
|
+
}
|
|
150
|
+
bars = [...groups].map(([group, { count, sum, numbers }]) => ({ key: group, label: group || words.none, value: op === "count" ? count : op === "sum" ? sum : numbers > 0 ? sum / numbers : null }));
|
|
151
|
+
title = op === "count" ? words.rows(by) : op === "sum" ? words.sum(by, value) : words.average(by, value);
|
|
152
|
+
} else {
|
|
153
|
+
bars = table.rows.map((row) => {
|
|
154
|
+
const number = row.values[value];
|
|
155
|
+
return { key: row.el, label: wordsOf(row.el.cells[0]), value: typeof number === "number" ? number : null };
|
|
156
|
+
});
|
|
157
|
+
title = words.each_(table.columns[0] ?? "", value);
|
|
158
|
+
}
|
|
159
|
+
return { title: asked.title || title, bars: bars.slice(0, MAX_BARS), all: bars.length, by, op, value };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* An element of the drawing, of `kind` (its class gallery-chart-view-<kind>).
|
|
164
|
+
* @template {keyof HTMLElementTagNameMap} K
|
|
165
|
+
* @param {K} tag
|
|
166
|
+
* @param {string} kind
|
|
167
|
+
* @param {string} [text]
|
|
168
|
+
*/
|
|
169
|
+
function made(tag, kind, text) {
|
|
170
|
+
const node = document.createElement(tag);
|
|
171
|
+
node.className = `gallery-chart-view-${kind}`;
|
|
172
|
+
if (text !== undefined) node.textContent = text;
|
|
173
|
+
return node;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* `parent`'s children made `nodes`, what is there already left in place: where only some are new, each
|
|
178
|
+
* standing where the one it replaces stood, those alone are put in; anything else is the list anew.
|
|
179
|
+
* @param {Element} parent
|
|
180
|
+
* @param {Element[]} nodes
|
|
181
|
+
*/
|
|
182
|
+
function put(parent, nodes) {
|
|
183
|
+
const now = [...parent.children];
|
|
184
|
+
if (now.length === nodes.length) {
|
|
185
|
+
const differs = nodes.map((node, at) => node !== now[at]);
|
|
186
|
+
if (!differs.includes(true)) return;
|
|
187
|
+
if (nodes.every((node, at) => !differs[at] || !node.isConnected)) {
|
|
188
|
+
nodes.forEach((node, at) => { if (differs[at]) parent.replaceChild(node, now[at]); });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
parent.replaceChildren(...nodes);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The settings strip: a field a setting, a select of its choices (`options()`, [value, words]) or a
|
|
197
|
+
* text, shown while `shown()` says so, each committed (DocKit.input: Enter, the field left, a choice
|
|
198
|
+
* made) as one write of the settings part, a key set or, for "", taken off.
|
|
199
|
+
* @param {DocComponentContext} context
|
|
200
|
+
* @param {{ key: string, label: string, options?: () => [string, string][], shown?: () => boolean, value: () => string }[]} fields
|
|
201
|
+
*/
|
|
202
|
+
function settingsStrip(context, fields) {
|
|
203
|
+
const strip = made("div", "strip");
|
|
204
|
+
strip.hidden = true;
|
|
205
|
+
const write = (/** @type {string} */ key, /** @type {string} */ value) => context.editJSON("settings", (draft) => {
|
|
206
|
+
const settings = draft !== null && typeof draft === "object" && !Array.isArray(draft) ? draft : {};
|
|
207
|
+
if (value.trim() === "") delete settings[key];
|
|
208
|
+
else settings[key] = value.trim();
|
|
209
|
+
return settings;
|
|
210
|
+
});
|
|
211
|
+
const controls = fields.map((field) => {
|
|
212
|
+
const label = made("label", "setting");
|
|
213
|
+
const control = field.options ? document.createElement("select") : document.createElement("input");
|
|
214
|
+
if (control instanceof HTMLInputElement) control.type = "text";
|
|
215
|
+
label.append(made("span", "setting-name", field.label), control);
|
|
216
|
+
strip.append(label);
|
|
217
|
+
const handle = DocKit.input(control, { commit: (value) => void write(field.key, value), signal: context.signal });
|
|
218
|
+
return { field, label, control, handle, signed: "" };
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
strip,
|
|
222
|
+
/** The fields as the settings and the table say now: their choices, which show, their values. */
|
|
223
|
+
update() {
|
|
224
|
+
for (const each of controls) {
|
|
225
|
+
each.label.hidden = each.field.shown ? !each.field.shown() : false;
|
|
226
|
+
const { control, field } = each;
|
|
227
|
+
if (field.options && control instanceof HTMLSelectElement) {
|
|
228
|
+
const options = field.options();
|
|
229
|
+
const signed = JSON.stringify(options);
|
|
230
|
+
if (signed !== each.signed) {
|
|
231
|
+
each.signed = signed;
|
|
232
|
+
control.replaceChildren(...options.map(([value, words]) => new Option(words, value)));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
each.handle.set(field.value());
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
DocKit.component({
|
|
242
|
+
id: "gallery/chart-view",
|
|
243
|
+
on: ["source", "data", "editable"],
|
|
244
|
+
mount(host, context) {
|
|
245
|
+
const language = languageOf(document.documentElement);
|
|
246
|
+
const words = WORDS[language];
|
|
247
|
+
const format = new Intl.NumberFormat(language === "en" ? "en-US" : "zh-CN", { maximumFractionDigits: 2 });
|
|
248
|
+
const face = made("div", "face");
|
|
249
|
+
const head = made("div", "head");
|
|
250
|
+
const title = made("span", "title");
|
|
251
|
+
const gear = made("button", "gear");
|
|
252
|
+
gear.type = "button";
|
|
253
|
+
gear.title = words.settings;
|
|
254
|
+
gear.setAttribute("aria-label", words.settings);
|
|
255
|
+
gear.setAttribute("aria-expanded", "false");
|
|
256
|
+
gear.innerHTML = GEAR;
|
|
257
|
+
head.append(title, gear);
|
|
258
|
+
const bars = made("div", "bars");
|
|
259
|
+
bars.setAttribute("role", "img");
|
|
260
|
+
const line = made("p", "line");
|
|
261
|
+
|
|
262
|
+
/** @type {DocTableView | null} */
|
|
263
|
+
let table = null;
|
|
264
|
+
let asked = askedOf({});
|
|
265
|
+
/** @type {{ by: string, op: string, value: string } | null} */
|
|
266
|
+
let drawn = null; // what it drew last: its column, its op, its numbers
|
|
267
|
+
const pairs = (/** @type {string[]} */ names) => names.map((name) => /** @type {[string, string]} */ ([name, name]));
|
|
268
|
+
const { strip, update: updateStrip } = settingsStrip(context, [
|
|
269
|
+
{ key: "by", label: words.by, options: () => [["", words.each], ...pairs(table ? table.columns.slice(1) : [])], value: () => drawn ? drawn.by : asked.by },
|
|
270
|
+
{ key: "op", label: words.op, options: () => [["count", words.ops.count], ["sum", words.ops.sum], ["average", words.ops.average]], shown: () => Boolean(drawn && drawn.by), value: () => drawn ? drawn.op : asked.op },
|
|
271
|
+
{ key: "value", label: words.value, options: () => pairs(table ? numberColumns(table) : []), shown: () => !drawn || !drawn.by || drawn.op !== "count", value: () => drawn ? drawn.value : asked.value },
|
|
272
|
+
{ key: "title", label: words.title, value: () => asked.title },
|
|
273
|
+
]);
|
|
274
|
+
face.append(head, strip, bars, line);
|
|
275
|
+
host.append(face);
|
|
276
|
+
gear.addEventListener("click", () => {
|
|
277
|
+
strip.hidden = !strip.hidden;
|
|
278
|
+
gear.setAttribute("aria-expanded", String(!strip.hidden));
|
|
279
|
+
}, { signal: context.signal });
|
|
280
|
+
|
|
281
|
+
/** @type {Map<unknown, { line: HTMLElement, label: HTMLElement, bar: HTMLElement, value: HTMLElement, said: string, at: string }>} */
|
|
282
|
+
const lines = new Map(); // a bar's key (its group's value, or its row's element) -> what draws it
|
|
283
|
+
const say = (/** @type {string | null} */ text) => {
|
|
284
|
+
line.hidden = text === null;
|
|
285
|
+
if (line.textContent !== (text ?? "")) line.textContent = text ?? "";
|
|
286
|
+
};
|
|
287
|
+
const drawNow = () => {
|
|
288
|
+
table = context.table();
|
|
289
|
+
asked = askedOf(context.readJSON("settings"));
|
|
290
|
+
face.toggleAttribute("data-writes", context.canEdit());
|
|
291
|
+
head.hidden = !table;
|
|
292
|
+
const chart = table ? chartOf(table, asked, words) : { line: host.hasAttribute("data-doc-from") ? words.unread : words.unbound };
|
|
293
|
+
if (chart.line !== undefined) {
|
|
294
|
+
drawn = null;
|
|
295
|
+
updateStrip();
|
|
296
|
+
title.textContent = asked.title;
|
|
297
|
+
head.classList.toggle("gallery-chart-view-untitled", !asked.title);
|
|
298
|
+
lines.clear();
|
|
299
|
+
bars.replaceChildren();
|
|
300
|
+
bars.hidden = true;
|
|
301
|
+
return say(chart.line);
|
|
302
|
+
}
|
|
303
|
+
drawn = { by: chart.by, op: chart.by ? chart.op : "", value: chart.value };
|
|
304
|
+
updateStrip();
|
|
305
|
+
head.classList.remove("gallery-chart-view-untitled");
|
|
306
|
+
if (title.textContent !== chart.title) title.textContent = chart.title;
|
|
307
|
+
bars.setAttribute("aria-label", chart.title);
|
|
308
|
+
bars.hidden = false;
|
|
309
|
+
say(chart.all > chart.bars.length ? words.more(chart.bars.length, chart.all) : null);
|
|
310
|
+
const values = /** @type {number[]} */ (chart.bars.map(({ value }) => value).filter((value) => value !== null));
|
|
311
|
+
const low = Math.min(0, ...values);
|
|
312
|
+
const span = Math.max(0, ...values) - low || 1;
|
|
313
|
+
const at = (/** @type {number} */ value) => ((value - low) / span) * 100;
|
|
314
|
+
const zero = at(0);
|
|
315
|
+
bars.classList.toggle("gallery-chart-view-below", low < 0);
|
|
316
|
+
bars.style.setProperty("--gallery-chart-view-zero", `${zero}%`);
|
|
317
|
+
const keys = new Set(chart.bars.map(({ key }) => key));
|
|
318
|
+
for (const key of [...lines.keys()]) if (!keys.has(key)) lines.delete(key);
|
|
319
|
+
put(bars, chart.bars.map(({ key, label, value }) => {
|
|
320
|
+
let kept = lines.get(key);
|
|
321
|
+
if (!kept) {
|
|
322
|
+
const row = made("div", "row");
|
|
323
|
+
const name = made("span", "label");
|
|
324
|
+
const lane = made("span", "lane");
|
|
325
|
+
const bar = made("span", "bar");
|
|
326
|
+
const said = made("span", "value");
|
|
327
|
+
lane.append(bar, said);
|
|
328
|
+
row.append(name, lane);
|
|
329
|
+
kept = { line: row, label: name, bar, value: said, said: "", at: "" };
|
|
330
|
+
lines.set(key, kept);
|
|
331
|
+
}
|
|
332
|
+
if (kept.label.textContent !== label) kept.label.textContent = label;
|
|
333
|
+
const text = value === null ? "" : format.format(value);
|
|
334
|
+
const from = value === null ? zero : Math.min(zero, at(value));
|
|
335
|
+
const to = value === null ? zero : Math.max(zero, at(value));
|
|
336
|
+
const place = `${from} ${to} ${value !== null && value < 0}`;
|
|
337
|
+
if (kept.said !== text) {
|
|
338
|
+
kept.said = text;
|
|
339
|
+
kept.value.textContent = text;
|
|
340
|
+
}
|
|
341
|
+
if (kept.at !== place) {
|
|
342
|
+
kept.at = place;
|
|
343
|
+
kept.bar.hidden = value === null;
|
|
344
|
+
kept.bar.style.left = `${from}%`;
|
|
345
|
+
kept.bar.style.width = `${to - from}%`;
|
|
346
|
+
kept.value.style.left = value !== null && value < 0 ? "" : `calc(${to}% + 4px)`;
|
|
347
|
+
kept.value.style.right = value !== null && value < 0 ? `calc(${100 - from}% + 4px)` : "";
|
|
348
|
+
}
|
|
349
|
+
return kept.line;
|
|
350
|
+
}));
|
|
351
|
+
};
|
|
352
|
+
drawNow();
|
|
353
|
+
return { update: () => context.frame(drawNow) };
|
|
354
|
+
},
|
|
355
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 120"><rect width="240" height="120" rx="8" fill="#f2f4f7"/><rect x="16" y="12" width="208" height="96" rx="8" fill="#ffffff" stroke="#d0d5dd"/><rect x="28" y="22" width="70" height="7" rx="3.5" fill="#475467"/><rect x="28" y="42" width="28" height="7" rx="3" fill="#98a2b3"/><rect x="64" y="40" width="110" height="11" rx="3" fill="#2678d8"/><rect x="28" y="60" width="28" height="7" rx="3" fill="#98a2b3"/><rect x="64" y="58" width="72" height="11" rx="3" fill="#2678d8"/><rect x="28" y="78" width="28" height="7" rx="3" fill="#98a2b3"/><rect x="64" y="76" width="140" height="11" rx="3" fill="#2678d8"/><rect x="180" y="43" width="16" height="5" rx="2" fill="#d0d5dd"/><rect x="142" y="61" width="16" height="5" rx="2" fill="#d0d5dd"/></svg>
|