@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/CHANGELOG.md +86 -1
- package/dist/index.cjs +54 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +54 -22
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +54 -22
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +54 -22
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/src/kinds/axis-ticks.ts +32 -0
- package/src/kinds/chart-bar.tsx +84 -16
- package/src/kinds/chart-line.tsx +24 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardenelabs/cdl",
|
|
3
|
-
"version": "0.40.
|
|
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
|
+
}
|
package/src/kinds/chart-bar.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 /
|
|
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
|
|
53
|
-
const gridTicks = Array.from({ length:
|
|
54
|
-
const v = (
|
|
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
|
-
{
|
|
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
|
-
|
|
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={
|
|
191
|
+
rx={角丸}
|
|
128
192
|
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
129
|
-
fillOpacity={
|
|
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={見えている頭 -
|
|
145
|
-
fontSize={
|
|
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={
|
|
222
|
+
fontSize={軸の名前の級}
|
|
155
223
|
textAnchor="middle"
|
|
156
224
|
fill="var(--cdl-text-dim, #5a6270)"
|
|
157
225
|
>
|
package/src/kinds/chart-line.tsx
CHANGED
|
@@ -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
|
-
|
|
21
|
-
const
|
|
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={
|
|
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={
|
|
330
|
+
fontSize={軸の名前の級}
|
|
315
331
|
textAnchor="middle"
|
|
316
332
|
fill="var(--cdl-text-dim, #5a6270)"
|
|
317
333
|
>
|