@cardenelabs/cdl 0.11.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.
@@ -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
- <circle cx={x1} cy={midY} r={2.5} fill={TONE.accent} fillOpacity={0.75} />
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: Array<{ x1: number; y1: number; x2: number; y2: number }>;
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
- const edges: Array<{ x1: number; y1: number; x2: number; y2: number }> = [];
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
- edges.push({ x1: parent.x, y1: parent.y + parentH / 2, x2: child.x, y2: child.y - childH / 2 });
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"
@@ -190,17 +190,57 @@ export function CdlNodeView({
190
190
  />
191
191
  );
192
192
  case "gantt-timeline":
193
- return <GanttNode node={resolvedNode} active={active} stateValues={stateValues} />;
193
+ return (
194
+ <GanttNode
195
+ node={resolvedNode}
196
+ active={active}
197
+ stateValues={stateValues}
198
+ drawing={drawing}
199
+ progress={progress}
200
+ />
201
+ );
194
202
  case "mind-map":
195
- return <MindMapNode node={resolvedNode} active={active} stateValues={stateValues} />;
203
+ return (
204
+ <MindMapNode
205
+ node={resolvedNode}
206
+ active={active}
207
+ stateValues={stateValues}
208
+ drawing={drawing}
209
+ progress={progress}
210
+ />
211
+ );
196
212
  case "funnel-stages":
197
- return <FunnelNode node={resolvedNode} active={active} stateValues={stateValues} />;
213
+ return (
214
+ <FunnelNode
215
+ node={resolvedNode}
216
+ active={active}
217
+ stateValues={stateValues}
218
+ drawing={drawing}
219
+ progress={progress}
220
+ />
221
+ );
198
222
  case "quadrant-matrix":
199
223
  return <QuadrantNode node={resolvedNode} active={active} stateValues={stateValues} />;
200
224
  case "tree-hierarchy":
201
- return <TreeHierarchyNode node={resolvedNode} active={active} stateValues={stateValues} />;
225
+ return (
226
+ <TreeHierarchyNode
227
+ node={resolvedNode}
228
+ active={active}
229
+ stateValues={stateValues}
230
+ drawing={drawing}
231
+ progress={progress}
232
+ />
233
+ );
202
234
  case "journey-map":
203
- return <UserJourneyNode node={resolvedNode} active={active} stateValues={stateValues} />;
235
+ return (
236
+ <UserJourneyNode
237
+ node={resolvedNode}
238
+ active={active}
239
+ stateValues={stateValues}
240
+ drawing={drawing}
241
+ progress={progress}
242
+ />
243
+ );
204
244
  case "shape-file":
205
245
  return <ShapeFileNode node={resolvedNode} active={active} />;
206
246
  case "shape-folder":
package/src/types.ts CHANGED
@@ -2135,6 +2135,11 @@ export type CdlPhase = {
2135
2135
  * | `chart-line` | 左端 | 線が右へ伸びる | 点と数字は線の先が届いた時 |
2136
2136
  * | `chart-bar` | 横軸 | 棒が上へ伸びる | 数字は棒の頭に付いて動く |
2137
2137
  * | `chart-pie` | 12 時 | 扇が時計回りに開く | 凡例はその扇が開き始めた時 |
2138
+ * | `journey-map` | 左端 | 気持ちの線が右へ伸びる | 段はその点に線の先が届いた時 |
2139
+ * | `mind-map` | 中心 | 枝が段ごとに外へ伸びる | 箱はその枝が届いた時 |
2140
+ * | `tree-hierarchy` | 根 | 枝が段ごとに下へ伸びる | 箱はその枝が届いた時 |
2141
+ * | `gantt-timeline` | 各帯の始端 | 帯が右へ伸びる | 依存の矢印は帯が出揃った時 |
2142
+ * | `funnel-stages` | 上端 | 段が上から順に積まれる | 件数と離脱率はその段が出切った時 |
2138
2143
  *
2139
2144
  * 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
2140
2145
  *
package/src/validate.ts CHANGED
@@ -17,8 +17,22 @@ const TONES = new Set<string>(TONE_NAMES);
17
17
  * | `chart-line` | 左端 | 線が右へ伸びる |
18
18
  * | `chart-bar` | 横軸 | 棒が上へ伸びる |
19
19
  * | `chart-pie` | 12 時 | 扇が時計回りに開く |
20
+ * | `journey-map` | 左端 | 気持ちの線が右へ伸びる |
21
+ * | `mind-map` | 中心 | 枝が中心から外へ伸びる |
22
+ * | `tree-hierarchy` | 根 | 枝が上の段から下の段へ伸びる |
23
+ * | `gantt-timeline` | 各帯の始端 | 帯が右へ伸びる |
24
+ * | `funnel-stages` | 上端 | 段が上から順に積まれる |
20
25
  */
21
- export const DRAW_KINDS: ReadonlySet<string> = new Set(["chart-line", "chart-bar", "chart-pie"]);
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
+ ]);
22
36
  const KINDS = new Set<string>(NODE_KINDS);
23
37
  const ENG_BANNED = /\b(FUNCTION|STORAGE|EVENT LOG|BALANCES MAPPING|READ|WRITE|EMIT|CALL)\b/;
24
38