@cardenelabs/cdl 0.40.5 → 0.40.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.40.5",
3
+ "version": "0.40.7",
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",
@@ -3,9 +3,41 @@ import type { LaidNode } from "../types";
3
3
  import { resolveChartData } from "../render/payload-binding";
4
4
  import { 進みを畳む } from "./draw-progress";
5
5
 
6
+ /**
7
+ * 目盛りの刻みを、人が読む数から選ぶ (cdl#747)。
8
+ *
9
+ * `1 / 2 / 2.5 / 5` に 10 の累乗を掛けた中から、目盛りが目安の本数前後になるものを取る。
10
+ * 最大値をそのまま割ると `680 / 4 = 170` のような数が目盛りに出て、機械が割った数字に見える。
11
+ *
12
+ * 最大が 0 以下の図では刻みを決められないので 0 を返す (呼ぶ側が今までの割り方に落ちる)。
13
+ */
14
+ export function 目盛りの刻み(最大: number, 目安の本数: number): number {
15
+ if (!(最大 > 0) || !(目安の本数 > 0)) return 0;
16
+ const 粗い = 最大 / 目安の本数;
17
+ const 桁 = 10 ** Math.floor(Math.log10(粗い));
18
+ const 正規 = 粗い / 桁;
19
+ const 候補 = 正規 <= 1 ? 1 : 正規 <= 2 ? 2 : 正規 <= 2.5 ? 2.5 : 正規 <= 5 ? 5 : 10;
20
+ return 候補 * 桁;
21
+ }
22
+
23
+ /**
24
+ * その数を書き切るのに要る小数の桁数。
25
+ *
26
+ * `String(0.0000025)` は指数の形になり `indexOf(".")` では数えられないため、
27
+ * 仮数と指数に分けて数える (`2.5e-7` = 仮数の小数 1 桁 - 指数 -7 = 8 桁)。
28
+ */
29
+ function 小数の桁(n: number): number {
30
+ const [仮数, 指数] = n.toExponential().split("e");
31
+ const 小数部 = (仮数 ?? "").split(".")[1]?.length ?? 0;
32
+ return Math.min(20, Math.max(0, 小数部 - Number(指数)));
33
+ }
34
+
35
+ /** 刻みの倍数を足し込んだ時に出る誤差 (`0.1 * 3 = 0.30000000000000004`) を落とす */
36
+ const 桁で丸める = (v: number, 桁: number): number => Number(v.toFixed(桁));
37
+
6
38
  /**
7
39
  * chart-bar kind ... 1 node に datum 配列 (chartData) を保持し、 SVG rect で棒グラフ描画する。
8
- * layout は node bbox 内側の canvas に縦棒を等間隔配置、 縦軸 = value 0-max、 横軸 = datum label。
40
+ * layout は node bbox 内側の canvas に縦棒を等間隔配置、 縦軸 = value 0-天井、 横軸 = datum label。
9
41
  * 各 value は数でも `{signal}` でもよく、 後者は状態が動くたび棒の高さが追随する。
10
42
  *
11
43
  * phase の `draw` にこの node の id が載っている間は、 棒が横軸から上へ伸びる (cdl#516)。
@@ -32,7 +64,8 @@ export function ChartBarNode({
32
64
  const y0 = node.cy - node.h / 2;
33
65
  const data = resolveChartData(node.chartData ?? [], stateValues);
34
66
 
35
- const PAD_TOP = 20;
67
+ // 値札を枠に重ねない余白 (cdl#747)。 天井まで届いた棒の数字がここに収まる
68
+ const PAD_TOP = 34;
36
69
  const PAD_RIGHT = 24;
37
70
  const PAD_BOTTOM = 44;
38
71
  const PAD_LEFT = 60;
@@ -40,21 +73,55 @@ export function ChartBarNode({
40
73
  const canvasH = node.h - PAD_TOP - PAD_BOTTOM;
41
74
 
42
75
  const vMax = data.length > 0 ? Math.max(...data.map((d) => d.value)) : 1;
43
- const vRange = vMax || 1;
44
76
 
45
- const barGap = 10;
77
+ const 目盛りの目安 = 4;
78
+ /**
79
+ * 目盛りを引く元の高さ。 値が 0 以下しか無い図 (と値を読めない図) では 1 を使う =
80
+ * 最大値をそのまま使うと 0 が 5 つ並ぶ縦軸になり、何も読めない。
81
+ */
82
+ const 天井のもと = vMax > 0 ? vMax : 1;
83
+ const 刻み = 目盛りの刻み(天井のもと, 目盛りの目安);
84
+ const 刻みの桁 = 小数の桁(刻み);
85
+ /**
86
+ * 縦軸の天井。 **最大値をそのまま天井にしない** (cdl#747)。
87
+ * 刻みの倍数へ切り上げるので、目盛りが人の読む数になる。
88
+ *
89
+ * **天井を水増しして余白を作らない** = 最大 100 の図は `0 / 25 / 50 / 75 / 100` が
90
+ * 正しい形で、余白のために `125` へ伸ばすと目盛りが不自然になる。
91
+ * 値札が枠に重ならない余白は `PAD_TOP` が持つ。
92
+ */
93
+ const 天井 = 桁で丸める(Math.ceil(天井のもと / 刻み - 1e-9) * 刻み, 刻みの桁);
94
+
95
+ /** 棒 1 本に対する隙間の割合。 棒 : 隙間 = この比 (cdl#747) */
96
+ const 棒と隙間の比 = 2.2;
97
+ /** 棒の太さの上限。 本数が少ない図で棒が塀のように太るのを止める */
98
+ const 棒の上限 = 96;
46
99
  const barCount = data.length || 1;
