@cardenelabs/cdl 0.7.0 → 0.10.0
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/CHANGELOG.md +446 -0
- package/SPEC.md +42 -18
- package/dist/index.cjs +793 -347
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -14
- package/dist/index.d.ts +73 -14
- package/dist/index.js +793 -347
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +658 -236
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +658 -236
- package/dist/react.js.map +1 -1
- package/dist/{render-DquCvgOB.d.cts → render-CH1Qn3Jv.d.cts} +47 -2
- package/dist/{render-DquCvgOB.d.ts → render-CH1Qn3Jv.d.ts} +47 -2
- package/package.json +6 -3
- package/src/builder.ts +13 -1
- package/src/kinds/box-lines.ts +120 -0
- package/src/kinds/chart-line.tsx +94 -25
- package/src/kinds/gantt.tsx +152 -24
- package/src/kinds/mind-map.tsx +50 -12
- package/src/kinds/quadrant.tsx +28 -2
- package/src/kinds/shape-blockchain.tsx +65 -12
- package/src/kinds/shape-region.tsx +152 -34
- package/src/kinds/tree.tsx +92 -20
- package/src/layout/collisions.ts +10 -2
- package/src/layout/footer-shape.ts +86 -24
- package/src/layout/lifeline.ts +80 -0
- package/src/layout/spec.ts +21 -2
- package/src/layout/text-width.ts +92 -4
- package/src/layout/viewbox.ts +0 -8
- package/src/layout.ts +8 -1
- package/src/presets.ts +120 -22
- package/src/render/header.tsx +3 -1
- package/src/render/nodes.tsx +15 -1
- package/src/render/payload-binding.ts +31 -8
- package/src/render/stage.tsx +21 -34
- package/src/render/template-fields.ts +212 -0
- package/src/render/utils.ts +32 -2
- package/src/render.tsx +2 -9
- package/src/types.ts +47 -2
- package/src/validate.ts +45 -13
- package/src/visual-validate.ts +34 -1
- package/src/layout/label-shift.ts +0 -28
|
@@ -148,6 +148,17 @@ type CdlNode = {
|
|
|
148
148
|
*/
|
|
149
149
|
posW?: number;
|
|
150
150
|
posH?: number;
|
|
151
|
+
/**
|
|
152
|
+
* 図の中での役割。 位置ではなく **組み立てた側が付ける目印** で、 描画側の分岐に使う。
|
|
153
|
+
*
|
|
154
|
+
* `lifeline-footer` = 順序図で lane の末尾に置く下端の名札 (`presets.ts` の `build()` が付ける)。
|
|
155
|
+
* 一部の `shape-` は絵を箱の下端から上へ積むため、 箱が低いと絵が縦線と重なる。 その回避で
|
|
156
|
+
* 絵を下げる対象がこの名札に限られる。
|
|
157
|
+
*
|
|
158
|
+
* **位置で推測してはいけない** = 手で組んだ図で一番下が名札でない `shape-` の node だと、
|
|
159
|
+
* その絵が下がってしまう (`#458`)。
|
|
160
|
+
*/
|
|
161
|
+
role?: "lifeline-footer";
|
|
151
162
|
/**
|
|
152
163
|
* node の主色。 未指定なら kind ごとの既定色。
|
|
153
164
|
*
|
|
@@ -208,6 +219,19 @@ type CdlNode = {
|
|
|
208
219
|
* kinds/gantt.tsx が縦軸 task lane + 横軸日付 rect bar で SVG 描画する。
|
|
209
220
|
*/
|
|
210
221
|
ganttData?: GanttTaskPayload[];
|
|
222
|
+
/**
|
|
223
|
+
* gantt-timeline の横軸を何目盛り分にするか (`#483`)。
|
|
224
|
+
*
|
|
225
|
+
* 書かない図では今までどおり全 task の最大値から自動で決まる。 書くと尺がその値で固定され、
|
|
226
|
+
* 状態で動く task があっても他の task の帯の位置と幅が変わらない。
|
|
227
|
+
*
|
|
228
|
+
* 位置は 0 から `ganttAxisMax - 1` までで、 そこから外れる task の帯は右端の目盛りに寄せて
|
|
229
|
+
* 描く (図の外へ出さない、 `kinds/gantt.tsx` SSOT)。
|
|
230
|
+
*
|
|
231
|
+
* 有限の正の数でなければ書かなかったものとして扱う = 尺が壊れて帯が図の外へ出るより、
|
|
232
|
+
* 自動に落ちる方が読める図になる。
|
|
233
|
+
*/
|
|
234
|
+
ganttAxisMax?: number;
|
|
211
235
|
/**
|
|
212
236
|
* mind-map 専用の branch payload。 tree 構造を 1 node に格納し、
|
|
213
237
|
* kinds/mind-map.tsx が中心 + 放射で SVG 描画する。
|
|
@@ -315,13 +339,20 @@ type QuadrantItemPayload = {
|
|
|
315
339
|
quadrant: BoundEnum<QuadrantKey>;
|
|
316
340
|
subtitle?: string;
|
|
317
341
|
};
|
|
318
|
-
/**
|
|
342
|
+
/**
|
|
343
|
+
* tree-hierarchy kind の 1 node payload。
|
|
344
|
+
*
|
|
345
|
+
* **上の小ラベル (`eyebrow`) は受けない** (#471)。 箱の高さは段の間隔から出ており
|
|
346
|
+
* (`Math.min(38, 間隔 * 0.7)`)、 名前と補足で 2 行を使うと 3 行目は入らない。 高さを広げると
|
|
347
|
+
* 段どうしが近づいて箱が重なる。 受け取って描かない欄は、 書き手から効いていないことが
|
|
348
|
+
* 読めないため置かない。
|
|
349
|
+
*/
|
|
319
350
|
type TreeNodePayload = {
|
|
320
351
|
id: string;
|
|
321
352
|
title: string;
|
|
322
353
|
parent?: string;
|
|
354
|
+
/** 箱の 2 行目に出す補足 */
|
|
323
355
|
subtitle?: string;
|
|
324
|
-
eyebrow?: string;
|
|
325
356
|
};
|
|
326
357
|
/** journey-map の気持ち 5 段階 */
|
|
327
358
|
type JourneyEmotion = "delighted" | "happy" | "neutral" | "frustrated" | "angry";
|
|
@@ -2016,6 +2047,20 @@ type CdlPhase = {
|
|
|
2016
2047
|
title: string;
|
|
2017
2048
|
body: string;
|
|
2018
2049
|
activate: string[];
|
|
2050
|
+
/**
|
|
2051
|
+
* この phase で「左の起点から描き始める」 node の id (cdl#512)。
|
|
2052
|
+
*
|
|
2053
|
+
* `activate` と同じ形で node id を並べる。 対象の node は phase の進み (0→1) に合わせて
|
|
2054
|
+
* 描かれ、 進みが 1 に達した時点で全体が出る。 現在の対象は `chart-line` だけで、
|
|
2055
|
+
* 折れ線が左端から右へ伸び、 点と値の数字は線の先が届いた時に現れる。
|
|
2056
|
+
*
|
|
2057
|
+
* **`activate` を兼ねさせない**。 1 箱で図全体を描く種別 (`chart-*` 等) では焦点が
|
|
2058
|
+
* その箱に解決されるため、 連続する phase で光り続ける図が実在する。 「光っていたら
|
|
2059
|
+
* 描く」 にすると、 値が動くだけの phase でも線を引き直すことになる。
|
|
2060
|
+
*
|
|
2061
|
+
* 書かない phase では従来どおり全長で描く (DOM に dash 属性も付かない)。
|
|
2062
|
+
*/
|
|
2063
|
+
draw?: string[];
|
|
2019
2064
|
/** 数値 state の線形補間 (write phase で 100→90 等) */
|
|
2020
2065
|
tweens: Array<{
|
|
2021
2066
|
stateId: string;
|
|
@@ -148,6 +148,17 @@ type CdlNode = {
|
|
|
148
148
|
*/
|
|
149
149
|
posW?: number;
|
|
150
150
|
posH?: number;
|
|
151
|
+
/**
|
|
152
|
+
* 図の中での役割。 位置ではなく **組み立てた側が付ける目印** で、 描画側の分岐に使う。
|
|
153
|
+
*
|
|
154
|
+
* `lifeline-footer` = 順序図で lane の末尾に置く下端の名札 (`presets.ts` の `build()` が付ける)。
|
|
155
|
+
* 一部の `shape-` は絵を箱の下端から上へ積むため、 箱が低いと絵が縦線と重なる。 その回避で
|
|
156
|
+
* 絵を下げる対象がこの名札に限られる。
|
|
157
|
+
*
|
|
158
|
+
* **位置で推測してはいけない** = 手で組んだ図で一番下が名札でない `shape-` の node だと、
|
|
159
|
+
* その絵が下がってしまう (`#458`)。
|
|
160
|
+
*/
|
|
161
|
+
role?: "lifeline-footer";
|
|
151
162
|
/**
|
|
152
163
|
* node の主色。 未指定なら kind ごとの既定色。
|
|
153
164
|
*
|
|
@@ -208,6 +219,19 @@ type CdlNode = {
|
|
|
208
219
|
* kinds/gantt.tsx が縦軸 task lane + 横軸日付 rect bar で SVG 描画する。
|
|
209
220
|
*/
|
|
210
221
|
ganttData?: GanttTaskPayload[];
|
|
222
|
+
/**
|
|
223
|
+
* gantt-timeline の横軸を何目盛り分にするか (`#483`)。
|
|
224
|
+
*
|
|
225
|
+
* 書かない図では今までどおり全 task の最大値から自動で決まる。 書くと尺がその値で固定され、
|
|
226
|
+
* 状態で動く task があっても他の task の帯の位置と幅が変わらない。
|
|
227
|
+
*
|
|
228
|
+
* 位置は 0 から `ganttAxisMax - 1` までで、 そこから外れる task の帯は右端の目盛りに寄せて
|
|
229
|
+
* 描く (図の外へ出さない、 `kinds/gantt.tsx` SSOT)。
|
|
230
|
+
*
|
|
231
|
+
* 有限の正の数でなければ書かなかったものとして扱う = 尺が壊れて帯が図の外へ出るより、
|
|
232
|
+
* 自動に落ちる方が読める図になる。
|
|
233
|
+
*/
|
|
234
|
+
ganttAxisMax?: number;
|
|
211
235
|
/**
|
|
212
236
|
* mind-map 専用の branch payload。 tree 構造を 1 node に格納し、
|
|
213
237
|
* kinds/mind-map.tsx が中心 + 放射で SVG 描画する。
|
|
@@ -315,13 +339,20 @@ type QuadrantItemPayload = {
|
|
|
315
339
|
quadrant: BoundEnum<QuadrantKey>;
|
|
316
340
|
subtitle?: string;
|
|
317
341
|
};
|
|
318
|
-
/**
|
|
342
|
+
/**
|
|
343
|
+
* tree-hierarchy kind の 1 node payload。
|
|
344
|
+
*
|
|
345
|
+
* **上の小ラベル (`eyebrow`) は受けない** (#471)。 箱の高さは段の間隔から出ており
|
|
346
|
+
* (`Math.min(38, 間隔 * 0.7)`)、 名前と補足で 2 行を使うと 3 行目は入らない。 高さを広げると
|
|
347
|
+
* 段どうしが近づいて箱が重なる。 受け取って描かない欄は、 書き手から効いていないことが
|
|
348
|
+
* 読めないため置かない。
|
|
349
|
+
*/
|
|
319
350
|
type TreeNodePayload = {
|
|
320
351
|
id: string;
|
|
321
352
|
title: string;
|
|
322
353
|
parent?: string;
|
|
354
|
+
/** 箱の 2 行目に出す補足 */
|
|
323
355
|
subtitle?: string;
|
|
324
|
-
eyebrow?: string;
|
|
325
356
|
};
|
|
326
357
|
/** journey-map の気持ち 5 段階 */
|
|
327
358
|
type JourneyEmotion = "delighted" | "happy" | "neutral" | "frustrated" | "angry";
|
|
@@ -2016,6 +2047,20 @@ type CdlPhase = {
|
|
|
2016
2047
|
title: string;
|
|
2017
2048
|
body: string;
|
|
2018
2049
|
activate: string[];
|
|
2050
|
+
/**
|
|
2051
|
+
* この phase で「左の起点から描き始める」 node の id (cdl#512)。
|
|
2052
|
+
*
|
|
2053
|
+
* `activate` と同じ形で node id を並べる。 対象の node は phase の進み (0→1) に合わせて
|
|
2054
|
+
* 描かれ、 進みが 1 に達した時点で全体が出る。 現在の対象は `chart-line` だけで、
|
|
2055
|
+
* 折れ線が左端から右へ伸び、 点と値の数字は線の先が届いた時に現れる。
|
|
2056
|
+
*
|
|
2057
|
+
* **`activate` を兼ねさせない**。 1 箱で図全体を描く種別 (`chart-*` 等) では焦点が
|
|
2058
|
+
* その箱に解決されるため、 連続する phase で光り続ける図が実在する。 「光っていたら
|
|
2059
|
+
* 描く」 にすると、 値が動くだけの phase でも線を引き直すことになる。
|
|
2060
|
+
*
|
|
2061
|
+
* 書かない phase では従来どおり全長で描く (DOM に dash 属性も付かない)。
|
|
2062
|
+
*/
|
|
2063
|
+
draw?: string[];
|
|
2019
2064
|
/** 数値 state の線形補間 (write phase で 100→90 等) */
|
|
2020
2065
|
tweens: Array<{
|
|
2021
2066
|
stateId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardenelabs/cdl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "CDL (Chainome Diagram Language). Mermaid-like declarative DSL that compiles to animated SVG diagrams. Built for blockchain / Solidity flows, generic enough for sequence / flow / state / ER / topology diagrams.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "cardene777",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"src",
|
|
48
48
|
"examples",
|
|
49
49
|
"README.md",
|
|
50
|
-
"SPEC.md"
|
|
50
|
+
"SPEC.md",
|
|
51
|
+
"CHANGELOG.md"
|
|
51
52
|
],
|
|
52
53
|
"publishConfig": {
|
|
53
54
|
"access": "public"
|
|
@@ -62,7 +63,9 @@
|
|
|
62
63
|
"typecheck": "tsc --noEmit",
|
|
63
64
|
"test": "cd ../.. && vitest run packages/cdl",
|
|
64
65
|
"lint": "tsc --noEmit",
|
|
65
|
-
"prepublishOnly": "pnpm run typecheck && pnpm run build"
|
|
66
|
+
"prepublishOnly": "pnpm run typecheck && pnpm run build",
|
|
67
|
+
"prepack": "node scripts/prepack-check.mjs",
|
|
68
|
+
"postpack": "node scripts/prepack-check.mjs"
|
|
66
69
|
},
|
|
67
70
|
"peerDependencies": {
|
|
68
71
|
"react": "^19.0.0",
|
package/src/builder.ts
CHANGED
|
@@ -1355,6 +1355,11 @@ export type InputChain = {
|
|
|
1355
1355
|
|
|
1356
1356
|
export type PhaseBuilder = {
|
|
1357
1357
|
activate: (...ids: string[]) => PhaseBuilder;
|
|
1358
|
+
/**
|
|
1359
|
+
* この phase で左の起点から描き始める node を指定する (cdl#512)。
|
|
1360
|
+
* 対象は phase の進み (0→1) に合わせて描かれる。 現在の対象種別は `chart-line`。
|
|
1361
|
+
*/
|
|
1362
|
+
draw: (...ids: string[]) => PhaseBuilder;
|
|
1358
1363
|
/** 数値 state の線形補間 (write phase で 100→90 等) */
|
|
1359
1364
|
tween: (stateId: string, from: number, to: number) => PhaseBuilder;
|
|
1360
1365
|
/** 任意型 state を即時切替 (この phase 到達で value 上書き、 lerp なし) */
|
|
@@ -1364,6 +1369,7 @@ export type PhaseBuilder = {
|
|
|
1364
1369
|
|
|
1365
1370
|
type PhaseAcc = {
|
|
1366
1371
|
activate: string[];
|
|
1372
|
+
draw: string[];
|
|
1367
1373
|
tweens: Array<{ stateId: string; from: number; to: number }>;
|
|
1368
1374
|
sets: Array<{ stateId: string; value: string | number }>;
|
|
1369
1375
|
badge?: string;
|
|
@@ -1375,6 +1381,10 @@ function makePhaseBuilder(acc: PhaseAcc): PhaseBuilder {
|
|
|
1375
1381
|
acc.activate.push(...ids);
|
|
1376
1382
|
return api;
|
|
1377
1383
|
},
|
|
1384
|
+
draw(...ids) {
|
|
1385
|
+
acc.draw.push(...ids);
|
|
1386
|
+
return api;
|
|
1387
|
+
},
|
|
1378
1388
|
tween(stateId, from, to) {
|
|
1379
1389
|
acc.tweens.push({ stateId, from, to });
|
|
1380
1390
|
return api;
|
|
@@ -3690,7 +3700,7 @@ export function diagram(
|
|
|
3690
3700
|
},
|
|
3691
3701
|
},
|
|
3692
3702
|
phase(phaseId, opts, build) {
|
|
3693
|
-
const acc: PhaseAcc = { activate: [], tweens: [], sets: [] };
|
|
3703
|
+
const acc: PhaseAcc = { activate: [], draw: [], tweens: [], sets: [] };
|
|
3694
3704
|
build(makePhaseBuilder(acc));
|
|
3695
3705
|
phases.push({
|
|
3696
3706
|
id: phaseId,
|
|
@@ -3698,6 +3708,8 @@ export function diagram(
|
|
|
3698
3708
|
title: opts.title,
|
|
3699
3709
|
body: opts.body,
|
|
3700
3710
|
activate: acc.activate,
|
|
3711
|
+
// 空なら欄ごと置かない。 置くと `draw` を使わない図の JSON の形が変わる
|
|
3712
|
+
...(acc.draw.length > 0 ? { draw: acc.draw } : {}),
|
|
3701
3713
|
tweens: acc.tweens,
|
|
3702
3714
|
sets: acc.sets,
|
|
3703
3715
|
badge: acc.badge,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { textWidth } from "./text-width";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 箱の中に文字の行を積む (#471)。
|
|
5
|
+
*
|
|
6
|
+
* **JSX を持たない module に置く**。 木 (`tree.tsx`) と放射 (`mind-map.tsx`) の両方が使う。
|
|
7
|
+
* `text-width.ts` が同じ理由で分かれているのに合わせる。
|
|
8
|
+
*
|
|
9
|
+
* 箱の大きさは呼び出し側が決めた値をそのまま受ける = **積んだ結果で高さを変えない**。
|
|
10
|
+
* 木も放射も箱の高さが段の間隔から出ており (`Math.min(38, 間隔 * 0.7)`)、 高さを変えると
|
|
11
|
+
* 段どうしの間隔まで動いて既存の図の見た目が変わる。 入らない行は落とす方を採る。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 1 行が字の大きさの何倍の高さを占めるか。
|
|
16
|
+
*
|
|
17
|
+
* 詰め気味にするのは、 箱の高さが 30 前後しかないため (放射の枝 = 30、 木 = 最大 38)。
|
|
18
|
+
* 読みやすさの目安である 1.4-1.5 を採ると 2 行が入らず、 補足が常に落ちる。
|
|
19
|
+
*/
|
|
20
|
+
const 行送りの比 = 1.15;
|
|
21
|
+
|
|
22
|
+
/** 大文字の高さが字の大きさの何倍か (baseline を出すのに使う) */
|
|
23
|
+
const 字の高さの比 = 0.72;
|
|
24
|
+
|
|
25
|
+
/** 箱の上下に空ける余白 */
|
|
26
|
+
const 上下の余白 = 2;
|
|
27
|
+
|
|
28
|
+
/** 箱の左右に空ける余白 (片側) */
|
|
29
|
+
const 左右の余白 = 8;
|
|
30
|
+
|
|
31
|
+
export type 積む行 = {
|
|
32
|
+
役割: "title" | "subtitle";
|
|
33
|
+
text: string;
|
|
34
|
+
fontSize: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type 積んだ行 = 積む行 & {
|
|
38
|
+
/** 箱の中心から見た baseline の上下のずれ */
|
|
39
|
+
dy: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 幅に収まるところまでで切り、 切った時は末尾に `…` を付ける。
|
|
44
|
+
*
|
|
45
|
+
* 字の幅は実測の見積り (`textWidth`) から出す。 SVG は描く前に実寸を返さないため、
|
|
46
|
+
* 描いてから測って縮める形は取れない。
|
|
47
|
+
*
|
|
48
|
+
* `測る` を差し替えられるのは、 等幅の書体を使う絵があるため (#469)。 等幅は字ごとの幅を
|
|
49
|
+
* 持たず字数で決まるので、 比例用の見積りを当てると実物とずれる。
|
|
50
|
+
*/
|
|
51
|
+
export function 幅で切る(
|
|
52
|
+
text: string,
|
|
53
|
+
fontSize: number,
|
|
54
|
+
使える幅: number,
|
|
55
|
+
測る: (s: string, size: number) => number = textWidth,
|
|
56
|
+
): string {
|
|
57
|
+
if (使える幅 <= 0) return "";
|
|
58
|
+
if (測る(text, fontSize) <= 使える幅) return text;
|
|
59
|
+
|
|
60
|
+
const 省略 = "…";
|
|
61
|
+
const 省略の幅 = 測る(省略, fontSize);
|
|
62
|
+
// 省略記号すら入らない幅では何も返さない。 返すと「幅に収まる文字を返す」 という約束を
|
|
63
|
+
// 破り、 箱から出る (深い放射や葉の多い木で内側の幅が極小になった時に起きる)
|
|
64
|
+
if (省略の幅 > 使える幅) return "";
|
|
65
|
+
|
|
66
|
+
let 出 = "";
|
|
67
|
+
let 幅 = 0;
|
|
68
|
+
for (const c of 書記素に分ける(text)) {
|
|
69
|
+
const 字の幅 = 測る(c, fontSize);
|
|
70
|
+
if (幅 + 字の幅 + 省略の幅 > 使える幅) break;
|
|
71
|
+
幅 += 字の幅;
|
|
72
|
+
出 += c;
|
|
73
|
+
}
|
|
74
|
+
return `${出}${省略}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 見た目の 1 字 (書記素クラスタ) に分ける。
|
|
79
|
+
*
|
|
80
|
+
* `for...of` は符号位置で回るため、 濁点や結合文字の付いた字と、 ZWJ で繋いだ絵文字を
|
|
81
|
+
* **途中で切る** (実測 = `é` が `e` だけになり、 `👩💻` が `👩` になった)。 幅の見積り
|
|
82
|
+
* (`textWidth`) も符号位置で足すので数は合うが、 見た目の 1 字が壊れる。
|
|
83
|
+
*
|
|
84
|
+
* `Intl.Segmenter` を持たない環境では符号位置に落ちる。 切り方が粗くなるだけで、 幅の
|
|
85
|
+
* 見積りとは食い違わない。
|
|
86
|
+
*/
|
|
87
|
+
function 書記素に分ける(text: string): string[] {
|
|
88
|
+
const 区切り = (Intl as { Segmenter?: new (l?: string, o?: { granularity: string }) => { segment: (s: string) => Iterable<{ segment: string }> } }).Segmenter;
|
|
89
|
+
if (typeof 区切り !== "function") return [...text];
|
|
90
|
+
return [...new 区切り(undefined, { granularity: "grapheme" }).segment(text)].map((g) => g.segment);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 箱に入る行だけを選び、 それぞれの baseline を返す。
|
|
95
|
+
*
|
|
96
|
+
* 先頭 (名前) は必ず残す。 入り切らない時は **後ろから落とす** = 補足より名前を優先する。
|
|
97
|
+
* 2 件目以降の空文字は行として数えない (補足を渡していない時に空行が空くのを避ける)。
|
|
98
|
+
*
|
|
99
|
+
* 返す `dy` は箱の中心から見たずれで、 行の塊を上下の中央に置いた時の baseline になる。
|
|
100
|
+
*/
|
|
101
|
+
export function 箱に積む(行: 積む行[], 箱の幅: number, 箱の高さ: number): 積んだ行[] {
|
|
102
|
+
const 候補 = 行.filter((r, i) => i === 0 || r.text.trim() !== "");
|
|
103
|
+
if (候補.length === 0) return [];
|
|
104
|
+
|
|
105
|
+
const 使える高さ = 箱の高さ - 上下の余白 * 2;
|
|
106
|
+
const 高さ = (rs: 積む行[]): number => rs.reduce((a, r) => a + r.fontSize * 行送りの比, 0);
|
|
107
|
+
|
|
108
|
+
// 入るまで後ろから落とす。 先頭は落とさない
|
|
109
|
+
const 残す = [...候補];
|
|
110
|
+
while (残す.length > 1 && 高さ(残す) > 使える高さ) 残す.pop();
|
|
111
|
+
|
|
112
|
+
const 使える幅 = 箱の幅 - 左右の余白 * 2;
|
|
113
|
+
const 総高 = 高さ(残す);
|
|
114
|
+
let 積み = -総高 / 2;
|
|
115
|
+
return 残す.map((r) => {
|
|
116
|
+
const dy = 積み + ((行送りの比 + 字の高さの比) / 2) * r.fontSize;
|
|
117
|
+
積み += r.fontSize * 行送りの比;
|
|
118
|
+
return { ...r, text: 幅で切る(r.text, r.fontSize, 使える幅), dy };
|
|
119
|
+
});
|
|
120
|
+
}
|
package/src/kinds/chart-line.tsx
CHANGED
|
@@ -7,15 +7,29 @@ import { resolveChartData } from "../render/payload-binding";
|
|
|
7
7
|
* layout は node bbox (node.w × node.h) 内側に padding (top/right/bottom/left) を確保し、
|
|
8
8
|
* その内側 rect を chart canvas とみなす。 縦軸 = value min-max、 横軸 = datum index。
|
|
9
9
|
* 各 value は数でも `{signal}` でもよく、 後者は状態が動くたび折れ線が追随する。
|
|
10
|
+
*
|
|
11
|
+
* phase の `draw` にこの node の id が載っている間は、 折れ線が左の起点から伸びる (cdl#512)。
|
|
12
|
+
* 伸ばし方は dash (`pathLength` / `strokeDasharray` / `strokeDashoffset`) で、 `points` は
|
|
13
|
+
* 進みに関わらず全点を持つ。 点と値の数字は線の先が自分の位置に届いた時に現れ、
|
|
14
|
+
* 軸 / 格子 / 横軸の名前は最初から出る。
|
|
10
15
|
*/
|
|
11
16
|
export function ChartLineNode({
|
|
12
17
|
node,
|
|
13
18
|
active,
|
|
14
19
|
stateValues = {},
|
|
20
|
+
drawing = false,
|
|
21
|
+
progress = 1,
|
|
15
22
|
}: {
|
|
16
23
|
node: LaidNode;
|
|
17
24
|
active: boolean;
|
|
18
25
|
stateValues?: Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* この phase の `draw` に載っているか (cdl#512)。 true の間だけ折れ線が左の起点から
|
|
28
|
+
* 伸び、 点と値の数字は線の先が自分の位置に届いた時に現れる。
|
|
29
|
+
*/
|
|
30
|
+
drawing?: boolean;
|
|
31
|
+
/** phase の進み (0→1)。 `drawing` が true の時だけ読む */
|
|
32
|
+
progress?: number;
|
|
19
33
|
}): JSX.Element {
|
|
20
34
|
const x0 = node.cx - node.w / 2;
|
|
21
35
|
const y0 = node.cy - node.h / 2;
|
|
@@ -45,6 +59,44 @@ export function ChartLineNode({
|
|
|
45
59
|
|
|
46
60
|
const points = data.map((d, idx) => `${xAt(idx)},${yAt(d.value)}`).join(" ");
|
|
47
61
|
|
|
62
|
+
// dash は polyline の実際の経路長で進むため、 点の表示判定も同じ経路長に揃える。
|
|
63
|
+
// 点番号の比率では、 急傾斜と平坦な区間が混ざる線で先端と点がずれる。
|
|
64
|
+
const 点までの長さ: number[] = [];
|
|
65
|
+
let 線の全長 = 0;
|
|
66
|
+
let 前の点: { x: number; y: number } | undefined;
|
|
67
|
+
for (const [idx, datum] of data.entries()) {
|
|
68
|
+
const 今の点 = { x: xAt(idx), y: yAt(datum.value) };
|
|
69
|
+
if (前の点) {
|
|
70
|
+
線の全長 += Math.hypot(今の点.x - 前の点.x, 今の点.y - 前の点.y);
|
|
71
|
+
}
|
|
72
|
+
点までの長さ.push(線の全長);
|
|
73
|
+
前の点 = 今の点;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 線がどこまで伸びたか (0..1、 cdl#512)。 描く指定が無ければ常に 1 = 全長。
|
|
78
|
+
*
|
|
79
|
+
* 進みは呼出側から来る値なので、 範囲の外 (負 / 1 超 / NaN) が入りうる。 そのまま
|
|
80
|
+
* `strokeDashoffset` に渡すと線が消えたり逆向きに縮んだりするため、 ここで畳む。
|
|
81
|
+
*/
|
|
82
|
+
const 伸び = drawing ? Math.min(1, Math.max(0, Number.isFinite(progress) ? progress : 1)) : 1;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* その点まで線の先が届いたか。
|
|
86
|
+
*
|
|
87
|
+
* 点の位置は polyline の累積経路長で判定する。 **左端は進み 0 でも出す** = そこが
|
|
88
|
+
* 線の起点で、 起点が無いと線がどこから伸びているのか読めない。
|
|
89
|
+
*
|
|
90
|
+
* 点が 1 つの図では線を引かない (下の `data.length > 1`) ため、 割り算の分母が 0 に
|
|
91
|
+
* なる形には入らないが、 呼出の順に依らず成り立つよう先に返す。
|
|
92
|
+
*/
|
|
93
|
+
const 線の先が届いた = (idx: number): boolean => {
|
|
94
|
+
if (!drawing) return true;
|
|
95
|
+
if (data.length <= 1) return true;
|
|
96
|
+
if (線の全長 === 0) return true;
|
|
97
|
+
return 伸び >= (点までの長さ[idx] ?? 0) / 線の全長;
|
|
98
|
+
};
|
|
99
|
+
|
|
48
100
|
const gridCount = 4;
|
|
49
101
|
const gridTicks = Array.from({ length: gridCount + 1 }, (_, i) => {
|
|
50
102
|
const v = vMin + (vRange * i) / gridCount;
|
|
@@ -114,6 +166,16 @@ export function ChartLineNode({
|
|
|
114
166
|
strokeWidth={2.5}
|
|
115
167
|
strokeLinejoin="round"
|
|
116
168
|
strokeLinecap="round"
|
|
169
|
+
// 伸ばすのは dash で行い、 **`points` は触らない** (cdl#512)。 点を削る形にすると
|
|
170
|
+
// 利用側が数えている点の数が進みの途中で変わる (dragon の `kind-geometry-check`)。
|
|
171
|
+
//
|
|
172
|
+
// `pathLength={1}` を置くのは、 実長を測るには DOM の ref が要り、 静的な書き出しでは
|
|
173
|
+
// 測れないため。 長さを 1 に正規化すれば進みをそのまま offset に使える。
|
|
174
|
+
//
|
|
175
|
+
// 描く指定が無い図には属性を 1 つも足さない = 既存の図の DOM を変えない
|
|
176
|
+
{...(drawing
|
|
177
|
+
? { pathLength: 1, strokeDasharray: 1, strokeDashoffset: 1 - 伸び }
|
|
178
|
+
: {})}
|
|
117
179
|
/>
|
|
118
180
|
)}
|
|
119
181
|
{data.map((d, idx) => {
|
|
@@ -129,33 +191,40 @@ export function ChartLineNode({
|
|
|
129
191
|
(prev === undefined || d.value <= prev) && (next === undefined || d.value <= next);
|
|
130
192
|
const labelAbove = !isValley;
|
|
131
193
|
const labelY = labelAbove ? cy - 12 : cy + 18;
|
|
194
|
+
// 点と値の数字は線の先に合わせて現れる。 **横軸の名前は最初から出す** =
|
|
195
|
+
// 名前は図の枠 (軸 / 格子) の側で、 これが順に増えると軸そのものが伸びて見える
|
|
196
|
+
const 出す = 線の先が届いた(idx);
|
|
132
197
|
return (
|
|
133
198
|
<g key={`pt-${idx}`} data-cdl-unresolved={d.unresolved ? "true" : undefined}>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
199
|
+
{出す && (
|
|
200
|
+
<>
|
|
201
|
+
<circle
|
|
202
|
+
cx={cx}
|
|
203
|
+
cy={cy}
|
|
204
|
+
r={5}
|
|
205
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
206
|
+
stroke="var(--cdl-tone-accent, #2d6a8f)"
|
|
207
|
+
strokeWidth={2.5}
|
|
208
|
+
/>
|
|
209
|
+
<circle
|
|
210
|
+
cx={cx}
|
|
211
|
+
cy={cy}
|
|
212
|
+
r={2.5}
|
|
213
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
214
|
+
/>
|
|
215
|
+
<text
|
|
216
|
+
data-cdl-role="chart-line-value"
|
|
217
|
+
x={cx}
|
|
218
|
+
y={labelY}
|
|
219
|
+
fontSize={11}
|
|
220
|
+
fontWeight={600}
|
|
221
|
+
textAnchor="middle"
|
|
222
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
223
|
+
>
|
|
224
|
+
{d.value.toLocaleString()}
|
|
225
|
+
</text>
|
|
226
|
+
</>
|
|
227
|
+
)}
|
|
159
228
|
<text
|
|
160
229
|
x={cx}
|
|
161
230
|
y={PAD_TOP + canvasH + 20}
|