@cardenelabs/cdl 0.11.0 → 0.12.1
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 +12 -2
- package/dist/index.cjs +361 -177
- 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 +361 -177
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +361 -177
- 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 +361 -177
- 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/box-lines.ts +12 -2
- 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 +70 -16
- package/src/kinds/tree.tsx +57 -7
- 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.1",
|
|
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/box-lines.ts
CHANGED
|
@@ -28,6 +28,16 @@ const 上下の余白 = 2;
|
|
|
28
28
|
/** 箱の左右に空ける余白 (片側) */
|
|
29
29
|
const 左右の余白 = 8;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* その箱で文字に使える幅。
|
|
33
|
+
*
|
|
34
|
+
* **余白を呼出側に写さない**。 写すと余白を変えた時に片方だけ古くなり、`箱に積む` を通る
|
|
35
|
+
* 経路と通らない経路で収まる幅が変わる (cdl#526 で 1 行だけの経路が幅を見ていなかった)。
|
|
36
|
+
*/
|
|
37
|
+
export function 使える幅(箱の幅: number): number {
|
|
38
|
+
return 箱の幅 - 左右の余白 * 2;
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
export type 積む行 = {
|
|
32
42
|
役割: "title" | "subtitle";
|
|
33
43
|
text: string;
|
|
@@ -109,12 +119,12 @@ export function 箱に積む(行: 積む行[], 箱の幅: number, 箱の高さ:
|
|
|
109
119
|
const 残す = [...候補];
|
|
110
120
|
while (残す.length > 1 && 高さ(残す) > 使える高さ) 残す.pop();
|
|
111
121
|
|
|
112
|
-
const
|
|
122
|
+
const 幅 = 使える幅(箱の幅);
|
|
113
123
|
const 総高 = 高さ(残す);
|
|
114
124
|
let 積み = -総高 / 2;
|
|
115
125
|
return 残す.map((r) => {
|
|
116
126
|
const dy = 積み + ((行送りの比 + 字の高さの比) / 2) * r.fontSize;
|
|
117
127
|
積み += r.fontSize * 行送りの比;
|
|
118
|
-
return { ...r, text: 幅で切る(r.text, r.fontSize,
|
|
128
|
+
return { ...r, text: 幅で切る(r.text, r.fontSize, 幅), dy };
|
|
119
129
|
});
|
|
120
130
|
}
|
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>
|