@cardenelabs/cdl 0.40.6 → 0.40.8

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.6",
3
+ "version": "0.40.8",
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",
@@ -0,0 +1,32 @@
1
+ /**
2
+ * 目盛りの刻みを、人が読む数から選ぶ (cdl#751)。
3
+ *
4
+ * `1 / 2 / 2.5 / 5` に 10 の累乗を掛けた中から、目盛りが目安の本数前後になるものを取る。
5
+ * 範囲をそのまま本数で割ると `680 / 4 = 170` のような数が目盛りに出て、
6
+ * 機械が割った数字に見える。
7
+ *
8
+ * **棒と折れ線が同じ規則を持つのでここに 1 つだけ置く**。 図ごとに書くと、片方だけ直した
9
+ * 時に目盛りの出方がずれる (実際に棒を直した cdl#747 で 2 箇所になった)。
10
+ * 図どうしに上下関係を作らないため、どちらの図でもない場所に置く。
11
+ *
12
+ * **天井と床の決め方はここに持たない**。 折れ線は床も動かし、棒は床が必ず 0 で、
13
+ * 規則が違う。 共有するのは刻みの選び方だけ。
14
+ */
15
+
16
+ /**
17
+ * 刻みを 1 つ選ぶ。
18
+ *
19
+ * @param 範囲 縦軸が受け持つ幅。 折れ線は `最大 - 最小`、棒は最小が 0 なので `最大`
20
+ * @param 目安の本数 目盛りを何本くらい引きたいか
21
+ * @returns 刻み。 **範囲か本数が 0 以下なら 0 を返す** = 刻みを決められないことを
22
+ * 0 で表し、呼ぶ側がその図に合った逃げ方を選ぶ (`rules/quality.md` の
23
+ * 「判定できなかったことを値に潰さない」 に合わせ、勝手に 1 を返さない)
24
+ */
25
+ export function 目盛りの刻み(範囲: number, 目安の本数: number): number {
26
+ if (!(範囲 > 0) || !(目安の本数 > 0)) return 0;
27
+ const 粗い = 範囲 / 目安の本数;
28
+ const 桁 = 10 ** Math.floor(Math.log10(粗い));
29
+ const 正規 = 粗い / 桁;
30
+ const 候補 = 正規 <= 1 ? 1 : 正規 <= 2 ? 2 : 正規 <= 2.5 ? 2.5 : 正規 <= 5 ? 5 : 10;
31
+ return 候補 * 桁;
32
+ }
@@ -2,10 +2,36 @@ import type { JSX } from "react";
2
2
  import type { LaidNode } from "../types";
3
3
  import { resolveChartData } from "../render/payload-binding";
4
4
  import { 進みを畳む } from "./draw-progress";
5
+ import { 目盛りの刻み } from "./axis-ticks";
6
+
7
+ /**
8
+ * その数を書き切るのに要る小数の桁数。
9
+ *
10
+ * `String(0.0000025)` は指数の形になり `indexOf(".")` では数えられないため、
11
+ * 仮数と指数に分けて数える (`2.5e-7` = 仮数の小数 1 桁 - 指数 -7 = 8 桁)。
12
+ */
13
+ function 小数の桁(n: number): number {
14
+ const [仮数, 指数] = n.toExponential().split("e");
15
+ const 小数部 = (仮数 ?? "").split(".")[1]?.length ?? 0;
16
+ return Math.min(20, Math.max(0, 小数部 - Number(指数)));
17
+ }
18
+
19
+ /**
20
+ * 字の大きさ (cdl#753)。
21
+ *
22
+ * **値札を軸の名前より大きくする** = 値札は棒の上に載る数そのもので、
23
+ * 軸の名前は目盛りの側にある。 折れ線 (`chart-line.tsx`) と同じ組にしてある。
24
+ * 2 図で揃っていることは検査が実物から読んで確かめる (`chart-label-rank.test.tsx`)。
25
+ */
26
+ const 値札の級 = 12;
27
+ const 軸の名前の級 = 11;
28
+
29
+ /** 刻みの倍数を足し込んだ時に出る誤差 (`0.1 * 3 = 0.30000000000000004`) を落とす */
30
+ const 桁で丸める = (v: number, 桁: number): number => Number(v.toFixed(桁));
5
31
 
