@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
|
@@ -2051,8 +2051,20 @@ type CdlPhase = {
|
|
|
2051
2051
|
* この phase で「左の起点から描き始める」 node の id (cdl#512)。
|
|
2052
2052
|
*
|
|
2053
2053
|
* `activate` と同じ形で node id を並べる。 対象の node は phase の進み (0→1) に合わせて
|
|
2054
|
-
* 描かれ、 進みが 1 に達した時点で全体が出る。
|
|
2055
|
-
*
|
|
2054
|
+
* 描かれ、 進みが 1 に達した時点で全体が出る。 起点は種別ごとに違う。
|
|
2055
|
+
*
|
|
2056
|
+
* | kind | 起点 | 動き | 添える値 |
|
|
2057
|
+
* |---|---|---|---|
|
|
2058
|
+
* | `chart-line` | 左端 | 線が右へ伸びる | 点と数字は線の先が届いた時 |
|
|
2059
|
+
* | `chart-bar` | 横軸 | 棒が上へ伸びる | 数字は棒の頭に付いて動く |
|
|
2060
|
+
* | `chart-pie` | 12 時 | 扇が時計回りに開く | 凡例はその扇が開き始めた時 |
|
|
2061
|
+
* | `journey-map` | 左端 | 気持ちの線が右へ伸びる | 段はその点に線の先が届いた時 |
|
|
2062
|
+
* | `mind-map` | 中心 | 枝が段ごとに外へ伸びる | 箱はその枝が届いた時 |
|
|
2063
|
+
* | `tree-hierarchy` | 根 | 枝が段ごとに下へ伸びる | 箱はその枝が届いた時 |
|
|
2064
|
+
* | `gantt-timeline` | 各帯の始端 | 帯が右へ伸びる | 依存の矢印は帯が出揃った時 |
|
|
2065
|
+
* | `funnel-stages` | 上端 | 段が上から順に積まれる | 件数と離脱率はその段が出切った時 |
|
|
2066
|
+
*
|
|
2067
|
+
* 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
|
|
2056
2068
|
*
|
|
2057
2069
|
* **`activate` を兼ねさせない**。 1 箱で図全体を描く種別 (`chart-*` 等) では焦点が
|
|
2058
2070
|
* その箱に解決されるため、 連続する phase で光り続ける図が実在する。 「光っていたら
|
|
@@ -2051,8 +2051,20 @@ type CdlPhase = {
|
|
|
2051
2051
|
* この phase で「左の起点から描き始める」 node の id (cdl#512)。
|
|
2052
2052
|
*
|
|
2053
2053
|
* `activate` と同じ形で node id を並べる。 対象の node は phase の進み (0→1) に合わせて
|
|
2054
|
-
* 描かれ、 進みが 1 に達した時点で全体が出る。
|
|
2055
|
-
*
|
|
2054
|
+
* 描かれ、 進みが 1 に達した時点で全体が出る。 起点は種別ごとに違う。
|
|
2055
|
+
*
|
|
2056
|
+
* | kind | 起点 | 動き | 添える値 |
|
|
2057
|
+
* |---|---|---|---|
|
|
2058
|
+
* | `chart-line` | 左端 | 線が右へ伸びる | 点と数字は線の先が届いた時 |
|
|
2059
|
+
* | `chart-bar` | 横軸 | 棒が上へ伸びる | 数字は棒の頭に付いて動く |
|
|
2060
|
+
* | `chart-pie` | 12 時 | 扇が時計回りに開く | 凡例はその扇が開き始めた時 |
|
|
2061
|
+
* | `journey-map` | 左端 | 気持ちの線が右へ伸びる | 段はその点に線の先が届いた時 |
|
|
2062
|
+
* | `mind-map` | 中心 | 枝が段ごとに外へ伸びる | 箱はその枝が届いた時 |
|
|
2063
|
+
* | `tree-hierarchy` | 根 | 枝が段ごとに下へ伸びる | 箱はその枝が届いた時 |
|
|
2064
|
+
* | `gantt-timeline` | 各帯の始端 | 帯が右へ伸びる | 依存の矢印は帯が出揃った時 |
|
|
2065
|
+
* | `funnel-stages` | 上端 | 段が上から順に積まれる | 件数と離脱率はその段が出切った時 |
|
|
2066
|
+
*
|
|
2067
|
+
* 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
|
|
2056
2068
|
*
|
|
2057
2069
|
* **`activate` を兼ねさせない**。 1 箱で図全体を描く種別 (`chart-*` 等) では焦点が
|
|
2058
2070
|
* その箱に解決されるため、 連続する phase で光り続ける図が実在する。 「光っていたら
|
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,20 +1,32 @@
|
|
|
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 で棒グラフ描画する。
|
|
7
8
|
* layout は node bbox 内側の canvas に縦棒を等間隔配置、 縦軸 = value 0-max、 横軸 = datum label。
|
|
8
9
|
* 各 value は数でも `{signal}` でもよく、 後者は状態が動くたび棒の高さが追随する。
|
|
10
|
+
*
|
|
11
|
+
* phase の `draw` にこの node の id が載っている間は、 棒が横軸から上へ伸びる (cdl#516)。
|
|
12
|
+
* 伸ばすのは棒を包む `<g>` の `transform` で、 `rect` の `x` / `y` / `width` / `height` は
|
|
13
|
+
* 進みに関わらず実値のまま。 値の数字は棒の頭に付いて動く。
|
|
14
|
+
* 軸 / 格子 / 横軸の名前は最初から出る。
|
|
9
15
|
*/
|
|
10
16
|
export function ChartBarNode({
|
|
11
17
|
node,
|
|
12
18
|
active,
|
|
13
19
|
stateValues = {},
|
|
20
|
+
drawing = false,
|
|
21
|
+
progress = 1,
|
|
14
22
|
}: {
|
|
15
23
|
node: LaidNode;
|
|
16
24
|
active: boolean;
|
|
17
25
|
stateValues?: Record<string, string>;
|
|
26
|
+
/** この phase の `draw` に載っているか (cdl#516) */
|
|
27
|
+
drawing?: boolean;
|
|
28
|
+
/** phase の進み (0→1)。 `drawing` が true の時だけ読む */
|
|
29
|
+
progress?: number;
|
|
18
30
|
}): JSX.Element {
|
|
19
31
|
const x0 = node.cx - node.w / 2;
|
|
20
32
|
const y0 = node.cy - node.h / 2;
|
|
@@ -43,6 +55,11 @@ export function ChartBarNode({
|
|
|
43
55
|
return { y: yAt(v), value: v };
|
|
44
56
|
});
|
|
45
57
|
|
|
58
|
+
/** 棒がどこまで伸びたか (0..1、 cdl#516)。 畳み方は `draw-progress.ts` が持つ */
|
|
59
|
+
const 伸び = 進みを畳む(drawing, progress);
|
|
60
|
+
/** 棒の足元。 ここを動かさない点として上へ伸ばす */
|
|
61
|
+
const 横軸のy = PAD_TOP + canvasH;
|
|
62
|
+
|
|
46
63
|
return (
|
|
47
64
|
<g transform={`translate(${x0} ${y0})`}>
|
|
48
65
|
<rect
|
|
@@ -93,22 +110,38 @@ export function ChartBarNode({
|
|
|
93
110
|
const bx = xAt(idx);
|
|
94
111
|
const by = yAt(d.value);
|
|
95
112
|
const bh = PAD_TOP + canvasH - by;
|
|
113
|
+
// 見えている棒の頭。 数字はここに付いて動く = 空いた場所に数字だけ浮かない
|
|
114
|
+
const 見えている頭 = 横軸のy - (横軸のy - by) * 伸び;
|
|
115
|
+
/**
|
|
116
|
+
* **`rect` の属性は触らない** (cdl#516)。 高さを直接動かすと、棒の高さで値を測る
|
|
117
|
+
* 利用側の検査が進みの途中で別の値を読む。 足元を動かさない点にした倍率で包む。
|
|
118
|
+
*/
|
|
119
|
+
const 棒 = (
|
|
120
|
+
<rect
|
|
121
|
+
data-cdl-role="chart-bar"
|
|
122
|
+
data-cdl-unresolved={d.unresolved ? "true" : undefined}
|
|
123
|
+
x={bx}
|
|
124
|
+
y={by}
|
|
125
|
+
width={barW}
|
|
126
|
+
height={bh}
|
|
127
|
+
rx={2}
|
|
128
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
129
|
+
fillOpacity={0.9}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
96
132
|
return (
|
|
97
133
|
<g key={`bar-${idx}`}>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
107
|
-
fillOpacity={0.9}
|
|
108
|
-
/>
|
|
134
|
+
{/* 描く指定が無い図には包みを足さない = 既存の図の DOM を 1 要素も変えない */}
|
|
135
|
+
{drawing ? (
|
|
136
|
+
<g transform={`translate(0 ${横軸のy}) scale(1 ${伸び}) translate(0 ${-横軸のy})`}>
|
|
137
|
+
{棒}
|
|
138
|
+
</g>
|
|
139
|
+
) : (
|
|
140
|
+
棒
|
|
141
|
+
)}
|
|
109
142
|
<text
|
|
110
143
|
x={bx + barW / 2}
|
|
111
|
-
y={
|
|
144
|
+
y={見えている頭 - 6}
|
|
112
145
|
fontSize={11}
|
|
113
146
|
textAnchor="middle"
|
|
114
147
|
fill="var(--cdl-text, #1a1f2a)"
|
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
|
@@ -1,22 +1,36 @@
|
|
|
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 { 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 で円グラフ描画する。
|
|
8
9
|
* layout は node bbox 内側の左半分に円を配置、 右半分に凡例 (label + %) を stack 表示する。
|
|
9
10
|
* 各 value は数でも `{signal}` でもよく、 後者は状態が動くたび扇の角度が追随する。
|
|
11
|
+
*
|
|
12
|
+
* phase の `draw` にこの node の id が載っている間は、 円が 12 時から時計回りに開く (cdl#516)。
|
|
13
|
+
* 開き方は扇形の切り抜き (`clipPath`) で、 各扇の `d` は進みに関わらず実値のまま。
|
|
14
|
+
* 凡例はその扇が開き始めた時に出る。
|
|
10
15
|
*/
|
|
11
16
|
export function ChartPieNode({
|
|
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
|
+
/** この phase の `draw` に載っているか (cdl#516) */
|
|
27
|
+
drawing?: boolean;
|
|
28
|
+
/** phase の進み (0→1)。 `drawing` が true の時だけ読む */
|
|
29
|
+
progress?: number;
|
|
19
30
|
}): JSX.Element {
|
|
31
|
+
// node id は diagram 内だけで一意。 同じ page に複数 diagram がある時も SVG の参照先が
|
|
32
|
+
// ぶつからないよう、 React が component instance ごとに割り当てる id を使う。
|
|
33
|
+
const instanceId = useId().replace(/[^A-Za-z0-9_-]/g, "");
|
|
20
34
|
const x0 = node.cx - node.w / 2;
|
|
21
35
|
const y0 = node.cy - node.h / 2;
|
|
22
36
|
const data = resolveChartData(node.chartData ?? [], stateValues);
|
|
@@ -31,12 +45,16 @@ export function ChartPieNode({
|
|
|
31
45
|
const cy = PAD_TOP + chartAreaH / 2;
|
|
32
46
|
|
|
33
47
|
let cumAngle = -Math.PI / 2;
|
|
48
|
+
/** その扇が始まるまでの取り分の累計。 開き始めたかの判定に使う (cdl#516) */
|
|
49
|
+
let cumFrac = 0;
|
|
34
50
|
const slices = data.map((d) => {
|
|
35
51
|
const frac = d.value / total;
|
|
36
52
|
const angle = frac * Math.PI * 2;
|
|
37
53
|
const a0 = cumAngle;
|
|
38
54
|
const a1 = cumAngle + angle;
|
|
39
55
|
cumAngle = a1;
|
|
56
|
+
const startFrac = cumFrac;
|
|
57
|
+
cumFrac += frac;
|
|
40
58
|
|
|
41
59
|
const x1 = cx + radius * Math.cos(a0);
|
|
42
60
|
const y1 = cy + radius * Math.sin(a0);
|
|
@@ -44,9 +62,16 @@ export function ChartPieNode({
|
|
|
44
62
|
const y2 = cy + radius * Math.sin(a1);
|
|
45
63
|
const large = angle > Math.PI ? 1 : 0;
|
|
46
64
|
const path = `M ${cx} ${cy} L ${x1} ${y1} A ${radius} ${radius} 0 ${large} 1 ${x2} ${y2} Z`;
|
|
47
|
-
return { d, frac, path };
|
|
65
|
+
return { d, frac, path, startFrac };
|
|
48
66
|
});
|
|
49
67
|
|
|
68
|
+
/** 円がどこまで開いたか (0..1、 cdl#516)。 畳み方は `draw-progress.ts` が持つ */
|
|
69
|
+
const 開き = 進みを畳む(drawing, progress);
|
|
70
|
+
// SVG の id は document 全体の名前空間。 node id ではなく instance id で分ける
|
|
71
|
+
const 切り抜きid = `cdl-pie-draw-${instanceId}`;
|
|
72
|
+
/** その扇が開き始めたか。 開き始めた扇の凡例だけを出す */
|
|
73
|
+
const 開き始めた = (startFrac: number): boolean => !drawing || 開き > startFrac;
|
|
74
|
+
|
|
50
75
|
const legendX = pieAreaW + 8;
|
|
51
76
|
const legendGap = Math.min(28, chartAreaH / Math.max(data.length, 1));
|
|
52
77
|
const legendStartY = PAD_TOP + (chartAreaH - legendGap * data.length) / 2 + legendGap / 2;
|
|
@@ -64,20 +89,40 @@ export function ChartPieNode({
|
|
|
64
89
|
stroke={active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)"}
|
|
65
90
|
strokeWidth={active ? 3 : 1.25}
|
|
66
91
|
/>
|
|
67
|
-
{
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
))}
|
|
92
|
+
{drawing && (
|
|
93
|
+
<defs>
|
|
94
|
+
<clipPath id={切り抜きid}>{扇形の切り抜き(cx, cy, radius + 2, 開き)}</clipPath>
|
|
95
|
+
</defs>
|
|
96
|
+
)}
|
|
97
|
+
{/*
|
|
98
|
+
**扇の `d` は触らない** (cdl#516)。 進みで再計算すると、扇の形から取り分を測る
|
|
99
|
+
利用側が進みの途中で別の値を読む。 見える範囲だけを切り抜きで変える。
|
|
100
|
+
|
|
101
|
+
描く指定が無い図には包みも `clip-path` も足さない = 既存の図の DOM を変えない
|
|
102
|
+
*/}
|
|
103
|
+
<g {...(drawing ? { clipPath: `url(#${切り抜きid})` } : {})}>
|
|
104
|
+
{slices.map((s, idx) => (
|
|
105
|
+
<path
|
|
106
|
+
key={`slice-${idx}`}
|
|
107
|
+
data-cdl-role="chart-pie-slice"
|
|
108
|
+
data-cdl-unresolved={s.d.unresolved ? "true" : undefined}
|
|
109
|
+
d={s.path}
|
|
110
|
+
fill={sliceColor(s.d.tone, idx)}
|
|
111
|
+
fillOpacity={active ? 0.95 : 0.85}
|
|
112
|
+
stroke="var(--cdl-node-fill, #ffffff)"
|
|
113
|
+
strokeWidth={1.5}
|
|
114
|
+
/>
|
|
115
|
+
))}
|
|
116
|
+
</g>
|
|
79
117
|
|
|
80
|
-
{
|
|
118
|
+
{/*
|
|
119
|
+
行の位置は `idx` から決まる。 いまの開き方は必ず先頭からの連続なので、開いた扇だけを
|
|
120
|
+
`filter` で抜いても位置は同じになる (変異試験で実測 = 差が出ない)。
|
|
121
|
+
|
|
122
|
+
それでも `idx` を保つ形で書くのは、位置の計算が抜き方に依存しないため。 開く順が
|
|
123
|
+
先頭からでなくなった時に、この場所を直し忘れても位置がずれない
|
|
124
|
+
*/}
|
|
125
|
+
{slices.map((s, idx) => !開き始めた(s.startFrac) ? null : (
|
|
81
126
|
<g key={`legend-${idx}`} transform={`translate(${legendX} ${legendStartY + idx * legendGap})`}>
|
|
82
127
|
<rect
|
|
83
128
|
x={0}
|
|
@@ -110,6 +155,22 @@ export function ChartPieNode({
|
|
|
110
155
|
}
|
|
111
156
|
|
|
112
157
|
|
|
158
|
+
/**
|
|
159
|
+
* 12 時から時計回りに開く扇形 (cdl#516)。 円を切り抜く形に使う。
|
|
160
|
+
*
|
|
161
|
+
* 1 周ちょうどは扇形で表せない (始点と終点が同じ点になり、 弧の向きが決まらない)。
|
|
162
|
+
* その時だけ円をそのまま返す = 進み 1 で図が消える形を避ける。
|
|
163
|
+
*/
|
|
164
|
+
function 扇形の切り抜き(cx: number, cy: number, r: number, 開き: number): JSX.Element | null {
|
|
165
|
+
if (開き <= 0) return null;
|
|
166
|
+
if (開き >= 1) return <circle cx={cx} cy={cy} r={r} />;
|
|
167
|
+
const a0 = -Math.PI / 2;
|
|
168
|
+
const a1 = a0 + 開き * Math.PI * 2;
|
|
169
|
+
const large = 開き > 0.5 ? 1 : 0;
|
|
170
|
+
const d = `M ${cx} ${cy} L ${cx + r * Math.cos(a0)} ${cy + r * Math.sin(a0)} A ${r} ${r} 0 ${large} 1 ${cx + r * Math.cos(a1)} ${cy + r * Math.sin(a1)} Z`;
|
|
171
|
+
return <path d={d} />;
|
|
172
|
+
}
|
|
173
|
+
|
|
113
174
|
const FALLBACK_ORDER: Tone[] = ["accent", "teal", "warning", "success", "info", "error"];
|
|
114
175
|
|
|
115
176
|
function sliceColor(tone: Tone | undefined, idx: number): string {
|
|
@@ -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
|
}
|