@cardenelabs/cdl 0.10.0 → 0.12.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 +65 -1
- package/SPEC.md +25 -3
- package/dist/index.cjs +387 -169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +388 -170
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +387 -169
- 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 +388 -170
- package/dist/react.js.map +1 -1
- package/dist/{render-CH1Qn3Jv.d.cts → render-C7FKvMqq.d.cts} +14 -2
- package/dist/{render-CH1Qn3Jv.d.ts → render-C7FKvMqq.d.ts} +14 -2
- package/package.json +1 -1
- package/src/kinds/chart-bar.tsx +45 -12
- package/src/kinds/chart-line.tsx +4 -10
- package/src/kinds/chart-pie.tsx +76 -15
- package/src/kinds/draw-progress.ts +100 -0
- package/src/kinds/funnel.tsx +101 -58
- package/src/kinds/gantt.tsx +25 -1
- package/src/kinds/mind-map.tsx +65 -14
- package/src/kinds/tree.tsx +52 -4
- package/src/kinds/user-journey.tsx +29 -1
- package/src/render/nodes.tsx +63 -7
- package/src/types.ts +14 -2
- package/src/validate.ts +32 -2
package/src/kinds/gantt.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import type { JSX } from "react";
|
|
|
2
2
|
import type { LaidNode, Tone } from "../types";
|
|
3
3
|
import { TONE } from "../render/tone";
|
|
4
4
|
import { resolveGanttData } from "../render/payload-binding";
|
|
5
|
+
import { 進みを畳む } from "./draw-progress";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* gantt-timeline kind ... 1 node に task 配列 (ganttData) を保持し、 帯状 timeline を SVG 描画する。
|
|
@@ -13,10 +14,16 @@ export function GanttNode({
|
|
|
13
14
|
node,
|
|
14
15
|
active,
|
|
15
16
|
stateValues = {},
|
|
17
|
+
drawing = false,
|
|
18
|
+
progress = 1,
|
|
16
19
|
}: {
|
|
17
20
|
node: LaidNode;
|
|
18
21
|
active: boolean;
|
|
19
22
|
stateValues?: Record<string, string>;
|
|
23
|
+
/** この phase の `draw` に載っているか (cdl#521) */
|
|
24
|
+
drawing?: boolean;
|
|
25
|
+
/** phase の進み (0→1)。 `drawing` が true の時だけ読む */
|
|
26
|
+
progress?: number;
|
|
20
27
|
}): JSX.Element {
|
|
21
28
|
const x0 = node.cx - node.w / 2;
|
|
22
29
|
const y0 = node.cy - node.h / 2;
|
|
@@ -166,6 +173,14 @@ export function GanttNode({
|
|
|
166
173
|
const 帯の左 = (idx: number): number => xAt(idx) + 帯の余白 / 2;
|
|
167
174
|
const 帯の右 = (idx: number): number => xAt(idx) + cellW - 帯の余白 / 2;
|
|
168
175
|
|
|
176
|
+
/**
|
|
177
|
+
* 帯がどこまで伸びたか (0..1、 cdl#521)。 描く指定が無ければ常に 1 = 全長。
|
|
178
|
+
*
|
|
179
|
+
* **帯ごとに始端が違う**。 棒グラフは全部が同じ横軸から伸びるが、工程表は工程ごとに
|
|
180
|
+
* 始まる位置が違うため、包みも帯ごとに作って不動点をその帯の左端に置く。
|
|
181
|
+
*/
|
|
182
|
+
const 伸び = 進みを畳む(drawing, progress);
|
|
183
|
+
|
|
169
184
|
const taskById = new Map(整えた.map((t) => [t.id, t]));
|
|
170
185
|
const rowOf = new Map(整えた.map((t, i) => [t.id, i]));
|
|
171
186
|
|
|
@@ -243,7 +258,12 @@ export function GanttNode({
|
|
|
243
258
|
// 余白の決め方を変えた時に負の幅を出さないために残す
|
|
244
259
|
const bw = Math.max(0, 帯の右(t.endIdx) - bx);
|
|
245
260
|
return (
|
|
246
|
-
<g
|
|
261
|
+
<g
|
|
262
|
+
key={`bar-${t.id}`}
|
|
263
|
+
{...(drawing
|
|
264
|
+
? { transform: `translate(${bx} 0) scale(${伸び} 1) translate(${-bx} 0)` }
|
|
265
|
+
: {})}
|
|
266
|
+
>
|
|
247
267
|
<rect
|
|
248
268
|
data-cdl-role="gantt-bar"
|
|
249
269
|
data-cdl-unresolved={t.unresolved ? "true" : undefined}
|
|
@@ -304,6 +324,9 @@ export function GanttNode({
|
|
|
304
324
|
` L ${enterX} ${(parentMidY + tipY) / 2} L ${enterX} ${tipY} L ${tipX} ${tipY}`;
|
|
305
325
|
const arrowHeadPath = `M ${tipX + arrowHeadSize} ${tipY} L ${tipX} ${tipY - arrowHeadSize / 2} L ${tipX} ${tipY + arrowHeadSize / 2} Z`;
|
|
306
326
|
return (
|
|
327
|
+
// 矢印は両端の帯が出てから描く。 帯が無い場所へ矢印だけが伸びると、
|
|
328
|
+
// 指す先の無い線になる (cdl#521)
|
|
329
|
+
(drawing && 伸び < 1) ? null : (
|
|
307
330
|
<g key={`dep-${parent.id}-${t.id}`} data-cdl-role="gantt-arrow">
|
|
308
331
|
<path
|
|
309
332
|
d={path}
|
|
@@ -321,6 +344,7 @@ export function GanttNode({
|
|
|
321
344
|
strokeLinejoin="round"
|
|
322
345
|
/>
|
|
323
346
|
</g>
|
|
347
|
+
)
|
|
324
348
|
);
|
|
325
349
|
})}
|
|
326
350
|
</g>
|
package/src/kinds/mind-map.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { 箱に積む } from "./box-lines";
|
|
|
3
3
|
import type { LaidNode, Tone, MindBranchNode } from "../types";
|
|
4
4
|
import { TONE } from "../render/tone";
|
|
5
5
|
import { resolveMindData } from "../render/payload-binding";
|
|
6
|
+
import { 進みを畳む, 伸ばすdash, 段の伸び } from "./draw-progress";
|
|
6
7
|
|
|
7
8
|
const MIND_TONES: Tone[] = ["accent", "teal", "success", "warning", "info", "error"];
|
|
8
9
|
|
|
@@ -31,10 +32,16 @@ export function MindMapNode({
|
|
|
31
32
|
node,
|
|
32
33
|
active,
|
|
33
34
|
stateValues = {},
|
|
35
|
+
drawing = false,
|
|
36
|
+
progress = 1,
|
|
34
37
|
}: {
|
|
35
38
|
node: LaidNode;
|
|
36
39
|
active: boolean;
|
|
37
40
|
stateValues?: Record<string, string>;
|
|
41
|
+
/** この phase の `draw` に載っているか (cdl#520) */
|
|
42
|
+
drawing?: boolean;
|
|
43
|
+
/** phase の進み (0→1)。 `drawing` が true の時だけ読む */
|
|
44
|
+
progress?: number;
|
|
38
45
|
}): JSX.Element {
|
|
39
46
|
const x0 = node.cx - node.w / 2;
|
|
40
47
|
const y0 = node.cy - node.h / 2;
|
|
@@ -50,6 +57,16 @@ export function MindMapNode({
|
|
|
50
57
|
|
|
51
58
|
const layout = computeMindMapLayout(mindData, canvasCx, canvasCy, canvasW, canvasH);
|
|
52
59
|
|
|
60
|
+
/**
|
|
61
|
+
* 枝がどこまで伸びたか (0..1、 cdl#520)。 描く指定が無ければ常に 1 = 全部出ている。
|
|
62
|
+
*
|
|
63
|
+
* 中心に近い段から順に伸ばす。 全部を同時に伸ばすと、離れた枝が宙に浮いたまま伸びる。
|
|
64
|
+
*/
|
|
65
|
+
const 伸び = 進みを畳む(drawing, progress);
|
|
66
|
+
const 最大深さ = layout.nodes.reduce((m, n) => Math.max(m, n.depth), 0);
|
|
67
|
+
/** その箱が出るか。 中心 (深さ 0) は起点なので進み 0 でも出す */
|
|
68
|
+
const 箱が出る = (深さ: number): boolean => !drawing || 段の伸び(伸び, 深さ, 最大深さ) >= 1;
|
|
69
|
+
|
|
53
70
|
return (
|
|
54
71
|
<g transform={`translate(${x0} ${y0})`}>
|
|
55
72
|
<rect
|
|
@@ -64,20 +81,29 @@ export function MindMapNode({
|
|
|
64
81
|
strokeWidth={active ? 3 : 1.25}
|
|
65
82
|
/>
|
|
66
83
|
|
|
67
|
-
{layout.edges.map((e, i) =>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
{layout.edges.map((e, i) => {
|
|
85
|
+
// 循環を閉じる枝は根から外へ向かう枝ではないため、全 node が出てから表示する
|
|
86
|
+
const 枝の伸び = e.deferUntilComplete
|
|
87
|
+
? (伸び >= 1 ? 1 : 0)
|
|
88
|
+
: 段の伸び(伸び, e.depth, 最大深さ);
|
|
89
|
+
return (
|
|
90
|
+
<path
|
|
91
|
+
key={`edge-${i}`}
|
|
92
|
+
data-cdl-role="mind-edge"
|
|
93
|
+
d={e.d}
|
|
94
|
+
fill="none"
|
|
95
|
+
stroke={toneColor(e.tone)}
|
|
96
|
+
strokeWidth={2.5}
|
|
97
|
+
strokeOpacity={0.55}
|
|
98
|
+
strokeLinecap="round"
|
|
99
|
+
{...伸ばすdash(drawing, 枝の伸び)}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
79
103
|
|
|
80
104
|
{layout.nodes.map((n) => {
|
|
105
|
+
// 枝が届いてから箱を出す。 届く前に出すと、繋がっていない箱が宙に浮く
|
|
106
|
+
if (!箱が出る(n.depth)) return null;
|
|
81
107
|
// 補足は箱の中の 2 行目に出す (#471)。 箱の高さは変えないため、 入らない時は落ちる
|
|
82
108
|
const 行 = 箱に積む(
|
|
83
109
|
[
|
|
@@ -150,6 +176,8 @@ type LayoutNode = {
|
|
|
150
176
|
h: number;
|
|
151
177
|
tone?: Tone;
|
|
152
178
|
isRoot: boolean;
|
|
179
|
+
/** 中心からの段数 (中心が 0、 cdl#520)。 段ごとに順に伸ばすのに使う */
|
|
180
|
+
depth: number;
|
|
153
181
|
/** 名前を状態から解決できなかった項目に付く印 (#467) */
|
|
154
182
|
unresolved?: true;
|
|
155
183
|
};
|
|
@@ -157,6 +185,10 @@ type LayoutNode = {
|
|
|
157
185
|
type LayoutEdge = {
|
|
158
186
|
d: string;
|
|
159
187
|
tone?: Tone;
|
|
188
|
+
/** 枝の先 (子) の段数 (cdl#520)。 段ごとに順に伸ばすのに使う */
|
|
189
|
+
depth: number;
|
|
190
|
+
/** 循環により、元の親が layout 上で子と同じ深さか深くなった枝 */
|
|
191
|
+
deferUntilComplete: boolean;
|
|
160
192
|
};
|
|
161
193
|
|
|
162
194
|
export function computeMindMapLayout(
|
|
@@ -179,6 +211,8 @@ export function computeMindMapLayout(
|
|
|
179
211
|
w: rootW,
|
|
180
212
|
h: rootH,
|
|
181
213
|
isRoot: true,
|
|
214
|
+
// 中心は起点なので段数 0 (cdl#520)
|
|
215
|
+
depth: 0,
|
|
182
216
|
...((mindData as { unresolved?: true }).unresolved ? { unresolved: true as const } : {}),
|
|
183
217
|
});
|
|
184
218
|
|
|
@@ -339,6 +373,8 @@ export function computeMindMapLayout(
|
|
|
339
373
|
y,
|
|
340
374
|
w,
|
|
341
375
|
h,
|
|
376
|
+
// 第 1 階層が段数 1 (`subDepth` は 0 始まり、 cdl#520)
|
|
377
|
+
depth: subDepth + 1,
|
|
342
378
|
tone: myTone,
|
|
343
379
|
isRoot: false,
|
|
344
380
|
...((node as { unresolved?: true }).unresolved ? { unresolved: true as const } : {}),
|
|
@@ -388,6 +424,9 @@ export function computeMindMapLayout(
|
|
|
388
424
|
}
|
|
389
425
|
}
|
|
390
426
|
|
|
427
|
+
// 枝の段数は「その枝の先 (子)」 の段数にする (cdl#520)。 箱を置き終えた後に引く
|
|
428
|
+
const 段数 = new Map(nodes.map((n) => [n.id, n.depth]));
|
|
429
|
+
|
|
391
430
|
// edge = 親の辺 → 子の辺。 root → 第 1 branch は root の辺から。
|
|
392
431
|
for (const br of mindData.branches) {
|
|
393
432
|
const childPos = nodePos.get(br.id);
|
|
@@ -395,17 +434,29 @@ export function computeMindMapLayout(
|
|
|
395
434
|
const isLeft = nodeSide.get(br.id) === "left";
|
|
396
435
|
const childHalfW = nodeHalfW.get(br.id) ?? subBranchW / 2;
|
|
397
436
|
const tone = nodeTone.get(br.id);
|
|
437
|
+
const childDepth = 段数.get(br.id) ?? 1;
|
|
438
|
+
const parentDepth = 段数.get(br.parent) ?? 0;
|
|
398
439
|
if (br.parent === mindData.rootId) {
|
|
399
440
|
const startX = rootEdgeXFor(isLeft);
|
|
400
441
|
const endX = childPos.x + (isLeft ? childHalfW : -childHalfW);
|
|
401
|
-
edges.push({
|
|
442
|
+
edges.push({
|
|
443
|
+
d: makeCurve(startX, cy, endX, childPos.y, isLeft),
|
|
444
|
+
tone,
|
|
445
|
+
depth: childDepth,
|
|
446
|
+
deferUntilComplete: false,
|
|
447
|
+
});
|
|
402
448
|
} else {
|
|
403
449
|
const parentPos = nodePos.get(br.parent);
|
|
404
450
|
if (!parentPos) continue;
|
|
405
451
|
const parentHalfW = nodeHalfW.get(br.parent) ?? branchW / 2;
|
|
406
452
|
const startX = parentPos.x + (isLeft ? -parentHalfW : parentHalfW);
|
|
407
453
|
const endX = childPos.x + (isLeft ? childHalfW : -childHalfW);
|
|
408
|
-
edges.push({
|
|
454
|
+
edges.push({
|
|
455
|
+
d: makeCurve(startX, parentPos.y, endX, childPos.y, isLeft),
|
|
456
|
+
tone,
|
|
457
|
+
depth: Math.max(parentDepth, childDepth),
|
|
458
|
+
deferUntilComplete: parentDepth >= childDepth,
|
|
459
|
+
});
|
|
409
460
|
}
|
|
410
461
|
}
|
|
411
462
|
|
package/src/kinds/tree.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import type { LaidNode, TreeNodePayload } from "../types";
|
|
|
3
3
|
import { TONE } from "../render/tone";
|
|
4
4
|
import { resolveTreeData } from "../render/payload-binding";
|
|
5
5
|
import { 箱に積む, type 積んだ行 } from "../kinds/box-lines";
|
|
6
|
+
import { 進みを畳む, 伸ばすdash, 段の伸び } from "./draw-progress";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* tree-hierarchy kind ... 1 node に tree node 配列 (treeData) を保持し、
|
|
@@ -53,10 +54,16 @@ export function TreeNode({
|
|
|
53
54
|
node,
|
|
54
55
|
active,
|
|
55
56
|
stateValues = {},
|
|
57
|
+
drawing = false,
|
|
58
|
+
progress = 1,
|
|
56
59
|
}: {
|
|
57
60
|
node: LaidNode;
|
|
58
61
|
active: boolean;
|
|
59
62
|
stateValues?: Record<string, string>;
|
|
63
|
+
/** この phase の `draw` に載っているか (cdl#520) */
|
|
64
|
+
drawing?: boolean;
|
|
65
|
+
/** phase の進み (0→1)。 `drawing` が true の時だけ読む */
|
|
66
|
+
progress?: number;
|
|
60
67
|
}): JSX.Element {
|
|
61
68
|
const x0 = node.cx - node.w / 2;
|
|
62
69
|
const y0 = node.cy - node.h / 2;
|
|
@@ -72,6 +79,16 @@ export function TreeNode({
|
|
|
72
79
|
const canvasH = node.h - PAD_TOP - PAD_BOTTOM;
|
|
73
80
|
|
|
74
81
|
const layout = computeTreeLayout(treeNodes, canvasW, canvasH);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 枝がどこまで伸びたか (0..1、 cdl#520)。 描く指定が無ければ常に 1 = 全部出ている。
|
|
85
|
+
*
|
|
86
|
+
* 根に近い段から順に伸ばす。 全部を同時に伸ばすと、離れた枝が宙に浮いたまま伸びる。
|
|
87
|
+
*/
|
|
88
|
+
const 伸び = 進みを畳む(drawing, progress);
|
|
89
|
+
const 最大深さ = layout.nodes.reduce((m, n) => Math.max(m, n.depth), 0);
|
|
90
|
+
/** その箱が出るか。 根 (深さ 0) は起点なので進み 0 でも出す */
|
|
91
|
+
const 箱が出る = (深さ: number): boolean => !drawing || 段の伸び(伸び, 深さ, 最大深さ) >= 1;
|
|
75
92
|
const depthColors = [TONE.accent, TONE.teal, TONE.success, TONE.warning, TONE.info];
|
|
76
93
|
|
|
77
94
|
return (
|
|
@@ -106,6 +123,10 @@ export function TreeNode({
|
|
|
106
123
|
const y2 = PAD_TOP + e.y2;
|
|
107
124
|
const midY = (y1 + y2) / 2;
|
|
108
125
|
const path = `M ${x1} ${y1} L ${x1} ${midY} L ${x2} ${midY} L ${x2} ${y2}`;
|
|
126
|
+
// 循環を閉じる枝は根から外へ向かう枝ではないため、全 node が出てから表示する
|
|
127
|
+
const 枝の伸び = e.deferUntilComplete
|
|
128
|
+
? (伸び >= 1 ? 1 : 0)
|
|
129
|
+
: 段の伸び(伸び, e.depth, 最大深さ);
|
|
109
130
|
return (
|
|
110
131
|
<g key={`edge-${i}`}>
|
|
111
132
|
<path
|
|
@@ -116,13 +137,19 @@ export function TreeNode({
|
|
|
116
137
|
strokeOpacity={0.55}
|
|
117
138
|
strokeWidth={1.5}
|
|
118
139
|
strokeLinejoin="round"
|
|
140
|
+
{...伸ばすdash(drawing, 枝の伸び)}
|
|
119
141
|
/>
|
|
120
|
-
|
|
142
|
+
{/* 曲がり角の点は枝が届いてから出す。 先に出ると線の無い場所に点が浮く */}
|
|
143
|
+
{(!drawing || 枝の伸び >= 1) && (
|
|
144
|
+
<circle cx={x1} cy={midY} r={2.5} fill={TONE.accent} fillOpacity={0.75} />
|
|
145
|
+
)}
|
|
121
146
|
</g>
|
|
122
147
|
);
|
|
123
148
|
})}
|
|
124
149
|
|
|
125
150
|
{layout.nodes.map((n) => {
|
|
151
|
+
// 枝が届いてから箱を出す。 届く前に出すと、繋がっていない箱が宙に浮く
|
|
152
|
+
if (!箱が出る(n.depth)) return null;
|
|
126
153
|
const accent = depthColors[Math.min(n.depth, depthColors.length - 1)] ?? depthColors[0]!;
|
|
127
154
|
const isRoot = n.depth === 0;
|
|
128
155
|
// 補足は箱の中の 2 行目に出す (#471)。 箱の高さは変えないため、 入らない時は落ちる
|
|
@@ -232,13 +259,23 @@ type LayoutNode = {
|
|
|
232
259
|
unresolved?: true;
|
|
233
260
|
};
|
|
234
261
|
|
|
262
|
+
type LayoutEdge = {
|
|
263
|
+
x1: number;
|
|
264
|
+
y1: number;
|
|
265
|
+
x2: number;
|
|
266
|
+
y2: number;
|
|
267
|
+
depth: number;
|
|
268
|
+
/** 循環により、元の親が layout 上で子と同じ深さか深くなった枝 */
|
|
269
|
+
deferUntilComplete: boolean;
|
|
270
|
+
};
|
|
271
|
+
|
|
235
272
|
export function computeTreeLayout(
|
|
236
273
|
treeNodes: TreeNodePayload[],
|
|
237
274
|
canvasW: number,
|
|
238
275
|
canvasH: number,
|
|
239
276
|
): {
|
|
240
277
|
nodes: LayoutNode[];
|
|
241
|
-
edges:
|
|
278
|
+
edges: LayoutEdge[];
|
|
242
279
|
} {
|
|
243
280
|
// parent が実在しない node は root 扱いにする。 そうしないと root からの再帰で辿れず、
|
|
244
281
|
// node が silent に消える (旧実装は全 node を列挙していたので消えなかった)。
|
|
@@ -357,7 +394,9 @@ export function computeTreeLayout(
|
|
|
357
394
|
}
|
|
358
395
|
const hById = new Map(nodes.map((n) => [n.id, n.h]));
|
|
359
396
|
|
|
360
|
-
|
|
397
|
+
// 枝の段数は「その枝の先 (子)」 の段数 (cdl#520)。 段ごとに順に伸ばすのに使う
|
|
398
|
+
const 段数 = new Map(nodes.map((n) => [n.id, n.depth]));
|
|
399
|
+
const edges: LayoutEdge[] = [];
|
|
361
400
|
for (const n of treeNodes) {
|
|
362
401
|
if (!n.parent) continue;
|
|
363
402
|
const parent = posOf.get(n.parent);
|
|
@@ -365,7 +404,16 @@ export function computeTreeLayout(
|
|
|
365
404
|
if (!parent || !child) continue;
|
|
366
405
|
const parentH = hById.get(n.parent) ?? nodeH;
|
|
367
406
|
const childH = hById.get(n.id) ?? nodeH;
|
|
368
|
-
|
|
407
|
+
const parentDepth = 段数.get(n.parent) ?? 0;
|
|
408
|
+
const childDepth = 段数.get(n.id) ?? 1;
|
|
409
|
+
edges.push({
|
|
410
|
+
x1: parent.x,
|
|
411
|
+
y1: parent.y + parentH / 2,
|
|
412
|
+
x2: child.x,
|
|
413
|
+
y2: child.y - childH / 2,
|
|
414
|
+
depth: Math.max(parentDepth, childDepth),
|
|
415
|
+
deferUntilComplete: parentDepth >= childDepth,
|
|
416
|
+
});
|
|
369
417
|
}
|
|
370
418
|
|
|
371
419
|
return { nodes, edges };
|
|
@@ -2,6 +2,7 @@ import type { JSX } from "react";
|
|
|
2
2
|
import type { LaidNode, JourneyEmotion } from "../types";
|
|
3
3
|
import { TONE } from "../render/tone";
|
|
4
4
|
import { resolveJourneyData } from "../render/payload-binding";
|
|
5
|
+
import { 進みを畳む, 伸ばすdash, ベジエの累積割合 } from "./draw-progress";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* journey-map kind ... 1 node に step 配列 (journeyData) を保持し、 UX journey map を SVG 描画する。
|
|
@@ -18,10 +19,16 @@ export function UserJourneyNode({
|
|
|
18
19
|
node,
|
|
19
20
|
active,
|
|
20
21
|
stateValues = {},
|
|
22
|
+
drawing = false,
|
|
23
|
+
progress = 1,
|
|
21
24
|
}: {
|
|
22
25
|
node: LaidNode;
|
|
23
26
|
active: boolean;
|
|
24
27
|
stateValues?: Record<string, string>;
|
|
28
|
+
/** この phase の `draw` に載っているか (cdl#520) */
|
|
29
|
+
drawing?: boolean;
|
|
30
|
+
/** phase の進み (0→1)。 `drawing` が true の時だけ読む */
|
|
31
|
+
progress?: number;
|
|
25
32
|
}): JSX.Element {
|
|
26
33
|
const x0 = node.cx - node.w / 2;
|
|
27
34
|
const y0 = node.cy - node.h / 2;
|
|
@@ -70,6 +77,21 @@ export function UserJourneyNode({
|
|
|
70
77
|
|
|
71
78
|
// smooth curve for emotion path
|
|
72
79
|
const pts = steps.map((s, i) => ({ x: xAt(i), y: yAt(s.emotion) }));
|
|
80
|
+
/**
|
|
81
|
+
* 線がどこまで伸びたか (0..1、 cdl#520)。 描く指定が無ければ常に 1 = 全長。
|
|
82
|
+
*/
|
|
83
|
+
const 伸び = 進みを畳む(drawing, progress);
|
|
84
|
+
/**
|
|
85
|
+
* 各段までの累積割合。 **点番号の比率ではなく実際の経路長で測る** (#513 の指摘)。
|
|
86
|
+
* 気持ちの上下で区間の長さが変わるため、比率で判定すると先端と丸がずれる。
|
|
87
|
+
*/
|
|
88
|
+
const 段までの割合 = ベジエの累積割合(pts, (前, 後) => {
|
|
89
|
+
const midX = (前.x + 後.x) / 2;
|
|
90
|
+
return [{ x: midX, y: 前.y }, { x: midX, y: 後.y }];
|
|
91
|
+
});
|
|
92
|
+
/** その段まで線の先が届いたか。 左端は進み 0 でも出す (そこが線の起点) */
|
|
93
|
+
const 線の先が届いた = (idx: number): boolean => !drawing || 伸び >= (段までの割合[idx] ?? 0);
|
|
94
|
+
|
|
73
95
|
const smoothPath = pts.length > 1
|
|
74
96
|
? pts.reduce((acc, p, i) => {
|
|
75
97
|
if (i === 0) return `M ${p.x} ${p.y}`;
|
|
@@ -144,6 +166,10 @@ export function UserJourneyNode({
|
|
|
144
166
|
|
|
145
167
|
{steps.length > 1 && (
|
|
146
168
|
<>
|
|
169
|
+
{/*
|
|
170
|
+
影と本体は同じ進みで伸ばす。 片方だけ先行すると、線の先に影だけが伸びた
|
|
171
|
+
形になる (cdl#520)
|
|
172
|
+
*/}
|
|
147
173
|
<path
|
|
148
174
|
data-cdl-role="journey-line-glow"
|
|
149
175
|
d={smoothPath}
|
|
@@ -152,6 +178,7 @@ export function UserJourneyNode({
|
|
|
152
178
|
strokeOpacity={0.18}
|
|
153
179
|
strokeWidth={10}
|
|
154
180
|
strokeLinecap="round"
|
|
181
|
+
{...伸ばすdash(drawing, 伸び)}
|
|
155
182
|
/>
|
|
156
183
|
<path
|
|
157
184
|
data-cdl-role="journey-line"
|
|
@@ -161,11 +188,12 @@ export function UserJourneyNode({
|
|
|
161
188
|
strokeWidth={2.75}
|
|
162
189
|
strokeLinejoin="round"
|
|
163
190
|
strokeLinecap="round"
|
|
191
|
+
{...伸ばすdash(drawing, 伸び)}
|
|
164
192
|
/>
|
|
165
193
|
</>
|
|
166
194
|
)}
|
|
167
195
|
|
|
168
|
-
{steps.map((s, idx) => (
|
|
196
|
+
{steps.map((s, idx) => !線の先が届いた(idx) ? null : (
|
|
169
197
|
<g
|
|
170
198
|
key={`step-${s.id}`}
|
|
171
199
|
data-cdl-role="journey-step"
|
package/src/render/nodes.tsx
CHANGED
|
@@ -170,21 +170,77 @@ export function CdlNodeView({
|
|
|
170
170
|
/>
|
|
171
171
|
);
|
|
172
172
|
case "chart-pie":
|
|
173
|
-
return
|
|
173
|
+
return (
|
|
174
|
+
<ChartPieNode
|
|
175
|
+
node={resolvedNode}
|
|
176
|
+
active={active}
|
|
177
|
+
stateValues={stateValues}
|
|
178
|
+
drawing={drawing}
|
|
179
|
+
progress={progress}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
174
182
|
case "chart-bar":
|
|
175
|
-
return
|
|
183
|
+
return (
|
|
184
|
+
<ChartBarNode
|
|
185
|
+
node={resolvedNode}
|
|
186
|
+
active={active}
|
|
187
|
+
stateValues={stateValues}
|
|
188
|
+
drawing={drawing}
|
|
189
|
+
progress={progress}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
176
192
|
case "gantt-timeline":
|
|
177
|
-
return
|
|
193
|
+
return (
|
|
194
|
+
<GanttNode
|
|
195
|
+
node={resolvedNode}
|
|
196
|
+
active={active}
|
|
197
|
+
stateValues={stateValues}
|
|
198
|
+
drawing={drawing}
|
|
199
|
+
progress={progress}
|
|
200
|
+
/>
|
|
201
|
+
);
|
|
178
202
|
case "mind-map":
|
|
179
|
-
return
|
|
203
|
+
return (
|
|
204
|
+
<MindMapNode
|
|
205
|
+
node={resolvedNode}
|
|
206
|
+
active={active}
|
|
207
|
+
stateValues={stateValues}
|
|
208
|
+
drawing={drawing}
|
|
209
|
+
progress={progress}
|
|
210
|
+
/>
|
|
211
|
+
);
|
|
180
212
|
case "funnel-stages":
|
|
181
|
-
return
|
|
213
|
+
return (
|
|
214
|
+
<FunnelNode
|
|
215
|
+
node={resolvedNode}
|
|
216
|
+
active={active}
|
|
217
|
+
stateValues={stateValues}
|
|
218
|
+
drawing={drawing}
|
|
219
|
+
progress={progress}
|
|
220
|
+
/>
|
|
221
|
+
);
|
|
182
222
|
case "quadrant-matrix":
|
|
183
223
|
return <QuadrantNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
184
224
|
case "tree-hierarchy":
|
|
185
|
-
return
|
|
225
|
+
return (
|
|
226
|
+
<TreeHierarchyNode
|
|
227
|
+
node={resolvedNode}
|
|
228
|
+
active={active}
|
|
229
|
+
stateValues={stateValues}
|
|
230
|
+
drawing={drawing}
|
|
231
|
+
progress={progress}
|
|
232
|
+
/>
|
|
233
|
+
);
|
|
186
234
|
case "journey-map":
|
|
187
|
-
return
|
|
235
|
+
return (
|
|
236
|
+
<UserJourneyNode
|
|
237
|
+
node={resolvedNode}
|
|
238
|
+
active={active}
|
|
239
|
+
stateValues={stateValues}
|
|
240
|
+
drawing={drawing}
|
|
241
|
+
progress={progress}
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
188
244
|
case "shape-file":
|
|
189
245
|
return <ShapeFileNode node={resolvedNode} active={active} />;
|
|
190
246
|
case "shape-folder":
|
package/src/types.ts
CHANGED
|
@@ -2128,8 +2128,20 @@ export type CdlPhase = {
|
|
|
2128
2128
|
* この phase で「左の起点から描き始める」 node の id (cdl#512)。
|
|
2129
2129
|
*
|
|
2130
2130
|
* `activate` と同じ形で node id を並べる。 対象の node は phase の進み (0→1) に合わせて
|
|
2131
|
-
* 描かれ、 進みが 1 に達した時点で全体が出る。
|
|
2132
|
-
*
|
|
2131
|
+
* 描かれ、 進みが 1 に達した時点で全体が出る。 起点は種別ごとに違う。
|
|
2132
|
+
*
|
|
2133
|
+
* | kind | 起点 | 動き | 添える値 |
|
|
2134
|
+
* |---|---|---|---|
|
|
2135
|
+
* | `chart-line` | 左端 | 線が右へ伸びる | 点と数字は線の先が届いた時 |
|
|
2136
|
+
* | `chart-bar` | 横軸 | 棒が上へ伸びる | 数字は棒の頭に付いて動く |
|
|
2137
|
+
* | `chart-pie` | 12 時 | 扇が時計回りに開く | 凡例はその扇が開き始めた時 |
|
|
2138
|
+
* | `journey-map` | 左端 | 気持ちの線が右へ伸びる | 段はその点に線の先が届いた時 |
|
|
2139
|
+
* | `mind-map` | 中心 | 枝が段ごとに外へ伸びる | 箱はその枝が届いた時 |
|
|
2140
|
+
* | `tree-hierarchy` | 根 | 枝が段ごとに下へ伸びる | 箱はその枝が届いた時 |
|
|
2141
|
+
* | `gantt-timeline` | 各帯の始端 | 帯が右へ伸びる | 依存の矢印は帯が出揃った時 |
|
|
2142
|
+
* | `funnel-stages` | 上端 | 段が上から順に積まれる | 件数と離脱率はその段が出切った時 |
|
|
2143
|
+
*
|
|
2144
|
+
* 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
|
|
2133
2145
|
*
|
|
2134
2146
|
* **`activate` を兼ねさせない**。 1 箱で図全体を描く種別 (`chart-*` 等) では焦点が
|
|
2135
2147
|
* その箱に解決されるため、 連続する phase で光り続ける図が実在する。 「光っていたら
|
package/src/validate.ts
CHANGED
|
@@ -5,6 +5,34 @@ import { nodeTemplateTexts } from "./render/template-fields";
|
|
|
5
5
|
import { derivedRefIds } from "./render/derived-values";
|
|
6
6
|
|
|
7
7
|
const TONES = new Set<string>(TONE_NAMES);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* phase の `draw` が効く kind (cdl#512 / cdl#516)。
|
|
11
|
+
*
|
|
12
|
+
* **一覧を 1 箇所で持つ**。 知らせの文面もここから組み立てる = 対応種別が増えた時に
|
|
13
|
+
* 片方だけ古くなる形を作らない。
|
|
14
|
+
*
|
|
15
|
+
* | kind | 起点 | 動き |
|
|
16
|
+
* |---|---|---|
|
|
17
|
+
* | `chart-line` | 左端 | 線が右へ伸びる |
|
|
18
|
+
* | `chart-bar` | 横軸 | 棒が上へ伸びる |
|
|
19
|
+
* | `chart-pie` | 12 時 | 扇が時計回りに開く |
|
|
20
|
+
* | `journey-map` | 左端 | 気持ちの線が右へ伸びる |
|
|
21
|
+
* | `mind-map` | 中心 | 枝が中心から外へ伸びる |
|
|
22
|
+
* | `tree-hierarchy` | 根 | 枝が上の段から下の段へ伸びる |
|
|
23
|
+
* | `gantt-timeline` | 各帯の始端 | 帯が右へ伸びる |
|
|
24
|
+
* | `funnel-stages` | 上端 | 段が上から順に積まれる |
|
|
25
|
+
*/
|
|
26
|
+
export const DRAW_KINDS: ReadonlySet<string> = new Set([
|
|
27
|
+
"chart-line",
|
|
28
|
+
"chart-bar",
|
|
29
|
+
"chart-pie",
|
|
30
|
+
"journey-map",
|
|
31
|
+
"mind-map",
|
|
32
|
+
"tree-hierarchy",
|
|
33
|
+
"gantt-timeline",
|
|
34
|
+
"funnel-stages",
|
|
35
|
+
]);
|
|
8
36
|
const KINDS = new Set<string>(NODE_KINDS);
|
|
9
37
|
const ENG_BANNED = /\b(FUNCTION|STORAGE|EVENT LOG|BALANCES MAPPING|READ|WRITE|EMIT|CALL)\b/;
|
|
10
38
|
|
|
@@ -166,8 +194,10 @@ export function validate(diag: CdlDiagram): void {
|
|
|
166
194
|
// 描き方を持たない種別を指しても図は出るが、 書いた指定は何もしない。
|
|
167
195
|
// 黙って捨てると「書いたのに伸びない」 が手掛かりなしで起きる
|
|
168
196
|
const kind = diag.nodes.find((n) => n.id === id)?.kind;
|
|
169
|
-
if (kind
|
|
170
|
-
warnings.push(
|
|
197
|
+
if (!DRAW_KINDS.has(String(kind))) {
|
|
198
|
+
warnings.push(
|
|
199
|
+
`phase "${p.id}" の draw "${id}" は kind "${kind}" で、 起点から描く動きを持たない (対象は ${[...DRAW_KINDS].join(" / ")})`,
|
|
200
|
+
);
|
|
171
201
|
}
|
|
172
202
|
}
|
|
173
203
|
for (const t of p.tweens) {
|