@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.
- package/CHANGELOG.md +46 -1
- package/SPEC.md +12 -2
- package/dist/index.cjs +319 -147
- 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 +319 -147
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +319 -147
- 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 +319 -147
- package/dist/react.js.map +1 -1
- package/dist/{render-DwH8v5Mb.d.cts → render-C7FKvMqq.d.cts} +5 -0
- package/dist/{render-DwH8v5Mb.d.ts → render-C7FKvMqq.d.ts} +5 -0
- package/package.json +1 -1
- package/src/kinds/chart-bar.tsx +3 -7
- package/src/kinds/chart-line.tsx +4 -10
- package/src/kinds/chart-pie.tsx +3 -7
- 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 +45 -5
- package/src/types.ts +5 -0
- package/src/validate.ts +15 -1
|
@@ -2058,6 +2058,11 @@ type CdlPhase = {
|
|
|
2058
2058
|
* | `chart-line` | 左端 | 線が右へ伸びる | 点と数字は線の先が届いた時 |
|
|
2059
2059
|
* | `chart-bar` | 横軸 | 棒が上へ伸びる | 数字は棒の頭に付いて動く |
|
|
2060
2060
|
* | `chart-pie` | 12 時 | 扇が時計回りに開く | 凡例はその扇が開き始めた時 |
|
|
2061
|
+
* | `journey-map` | 左端 | 気持ちの線が右へ伸びる | 段はその点に線の先が届いた時 |
|
|
2062
|
+
* | `mind-map` | 中心 | 枝が段ごとに外へ伸びる | 箱はその枝が届いた時 |
|
|
2063
|
+
* | `tree-hierarchy` | 根 | 枝が段ごとに下へ伸びる | 箱はその枝が届いた時 |
|
|
2064
|
+
* | `gantt-timeline` | 各帯の始端 | 帯が右へ伸びる | 依存の矢印は帯が出揃った時 |
|
|
2065
|
+
* | `funnel-stages` | 上端 | 段が上から順に積まれる | 件数と離脱率はその段が出切った時 |
|
|
2061
2066
|
*
|
|
2062
2067
|
* 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
|
|
2063
2068
|
*
|
|
@@ -2058,6 +2058,11 @@ type CdlPhase = {
|
|
|
2058
2058
|
* | `chart-line` | 左端 | 線が右へ伸びる | 点と数字は線の先が届いた時 |
|
|
2059
2059
|
* | `chart-bar` | 横軸 | 棒が上へ伸びる | 数字は棒の頭に付いて動く |
|
|
2060
2060
|
* | `chart-pie` | 12 時 | 扇が時計回りに開く | 凡例はその扇が開き始めた時 |
|
|
2061
|
+
* | `journey-map` | 左端 | 気持ちの線が右へ伸びる | 段はその点に線の先が届いた時 |
|
|
2062
|
+
* | `mind-map` | 中心 | 枝が段ごとに外へ伸びる | 箱はその枝が届いた時 |
|
|
2063
|
+
* | `tree-hierarchy` | 根 | 枝が段ごとに下へ伸びる | 箱はその枝が届いた時 |
|
|
2064
|
+
* | `gantt-timeline` | 各帯の始端 | 帯が右へ伸びる | 依存の矢印は帯が出揃った時 |
|
|
2065
|
+
* | `funnel-stages` | 上端 | 段が上から順に積まれる | 件数と離脱率はその段が出切った時 |
|
|
2061
2066
|
*
|
|
2062
2067
|
* 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
|
|
2063
2068
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardenelabs/cdl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
package/src/kinds/chart-bar.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
2
|
import type { LaidNode } from "../types";
|
|
3
3
|
import { resolveChartData } from "../render/payload-binding";
|
|
4
|
+
import { 進みを畳む } from "./draw-progress";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* chart-bar kind ... 1 node に datum 配列 (chartData) を保持し、 SVG rect で棒グラフ描画する。
|
|
@@ -54,13 +55,8 @@ export function ChartBarNode({
|
|
|
54
55
|
return { y: yAt(v), value: v };
|
|
55
56
|
});
|
|
56
57
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
*
|
|
60
|
-
* 進みは呼出側から来る値なので範囲の外 (負 / 1 超 / NaN) が入りうる。 そのまま倍率に
|
|
61
|
-
* 渡すと棒が上下反転したり突き抜けたりするため、 ここで畳む。
|
|
62
|
-
*/
|
|
63
|
-
const 伸び = drawing ? Math.min(1, Math.max(0, Number.isFinite(progress) ? progress : 1)) : 1;
|
|
58
|
+
/** 棒がどこまで伸びたか (0..1、 cdl#516)。 畳み方は `draw-progress.ts` が持つ */
|
|
59
|
+
const 伸び = 進みを畳む(drawing, progress);
|
|
64
60
|
/** 棒の足元。 ここを動かさない点として上へ伸ばす */
|
|
65
61
|
const 横軸のy = PAD_TOP + canvasH;
|
|
66
62
|
|
package/src/kinds/chart-line.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
2
|
import type { LaidNode } from "../types";
|
|
3
3
|
import { resolveChartData } from "../render/payload-binding";
|
|
4
|
+
import { 進みを畳む, 伸ばすdash } from "./draw-progress";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* chart-line kind ... 1 node に datum 配列 (chartData) を保持し、 SVG polyline で折れ線描画する。
|
|
@@ -73,13 +74,8 @@ export function ChartLineNode({
|
|
|
73
74
|
前の点 = 今の点;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
*
|
|
79
|
-
* 進みは呼出側から来る値なので、 範囲の外 (負 / 1 超 / NaN) が入りうる。 そのまま
|
|
80
|
-
* `strokeDashoffset` に渡すと線が消えたり逆向きに縮んだりするため、 ここで畳む。
|
|
81
|
-
*/
|
|
82
|
-
const 伸び = drawing ? Math.min(1, Math.max(0, Number.isFinite(progress) ? progress : 1)) : 1;
|
|
77
|
+
/** 線がどこまで伸びたか (0..1、 cdl#512)。 畳み方は `draw-progress.ts` が持つ */
|
|
78
|
+
const 伸び = 進みを畳む(drawing, progress);
|
|
83
79
|
|
|
84
80
|
/**
|
|
85
81
|
* その点まで線の先が届いたか。
|
|
@@ -173,9 +169,7 @@ export function ChartLineNode({
|
|
|
173
169
|
// 測れないため。 長さを 1 に正規化すれば進みをそのまま offset に使える。
|
|
174
170
|
//
|
|
175
171
|
// 描く指定が無い図には属性を 1 つも足さない = 既存の図の DOM を変えない
|
|
176
|
-
{
|
|
177
|
-
? { pathLength: 1, strokeDasharray: 1, strokeDashoffset: 1 - 伸び }
|
|
178
|
-
: {})}
|
|
172
|
+
{...伸ばすdash(drawing, 伸び)}
|
|
179
173
|
/>
|
|
180
174
|
)}
|
|
181
175
|
{data.map((d, idx) => {
|
package/src/kinds/chart-pie.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useId, type JSX } from "react";
|
|
|
2
2
|
import type { LaidNode, Tone } from "../types";
|
|
3
3
|
import { TONE } from "../render/tone";
|
|
4
4
|
import { resolveChartData } from "../render/payload-binding";
|
|
5
|
+
import { 進みを畳む } from "./draw-progress";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* chart-pie kind ... 1 node に datum 配列 (chartData) を保持し、 SVG path arc で円グラフ描画する。
|
|
@@ -64,13 +65,8 @@ export function ChartPieNode({
|
|
|
64
65
|
return { d, frac, path, startFrac };
|
|
65
66
|
});
|
|
66
67
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
*
|
|
70
|
-
* 進みは呼出側から来る値なので範囲の外 (負 / 1 超 / NaN) が入りうる。 そのまま角度に
|
|
71
|
-
* 掛けると 1 周を超えて切り抜きが壊れるため、 ここで畳む。
|
|
72
|
-
*/
|
|
73
|
-
const 開き = drawing ? Math.min(1, Math.max(0, Number.isFinite(progress) ? progress : 1)) : 1;
|
|
68
|
+
/** 円がどこまで開いたか (0..1、 cdl#516)。 畳み方は `draw-progress.ts` が持つ */
|
|
69
|
+
const 開き = 進みを畳む(drawing, progress);
|
|
74
70
|
// SVG の id は document 全体の名前空間。 node id ではなく instance id で分ける
|
|
75
71
|
const 切り抜きid = `cdl-pie-draw-${instanceId}`;
|
|
76
72
|
/** その扇が開き始めたか。 開き始めた扇の凡例だけを出す */
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 段の `draw` で図形を起点から現す時の、共通の計算 (cdl#520)。
|
|
3
|
+
*
|
|
4
|
+
* `chart-line` (#513) と `chart-bar` / `chart-pie` (#517) が同じ形を各 file に持っていた。
|
|
5
|
+
* 対応種別が 6 つに増えるため、**進みの畳み方と dash の当て方を 1 箇所に集める**。
|
|
6
|
+
*
|
|
7
|
+
* ここに置くのは「どの図形にも同じ意味で効く計算」 だけ。 起点の取り方 (左端 / 横軸 /
|
|
8
|
+
* 12 時 / 根) は種別ごとに違うため各 file が持つ。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 進みを 0..1 に畳む。 描く指定が無ければ常に 1 = 全体が出ている。
|
|
13
|
+
*
|
|
14
|
+
* 進みは呼出側から来る値なので範囲の外 (負 / 1 超 / NaN) が入りうる。 そのまま倍率や
|
|
15
|
+
* 角度に渡すと、図形が反転したり 1 周を超えて切り抜きが壊れたりする。
|
|
16
|
+
*/
|
|
17
|
+
export function 進みを畳む(drawing: boolean, progress: number): number {
|
|
18
|
+
if (!drawing) return 1;
|
|
19
|
+
return Math.min(1, Math.max(0, Number.isFinite(progress) ? progress : 1));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `path` を起点から伸ばす dash の属性。 描く指定が無い時は **何も返さない**。
|
|
24
|
+
*
|
|
25
|
+
* `pathLength` を 1 に正規化するため、進みをそのまま残り (`strokeDashoffset`) に使える。
|
|
26
|
+
* 実長を測るには DOM の ref が要り、静的な書き出しでは測れない。
|
|
27
|
+
*
|
|
28
|
+
* **`d` は触らない**。 経路を切り詰めると、形から位置や量を測る利用側が進みの途中で
|
|
29
|
+
* 別の値を読む (#513 で `points` について決めたのと同じ)。
|
|
30
|
+
*/
|
|
31
|
+
export function 伸ばすdash(
|
|
32
|
+
drawing: boolean,
|
|
33
|
+
伸び: number,
|
|
34
|
+
): { pathLength: number; strokeDasharray: number; strokeDashoffset: number } | Record<string, never> {
|
|
35
|
+
if (!drawing) return {};
|
|
36
|
+
return { pathLength: 1, strokeDasharray: 1, strokeDashoffset: 1 - 伸び };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 段 (深さ) ごとに区切った進み (cdl#520)。
|
|
41
|
+
*
|
|
42
|
+
* 枝が複数ある図 (`mind-map` / `tree-hierarchy`) で、全部を同時に伸ばすと離れた枝が
|
|
43
|
+
* 宙に浮いたまま伸びる。 **根 / 中心に近い段から順に伸ばす**。
|
|
44
|
+
*
|
|
45
|
+
* 深さ 1..`最大深さ` を等分し、その段の区間に入ってから出るまでを 0→1 で返す。
|
|
46
|
+
* 区間より手前なら 0、通り過ぎていれば 1。
|
|
47
|
+
*
|
|
48
|
+
* 経路長で順を決める案は採らない。 枝は別々の `path` なので、累積経路長で並べると
|
|
49
|
+
* 1 本ずつ順に伸びる形になり、放射状の図では育ち方が読めなくなる (深さで区切ると
|
|
50
|
+
* 同じ世代が一緒に伸びる)。
|
|
51
|
+
*/
|
|
52
|
+
export function 段の伸び(伸び: number, 深さ: number, 最大深さ: number): number {
|
|
53
|
+
if (最大深さ <= 0 || 深さ <= 0) return 1;
|
|
54
|
+
const 幅 = 1 / 最大深さ;
|
|
55
|
+
const 始まり = (深さ - 1) * 幅;
|
|
56
|
+
return Math.min(1, Math.max(0, (伸び - 始まり) / 幅));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 3 次ベジエでつないだ折れ線の、各点までの累積割合 (cdl#520)。
|
|
61
|
+
*
|
|
62
|
+
* 返すのは 0..1 の並びで、先頭は必ず 0、末尾は必ず 1。 点が 1 つ以下なら `[0]` 相当。
|
|
63
|
+
*
|
|
64
|
+
* **点番号の比率では代用できない**。 dash は実際の経路長で進むため、区間の長さが違うと
|
|
65
|
+
* 先端と点がずれる (#513 で実測、急傾斜から始まる並びで 2 点目が線の先より先に出た)。
|
|
66
|
+
*
|
|
67
|
+
* 曲線の長さは折れ線で近似する。 区間を `分割数` で刻んで弦の長さを足す = 実長との差は
|
|
68
|
+
* 0.1% 未満 (区間 16 分割、実測)。 `getTotalLength()` は DOM の ref が要り、静的な
|
|
69
|
+
* 書き出しでは測れないため使わない。
|
|
70
|
+
*/
|
|
71
|
+
export function ベジエの累積割合(
|
|
72
|
+
点: ReadonlyArray<{ x: number; y: number }>,
|
|
73
|
+
制御点: (前: { x: number; y: number }, 後: { x: number; y: number }) => [
|
|
74
|
+
{ x: number; y: number },
|
|
75
|
+
{ x: number; y: number },
|
|
76
|
+
],
|
|
77
|
+
分割数 = 16,
|
|
78
|
+
): number[] {
|
|
79
|
+
if (点.length <= 1) return 点.map(() => 0);
|
|
80
|
+
const 長さ: number[] = [0];
|
|
81
|
+
let 合計 = 0;
|
|
82
|
+
for (let i = 1; i < 点.length; i++) {
|
|
83
|
+
const p0 = 点[i - 1]!;
|
|
84
|
+
const p3 = 点[i]!;
|
|
85
|
+
const [p1, p2] = 制御点(p0, p3);
|
|
86
|
+
let 前 = p0;
|
|
87
|
+
for (let s = 1; s <= 分割数; s++) {
|
|
88
|
+
const t = s / 分割数;
|
|
89
|
+
const u = 1 - t;
|
|
90
|
+
const x = u * u * u * p0.x + 3 * u * u * t * p1.x + 3 * u * t * t * p2.x + t * t * t * p3.x;
|
|
91
|
+
const y = u * u * u * p0.y + 3 * u * u * t * p1.y + 3 * u * t * t * p2.y + t * t * t * p3.y;
|
|
92
|
+
合計 += Math.hypot(x - 前.x, y - 前.y);
|
|
93
|
+
前 = { x, y };
|
|
94
|
+
}
|
|
95
|
+
長さ.push(合計);
|
|
96
|
+
}
|
|
97
|
+
// 全長 0 (同じ点が並ぶ) では割れない。 その時は全点を起点扱いにする
|
|
98
|
+
if (合計 === 0) return 点.map(() => 0);
|
|
99
|
+
return 長さ.map((l) => l / 合計);
|
|
100
|
+
}
|
package/src/kinds/funnel.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useId, type JSX } from "react";
|
|
2
2
|
import type { LaidNode, Tone } from "../types";
|
|
3
3
|
import { TONE } from "../render/tone";
|
|
4
4
|
import { resolveFunnelData } from "../render/payload-binding";
|
|
5
|
+
import { 進みを畳む } from "./draw-progress";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* funnel-stages kind ... 1 node に stage 配列 (funnelData) を保持し、 逆三角形 funnel を SVG 描画する。
|
|
@@ -14,10 +15,16 @@ export function FunnelNode({
|
|
|
14
15
|
node,
|
|
15
16
|
active,
|
|
16
17
|
stateValues = {},
|
|
18
|
+
drawing = false,
|
|
19
|
+
progress = 1,
|
|
17
20
|
}: {
|
|
18
21
|
node: LaidNode;
|
|
19
22
|
active: boolean;
|
|
20
23
|
stateValues?: Record<string, string>;
|
|
24
|
+
/** この phase の `draw` に載っているか (cdl#521) */
|
|
25
|
+
drawing?: boolean;
|
|
26
|
+
/** phase の進み (0→1)。 `drawing` が true の時だけ読む */
|
|
27
|
+
progress?: number;
|
|
21
28
|
}): JSX.Element {
|
|
22
29
|
const x0 = node.cx - node.w / 2;
|
|
23
30
|
const y0 = node.cy - node.h / 2;
|
|
@@ -35,6 +42,25 @@ export function FunnelNode({
|
|
|
35
42
|
const gap = 4;
|
|
36
43
|
const stageH = (canvasH - gap * (stageCount - 1)) / stageCount;
|
|
37
44
|
|
|
45
|
+
/**
|
|
46
|
+
* 段がどこまで積まれたか (0..1、 cdl#521)。 描く指定が無ければ常に 1 = 全部出ている。
|
|
47
|
+
*
|
|
48
|
+
* 上から下へ流れる図なので、切り抜きを上から広げると段が順に現れる。
|
|
49
|
+
*/
|
|
50
|
+
const 伸び = 進みを畳む(drawing, progress);
|
|
51
|
+
// SVG の id は document 全体の名前空間。 node id は図の中でだけ一意なので instance id で分ける
|
|
52
|
+
const 切り抜きid = `cdl-funnel-draw-${useId().replace(/[^A-Za-z0-9_-]/g, "")}`;
|
|
53
|
+
/**
|
|
54
|
+
* その段の下端までの割合。 **高さの累計で測る** (件数の比ではない)。
|
|
55
|
+
* 段の高さは件数に比例しないため、件数で測ると出る順が図と合わない。
|
|
56
|
+
*/
|
|
57
|
+
const 段の下端の割合 = (idx: number): number => {
|
|
58
|
+
if (canvasH <= 0) return 0;
|
|
59
|
+
return ((idx + 1) * stageH + idx * gap) / canvasH;
|
|
60
|
+
};
|
|
61
|
+
/** その段が出切ったか。 出切ってから件数と離脱率を出す */
|
|
62
|
+
const 段が出切った = (idx: number): boolean => !drawing || 伸び >= 段の下端の割合(idx);
|
|
63
|
+
|
|
38
64
|
const widthAtStep = (step: number): number => {
|
|
39
65
|
const topRatio = 1.0;
|
|
40
66
|
const bottomRatio = 0.35;
|
|
@@ -42,6 +68,67 @@ export function FunnelNode({
|
|
|
42
68
|
return canvasW * (topRatio + (bottomRatio - topRatio) * t);
|
|
43
69
|
};
|
|
44
70
|
|
|
71
|
+
const 段 = stages.map((s, idx) => {
|
|
72
|
+
const prev = idx > 0 ? stages[idx - 1]! : null;
|
|
73
|
+
// 前段が 0 件なら離脱率は決まらない (0 で割ることになる)。 静止画では起きなかったが、
|
|
74
|
+
// count が状態を読むようになると動き始めの 0 で必ず通る。
|
|
75
|
+
const dropRate = prev && prev.count > 0 ? ((prev.count - s.count) / prev.count) * 100 : 0;
|
|
76
|
+
const y = PAD_TOP + idx * (stageH + gap);
|
|
77
|
+
const topW = widthAtStep(idx);
|
|
78
|
+
const bottomW = widthAtStep(idx + 1);
|
|
79
|
+
const cx = PAD_LEFT + canvasW / 2;
|
|
80
|
+
const topX = cx - topW / 2;
|
|
81
|
+
const bottomX = cx - bottomW / 2;
|
|
82
|
+
const points = `${topX},${y} ${topX + topW},${y} ${bottomX + bottomW},${y + stageH} ${bottomX},${y + stageH}`;
|
|
83
|
+
const tone = toneByIndex(idx);
|
|
84
|
+
const legendX = PAD_LEFT + canvasW + 24;
|
|
85
|
+
return (
|
|
86
|
+
<g key={`stage-${s.id}`}>
|
|
87
|
+
<polygon
|
|
88
|
+
data-cdl-role="funnel-stage"
|
|
89
|
+
data-cdl-unresolved={s.unresolved ? "true" : undefined}
|
|
90
|
+
points={points}
|
|
91
|
+
fill={tone}
|
|
92
|
+
fillOpacity={0.9}
|
|
93
|
+
stroke={tone}
|
|
94
|
+
strokeWidth={0}
|
|
95
|
+
/>
|
|
96
|
+
<text
|
|
97
|
+
x={cx}
|
|
98
|
+
y={y + stageH / 2 + 5}
|
|
99
|
+
fontSize={14}
|
|
100
|
+
fontWeight={700}
|
|
101
|
+
textAnchor="middle"
|
|
102
|
+
fill="var(--cdl-node-fill, #ffffff)"
|
|
103
|
+
>
|
|
104
|
+
{s.title}
|
|
105
|
+
</text>
|
|
106
|
+
{/* 件数と離脱率は段が出切ってから。 切り抜きだけでは途中から見えるため自分で判定する */}
|
|
107
|
+
{段が出切った(idx) && (
|
|
108
|
+
<text
|
|
109
|
+
x={legendX}
|
|
110
|
+
y={y + stageH / 2 - 2}
|
|
111
|
+
fontSize={13}
|
|
112
|
+
fontWeight={700}
|
|
113
|
+
fill="var(--cdl-text, #1a1f2a)"
|
|
114
|
+
>
|
|
115
|
+
{s.count.toLocaleString()}
|
|
116
|
+
</text>
|
|
117
|
+
)}
|
|
118
|
+
{段が出切った(idx) && prev && (
|
|
119
|
+
<text
|
|
120
|
+
x={legendX}
|
|
121
|
+
y={y + stageH / 2 + 14}
|
|
122
|
+
fontSize={11}
|
|
123
|
+
fill={dropRate > 0 ? "var(--cdl-text-dim, #5a6270)" : "var(--cdl-text-mute, #8a8678)"}
|
|
124
|
+
>
|
|
125
|
+
{dropRate > 0 ? `↓ ${dropRate.toFixed(1)}%` : "±0%"}
|
|
126
|
+
</text>
|
|
127
|
+
)}
|
|
128
|
+
</g>
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
45
132
|
return (
|
|
46
133
|
<g transform={`translate(${x0} ${y0})`}>
|
|
47
134
|
<rect
|
|
@@ -56,63 +143,19 @@ export function FunnelNode({
|
|
|
56
143
|
strokeWidth={active ? 3 : 1.25}
|
|
57
144
|
/>
|
|
58
145
|
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const legendX = PAD_LEFT + canvasW + 24;
|
|
73
|
-
return (
|
|
74
|
-
<g key={`stage-${s.id}`}>
|
|
75
|
-
<polygon
|
|
76
|
-
data-cdl-role="funnel-stage"
|
|
77
|
-
data-cdl-unresolved={s.unresolved ? "true" : undefined}
|
|
78
|
-
points={points}
|
|
79
|
-
fill={tone}
|
|
80
|
-
fillOpacity={0.9}
|
|
81
|
-
stroke={tone}
|
|
82
|
-
strokeWidth={0}
|
|
83
|
-
/>
|
|
84
|
-
<text
|
|
85
|
-
x={cx}
|
|
86
|
-
y={y + stageH / 2 + 5}
|
|
87
|
-
fontSize={14}
|
|
88
|
-
fontWeight={700}
|
|
89
|
-
textAnchor="middle"
|
|
90
|
-
fill="var(--cdl-node-fill, #ffffff)"
|
|
91
|
-
>
|
|
92
|
-
{s.title}
|
|
93
|
-
</text>
|
|
94
|
-
<text
|
|
95
|
-
x={legendX}
|
|
96
|
-
y={y + stageH / 2 - 2}
|
|
97
|
-
fontSize={13}
|
|
98
|
-
fontWeight={700}
|
|
99
|
-
fill="var(--cdl-text, #1a1f2a)"
|
|
100
|
-
>
|
|
101
|
-
{s.count.toLocaleString()}
|
|
102
|
-
</text>
|
|
103
|
-
{prev && (
|
|
104
|
-
<text
|
|
105
|
-
x={legendX}
|
|
106
|
-
y={y + stageH / 2 + 14}
|
|
107
|
-
fontSize={11}
|
|
108
|
-
fill={dropRate > 0 ? "var(--cdl-text-dim, #5a6270)" : "var(--cdl-text-mute, #8a8678)"}
|
|
109
|
-
>
|
|
110
|
-
{dropRate > 0 ? `↓ ${dropRate.toFixed(1)}%` : "±0%"}
|
|
111
|
-
</text>
|
|
112
|
-
)}
|
|
113
|
-
</g>
|
|
114
|
-
);
|
|
115
|
-
})}
|
|
146
|
+
{drawing && (
|
|
147
|
+
<defs>
|
|
148
|
+
{/*
|
|
149
|
+
上から広がる切り抜き。 **`points` は触らない** (cdl#521) = 台形の形から
|
|
150
|
+
件数を測る利用側が、進みの途中で別の値を読まないようにする
|
|
151
|
+
*/}
|
|
152
|
+
<clipPath id={切り抜きid}>
|
|
153
|
+
<rect x={0} y={0} width={node.w} height={PAD_TOP + canvasH * 伸び} />
|
|
154
|
+
</clipPath>
|
|
155
|
+
</defs>
|
|
156
|
+
)}
|
|
157
|
+
{/* 描く指定が無い図には包みを足さず、従来の DOM 構造を保つ */}
|
|
158
|
+
{drawing ? <g clipPath={`url(#${切り抜きid})`}>{段}</g> : 段}
|
|
116
159
|
</g>
|
|
117
160
|
);
|
|
118
161
|
}
|
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
|
|