47
- const barW = (canvasW - barGap * (barCount + 1)) / barCount;
100
+ // 隙間 1 つ分を単位に、棒 barCount 本 + 隙間 (barCount + 1) 個で canvas を割る
101
+ const 単位 = canvasW / (barCount * 棒と隙間の比 + (barCount + 1));
102
+ const barW = Math.max(0, Math.min(単位 * 棒と隙間の比, 棒の上限));
103
+ // 上限で余った幅は隙間へ均等に配る = 棒の並びが片寄らない
104
+ const barGap = (canvasW - barW * barCount) / (barCount + 1);
48
105
 
49
- const yAt = (v: number) => PAD_TOP + canvasH - (v / vRange) * canvasH;
106
+ const yAt = (v: number) => PAD_TOP + canvasH - (v / 天井) * canvasH;
50
107
  const xAt = (idx: number) => PAD_LEFT + barGap + idx * (barW + barGap);
51
108
 
52
- const gridCount = 4;
53
- const gridTicks = Array.from({ length: gridCount + 1 }, (_, i) => {
54
- const v = (vMax * i) / gridCount;
109
+ const 目盛りの本数 = Math.round(天井 / 刻み);
110
+ const gridTicks = Array.from({ length: 目盛りの本数 + 1 }, (_, i) => {
111
+ const v = 桁で丸める(刻み * i, 刻みの桁);
55
112
  return { y: yAt(v), value: v };
56
113
  });
57
114
 
115
+ /**
116
+ * 棒の濃さ。 **いちばん大きい棒だけ濃く出す** (cdl#747)。
117
+ *
118
+ * 色は 1 つのまま濃さだけを変える = 棒グラフは同じ量どうしの比較で、色は量を表さない。
119
+ * 4 本に 4 色を配ると「色に意味がある」 と誤読する余地が残る。
120
+ */
121
+ const 濃さ = (v: number): number => (vMax > 0 ? (v >= vMax ? 1 : 0.45) : 0.9);
122
+ /** 角丸は棒の太さに合わせる。 太い棒に固定値を当てると直角に見える */
123
+ const 角丸 = Math.min(barW * 0.06, 8);
124
+
58
125
  /** 棒がどこまで伸びたか (0..1、 cdl#516)。 畳み方は `draw-progress.ts` が持つ */
59
126
  const 伸び = 進みを畳む(drawing, progress);
60
127
  /** 棒の足元。 ここを動かさない点として上へ伸ばす */
@@ -86,13 +153,14 @@ export function ChartBarNode({
86
153
  opacity={0.65}
87
154
  />
88
155
  <text
156
+ data-cdl-role="chart-bar-tick"
89
157
  x={PAD_LEFT - 8}
90
158
  y={t.y + 4}
91
159
  fontSize={11}
92
160
  textAnchor="end"
93
161
  fill="var(--cdl-text-dim, #5a6270)"
94
162
  >
95
- {Math.round(t.value)}
163
+ {t.value}
96
164
  </text>
97
165
  </g>
98
166
  ))}
@@ -109,7 +177,8 @@ export function ChartBarNode({
109
177
  {data.map((d, idx) => {
110
178
  const bx = xAt(idx);
111
179
  const by = yAt(d.value);
112
- const bh = PAD_TOP + canvasH - by;
180
+ // 負の値は下へ伸ばす軸を持たないので足元で止める (`rect` に負の高さを書かない)
181
+ const bh = Math.max(0, PAD_TOP + canvasH - by);
113
182
  // 見えている棒の頭。 数字はここに付いて動く = 空いた場所に数字だけ浮かない
114
183
  const 見えている頭 = 横軸のy - (横軸のy - by) * 伸び;
115
184
  /**
@@ -124,9 +193,9 @@ export function ChartBarNode({
124
193
  y={by}
125
194
  width={barW}
126
195
  height={bh}
127
- rx={2}
196
+ rx={角丸}
128
197
  fill="var(--cdl-tone-accent, #2d6a8f)"
129
- fillOpacity={0.9}
198
+ fillOpacity={濃さ(d.value)}
130
199
  />
131
200
  );
132
201
  return (
@@ -139,19 +208,23 @@ export function ChartBarNode({
139
208
  ) : (
140
209
  棒
141
210
  )}
211
+ {/* 値のほうが読ませたい数。 横軸の名前より大きく太くする (cdl#747) */}
142
212
  <text
213
+ data-cdl-role="chart-bar-value"
143
214
  x={bx + barW / 2}
144
- y={見えている頭 - 6}
145
- fontSize={11}
215
+ y={見えている頭 - 8}
216
+ fontSize={12}
217
+ fontWeight={600}
146
218
  textAnchor="middle"
147
219
  fill="var(--cdl-text, #1a1f2a)"
148
220
  >
149
221
  {d.value.toLocaleString()}
150
222
  </text>
151
223
  <text
224
+ data-cdl-role="chart-bar-label"
152
225
  x={bx + barW / 2}
153
226
  y={PAD_TOP + canvasH + 18}
154
- fontSize={12}
227
+ fontSize={11}
155
228
  textAnchor="middle"
156
229
  fill="var(--cdl-text-dim, #5a6270)"
157
230
  >
package/src/validate.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CdlDiagram } from "./types";
1
+ import type { CdlDiagram, CdlNode } from "./types";
2
2
  import { TONES as TONE_NAMES, NODE_KINDS } from "./types";
3
3
  import { templateRefIds } from "./render/utils";
4
4
  import { nodeTemplateTexts } from "./render/template-fields";
@@ -46,6 +46,31 @@ export const DRAW_KINDS: ReadonlySet<string> = new Set([
46
46
  const KINDS = new Set<string>(NODE_KINDS);
47
47
  const ENG_BANNED = /\b(FUNCTION|STORAGE|EVENT LOG|BALANCES MAPPING|READ|WRITE|EMIT|CALL)\b/;
48
48
 
49
+ /**
50
+ * 図表の荷 (payload) を持つ節の欄 (cdl#743)。
51
+ *
52
+ * これらの欄を持つ節は **その節自体が図** で、段が指し示す相手ではない。 記法にも図表の節を
53
+ * activate する書き方が無いため、「どの phase でも activate されていない」 は書き手に直せない。
54
+ *
55
+ * **種別の一覧を持たない** = 図表の種別は増える (実測で 15 種)。 一覧を手で書くと、種別を
56
+ * 足した時にここへの追記を忘れて警告が復活する。 荷の欄で判定すれば自動で追従する
57
+ * (`rules/quality.md § 導出可能記述は人手で書かない` の経路 1)。
58
+ */
59
+ const 図表の荷の欄 = [
60
+ "chartData",
61
+ "ganttData",
62
+ "mindData",
63
+ "funnelData",
64
+ "quadrantData",
65
+ "treeData",
66
+ "journeyData",
67
+ ] as const satisfies readonly (keyof CdlNode)[];
68
+
69
+ /** 図表の荷を 1 つでも持つか。 持つ節は「使われていない」 の判定から外す */
70
+ function 図表の節(n: CdlNode): boolean {
71
+ return 図表の荷の欄.some((欄) => n[欄] !== undefined);
72
+ }
73
+
49
74
  /**
50
75
  * cdl が compile される前に通る validator。
51
76
  * 構造的破綻 (重複 id / 不存在 id 参照 / 必須要素 0 件 等) は throw、
@@ -263,7 +288,7 @@ export function validate(diag: CdlDiagram): void {
263
288
  for (const id of p.activate) allActivated.add(id);
264
289
  }
265
290
  for (const n of diag.nodes) {
266
- if (!allActivated.has(n.id)) {
291
+ if (!allActivated.has(n.id) && !図表の節(n)) {
267
292
  warnings.push(`node "${n.id}" がどの phase でも activate されていない (unused、 typo の可能性)`);
268
293
  }
269
294
  }