6
32
  /**
7
33
  * chart-bar kind ... 1 node に datum 配列 (chartData) を保持し、 SVG rect で棒グラフ描画する。
8
- * layout は node bbox 内側の canvas に縦棒を等間隔配置、 縦軸 = value 0-max、 横軸 = datum label。
34
+ * layout は node bbox 内側の canvas に縦棒を等間隔配置、 縦軸 = value 0-天井、 横軸 = datum label。
9
35
  * 各 value は数でも `{signal}` でもよく、 後者は状態が動くたび棒の高さが追随する。
10
36
  *
11
37
  * phase の `draw` にこの node の id が載っている間は、 棒が横軸から上へ伸びる (cdl#516)。
@@ -32,7 +58,8 @@ export function ChartBarNode({
32
58
  const y0 = node.cy - node.h / 2;
33
59
  const data = resolveChartData(node.chartData ?? [], stateValues);
34
60
 
35
- const PAD_TOP = 20;
61
+ // 値札を枠に重ねない余白 (cdl#747)。 天井まで届いた棒の数字がここに収まる
62
+ const PAD_TOP = 34;
36
63
  const PAD_RIGHT = 24;
37
64
  const PAD_BOTTOM = 44;
38
65
  const PAD_LEFT = 60;
@@ -40,21 +67,56 @@ export function ChartBarNode({
40
67
  const canvasH = node.h - PAD_TOP - PAD_BOTTOM;
41
68
 
42
69
  const vMax = data.length > 0 ? Math.max(...data.map((d) => d.value)) : 1;
43
- const vRange = vMax || 1;
44
70
 
45
- const barGap = 10;
71
+ const 目盛りの目安 = 4;
72
+ /**
73
+ * 目盛りを引く元の高さ。 値が 0 以下しか無い図 (と値を読めない図) では 1 を使う =
74
+ * 最大値をそのまま使うと 0 が 5 つ並ぶ縦軸になり、何も読めない。
75
+ */
76
+ const 天井のもと = vMax > 0 ? vMax : 1;
77
+ // 棒は床が必ず 0 なので、範囲 = 天井のもと
78
+ const 刻み = 目盛りの刻み(天井のもと, 目盛りの目安);
79
+ const 刻みの桁 = 小数の桁(刻み);
80
+ /**
81
+ * 縦軸の天井。 **最大値をそのまま天井にしない** (cdl#747)。
82
+ * 刻みの倍数へ切り上げるので、目盛りが人の読む数になる。
83
+ *
84
+ * **天井を水増しして余白を作らない** = 最大 100 の図は `0 / 25 / 50 / 75 / 100` が
85
+ * 正しい形で、余白のために `125` へ伸ばすと目盛りが不自然になる。
86
+ * 値札が枠に重ならない余白は `PAD_TOP` が持つ。
87
+ */
88
+ const 天井 = 桁で丸める(Math.ceil(天井のもと / 刻み - 1e-9) * 刻み, 刻みの桁);
89
+
90
+ /** 棒 1 本に対する隙間の割合。 棒 : 隙間 = この比 (cdl#747) */
91
+ const 棒と隙間の比 = 2.2;
92
+ /** 棒の太さの上限。 本数が少ない図で棒が塀のように太るのを止める */
93
+ const 棒の上限 = 96;
46
94
  const barCount = data.length || 1;
47
- const barW = (canvasW - barGap * (barCount + 1)) / barCount;
95
+ // 隙間 1 つ分を単位に、棒 barCount 本 + 隙間 (barCount + 1) 個で canvas を割る
96
+ const 単位 = canvasW / (barCount * 棒と隙間の比 + (barCount + 1));
97
+ const barW = Math.max(0, Math.min(単位 * 棒と隙間の比, 棒の上限));
98
+ // 上限で余った幅は隙間へ均等に配る = 棒の並びが片寄らない
99
+ const barGap = (canvasW - barW * barCount) / (barCount + 1);
48
100
 
49
- const yAt = (v: number) => PAD_TOP + canvasH - (v / vRange) * canvasH;
101
+ const yAt = (v: number) => PAD_TOP + canvasH - (v / 天井) * canvasH;
50
102
  const xAt = (idx: number) => PAD_LEFT + barGap + idx * (barW + barGap);
