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