51
103
 
52
- const gridCount = 4;
53
- const gridTicks = Array.from({ length: gridCount + 1 }, (_, i) => {
54
- const v = (vMax * i) / gridCount;
104
+ const 目盛りの本数 = Math.round(天井 / 刻み);
105
+ const gridTicks = Array.from({ length: 目盛りの本数 + 1 }, (_, i) => {
106
+ const v = 桁で丸める(刻み * i, 刻みの桁);
55
107
  return { y: yAt(v), value: v };
56
108
  });
57
109
 
110
+ /**
111
+ * 棒の濃さ。 **いちばん大きい棒だけ濃く出す** (cdl#747)。
112
+ *
113
+ * 色は 1 つのまま濃さだけを変える = 棒グラフは同じ量どうしの比較で、色は量を表さない。
114
+ * 4 本に 4 色を配ると「色に意味がある」 と誤読する余地が残る。
115
+ */
116
+ const 濃さ = (v: number): number => (vMax > 0 ? (v >= vMax ? 1 : 0.45) : 0.9);
117
+ /** 角丸は棒の太さに合わせる。 太い棒に固定値を当てると直角に見える */
118
+ const 角丸 = Math.min(barW * 0.06, 8);
119
+
58
120
  /** 棒がどこまで伸びたか (0..1、 cdl#516)。 畳み方は `draw-progress.ts` が持つ */
59
121
  const 伸び = 進みを畳む(drawing, progress);
60
122
  /** 棒の足元。 ここを動かさない点として上へ伸ばす */
@@ -86,13 +148,14 @@ export function ChartBarNode({
86
148
  opacity={0.65}
87
149
  />
88
150
  <text
151
+ data-cdl-role="chart-bar-tick"
89
152
  x={PAD_LEFT - 8}
90
153
  y={t.y + 4}
91
154
  fontSize={11}
92
155
  textAnchor="end"
93
156
  fill="var(--cdl-text-dim, #5a6270)"
94
157
  >
95
- {Math.round(t.value)}
158
+ {t.value}
96
159
  </text>
97
160
  </g>
98
161
  ))}
@@ -109,7 +172,8 @@ export function ChartBarNode({
109
172
  {data.map((d, idx) => {
110
173
  const bx = xAt(idx);
111
174
  const by = yAt(d.value);
112
- const bh = PAD_TOP + canvasH - by;
175
+ // 負の値は下へ伸ばす軸を持たないので足元で止める (`rect` に負の高さを書かない)
176
+ const bh = Math.max(0, PAD_TOP + canvasH - by);
113
177
  // 見えている棒の頭。 数字はここに付いて動く = 空いた場所に数字だけ浮かない
114
178
  const 見えている頭 = 横軸のy - (横軸のy - by) * 伸び;
115
179
  /**
@@ -124,9 +188,9 @@ export function ChartBarNode({
124
188
  y={by}
125
189
  width={barW}
126
190
  height={bh}
127
- rx={2}
191
+ rx={角丸}
128
192
  fill="var(--cdl-tone-accent, #2d6a8f)"
129
- fillOpacity={0.9}
193
+ fillOpacity={濃さ(d.value)}
130
194
  />
131
195
  );
132
196
  return (
@@ -139,19 +203,23 @@ export function ChartBarNode({
139
203
  ) : (
140
204
  棒
141
205
  )}
206
+ {/* 値のほうが読ませたい数。 横軸の名前より大きく太くする (cdl#747) */}
142
207
  <text
208
+ data-cdl-role="chart-bar-value"
143
209
  x={bx + barW / 2}
144
- y={見えている頭 - 6}
145
- fontSize={11}
210
+ y={見えている頭 - 8}
211
+ fontSize={値札の級}
212
+ fontWeight={600}
146
213
  textAnchor="middle"
147
214
  fill="var(--cdl-text, #1a1f2a)"
148
215
  >
149
216
  {d.value.toLocaleString()}
150
217
  </text>
151
218
  <text
219
+ data-cdl-role="chart-bar-label"
152
220
  x={bx + barW / 2}
153
221
  y={PAD_TOP + canvasH + 18}
154
- fontSize={12}
222
+ fontSize={軸の名前の級}
155
223
  textAnchor="middle"
156
224
  fill="var(--cdl-text-dim, #5a6270)"
157
225
  >
@@ -2,9 +2,24 @@ import type { JSX } from "react";
2
2
  import type { LaidNode } from "../types";
3
3
  import { resolveChartData } from "../render/payload-binding";
4
4
  import { 進みを畳む, 伸ばすdash } from "./draw-progress";
5
+ import { 目盛りの刻み } from "./axis-ticks";
5
6
 
6
7
  export type ChartLineScale = { min: number; max: number; ticks: number[] };
7
8
 
9
+ /** 縦軸に引きたい目盛りの本数の目安 */
10
+ const 目盛りの目安 = 4;
11
+
12
+ /**
13
+ * 字の大きさ (cdl#753)。
14
+ *
15
+ * **値札を軸の名前より大きくする** = 値札は印の上に載る数そのもので、
16
+ * 軸の名前は目盛りの側にある。 棒グラフ (`chart-bar.tsx`) と同じ組にしてある。
17
+ * 凡例を持つ図 (半円ゲージ / 同心の弧 / 100 個の印 / 内訳の帯) は
18
+ * 名前が行を見分ける鍵になるので逆の順で、そちらは揃えない。
19
+ */
20
+ const 値札の級 = 12;
21
+ const 軸の名前の級 = 11;
22
+
8
23
  const 数を書く = (value: number): string =>
9
24
  value.toLocaleString("en-US", { maximumFractionDigits: 12 });
10
25
 
@@ -17,14 +32,13 @@ export function 折れ線の目盛(values: readonly number[]): ChartLineScale {
17
32
  rawMax += room;
18
33
  }
19
34
 
20
- const roughStep = (rawMax - rawMin) / 4;
21
- const power = 10 ** Math.floor(Math.log10(roughStep));
22
- const fraction = roughStep / power;
23
- const niceFraction =
24
- fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10;
25
- const step = niceFraction * power;
35
+ // 折れ線は床も動かすので、範囲 = 最大 - 最小。 直前の広げ方で必ず正になる
36
+ const step = 目盛りの刻み(rawMax - rawMin, 目盛りの目安);
26
37
  const min = Math.floor(rawMin / step) * step;
27
38
  const max = Math.ceil(rawMax / step) * step;
39
+ // 刻みが決まらない入力 (値が数でない) では `tickCount` が数にならず、
40
+ // 目盛りは空になる。 **この空を守りの分岐で作らない** = 同じものを 2 通りで作ると、
41
+ // 片方を外しても検査が落ちず、要るのか要らないのか判らなくなる (cdl#751 の変異試験で実測)
28
42
  const tickCount = Math.round((max - min) / step);
29
43
  const ticks = Array.from({ length: tickCount + 1 }, (_, i) => {
30
44
  const value = min + step * i;
@@ -165,6 +179,7 @@ export function ChartLineNode({
165
179
  opacity={0.65}
166
180
  />
167
181
  <text
182
+ data-cdl-role="chart-line-tick"
168
183
  x={PAD_LEFT - 8}
169
184
  y={t.y + 4}
170
185
  fontSize={11}
@@ -254,7 +269,7 @@ export function ChartLineNode({
254
269
  data-cdl-role="chart-line-value"
255
270
  x={cx}
256
271
  y={labelY}
257
- fontSize={11}
272
+ fontSize={値札の級}
258
273
  fontWeight={600}
259
274
  textAnchor="middle"
260
275
  fill="var(--cdl-text, #1a1f2a)"
@@ -309,9 +324,10 @@ export function ChartLineNode({
309
324
  <g key={`pt-${idx}`} data-cdl-unresolved={d.unresolved ? "true" : undefined}>
310
325
  {点と値}
311
326
  <text
327
+ data-cdl-role="chart-line-label"
312
328
  x={cx}
313
329
  y={PAD_TOP + canvasH + 20}
314
- fontSize={12}
330
+ fontSize={軸の名前の級}
315
331
  textAnchor="middle"
316
332
  fill="var(--cdl-text-dim, #5a6270)"
317
333
  >