@cardenelabs/cdl 0.40.4 → 0.40.5

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.4",
3
+ "version": "0.40.5",
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",
@@ -86,21 +86,85 @@ export function ChartGaugeNode({
86
86
  * 級を下げて 1 行に収める案は採らない = 件数 12 では級 7.7 が要り、この repo が
87
87
  * 他の図で使っている下限の 9 を下回る。
88
88
  */
89
- const 内訳の数値 = data.map((d) => `${実値表記(d.value)} ${割合表記(d.value / total)}`);
89
+ /** 内訳 1 件の数値の行 (実値と割合) */
90
+ const 数値の行 = (d: { value: number }): string =>
91
+ `${実値表記(d.value)} ${割合表記(d.value / total)}`;
90
92
 
91
93
  /**
92
- * 内訳 1 件が横に要る幅。 数値の行と色の印の広い方に、区画どうしの間を足す。
94
+ * 件の並びから、1 行に置ける件数と段数を出す。
93
95
  *
94
- * **印の幅が広い方になる入力は組めない** = 数値の行は最も短い形 (`1 50%`) でも級 13 で
96
+ * **印が広い方になる入力は組めない** = 数値の行は最も短い形 (`1 50%`) でも級 13 で
95
97
  * 幅 56.7 あり、印の 30 を必ず上回る。 件数 0 の図で `Math.max` の引数が空になるのを
96
98
  * 防ぐ下限として置いている。 万一この関係が崩れても印は区画に合わせて縮む (下の `Math.min`)。
97
99
  */
98
- const 件に要る幅 = Math.max(印の幅, ...内訳の数値.map((t) => textWidth(t, 数値の級))) + 区画の間;
99
- const 行の件数 = Math.min(
100
- Math.max(1, Math.floor(使える幅 / Math.max(件に要る幅, 1))),
101
- Math.max(data.length, 1),
100
+ const 割り付ける = (件たち: typeof data): { 行の件数: number; 段数: number } => {
101
+ const 幅 = Math.max(印の幅, ...件たち.map((d) => textWidth(数値の行(d), 数値の級))) + 区画の間;
102
+ const 行の件数 = Math.min(
103
+ Math.max(1, Math.floor(使える幅 / Math.max(幅, 1))),
104
+ Math.max(件たち.length, 1),
105
+ );
106
+ return { 行の件数, 段数: Math.ceil(Math.max(件たち.length, 1) / 行の件数) };
107
+ };
108
+
109
+ /**
110
+ * 段の数の上限 (cdl#735)。
111
+ *
112
+ * 段が増えると弧に残る高さが尽き、**合計の字が内訳の名前より小さくなる**。 この種別は
113
+ * 合計を大きく見せるために作ったので、主役が入れ替わると図の意味が変わる
114
+ * (実測で件数 22 から合計 13 に対し名前 14、件数 30 では合計が弧の穴から 9.3 はみ出した)。
115
+ *
116
+ * 弧に残す高さを先に確保し、そこから置ける段数を逆算する。 下限 48 は
117
+ * 「合計の級が名前の級 14 を上回る」 ことから決めた = 縦から決まる級は半径の 0.3224 倍
118
+ * (内半径 0.62 × 0.52) なので、14 を超えるには半径 43.4 が要る。
119
+ */
120
+ const 弧に残す高さ = 48;
121
+ const 置ける段数 = Math.max(
122
+ 1,
123
+ Math.floor((使える高さ - 弧に残す高さ - 弧と内訳の間) / 段の高さ),
102
124
  );
103
- const 内訳の段数 = Math.ceil(Math.max(data.length, 1) / 行の件数);
125
+
126
+ /**
127
+ * 入り切らない件を末尾 1 件に畳む。 合計は変えない = 畳んだ件の値を足して持たせる。
128
+ *
129
+ * 弧も同じ並びで描く = 内訳と扇が食い違うと、どの色がどの件かを読めなくなる。
130
+ */
131
+ const 畳む = (残す件数: number): typeof data => {
132
+ if (残す件数 >= data.length) return data;
133
+ const 前 = data.slice(0, Math.max(0, 残す件数 - 1));
134
+ const 残り = data.slice(Math.max(0, 残す件数 - 1));
135
+ const 値 = 残り.reduce((acc, d) => acc + d.value, 0);
136
+ // 前の時点の値は畳めない (どの件のものか決まらない) ので落とす。
137
+ // 読めなかった値が 1 件でもあれば、畳んだ件も読めなかった扱いにする
138
+ const 読めない = 残り.some((d) => d.unresolved);
139
+ const 畳んだ: (typeof data)[number] = {
140
+ ...残り[0]!,
141
+ label: "その他",
142
+ value: 値,
143
+ previous: undefined,
144
+ ...(読めない ? { unresolved: true as const } : { unresolved: undefined }),
145
+ };
146
+ return [...前, 畳んだ];
147
+ };
148
+
149
+ /**
150
+ * 段が上限に収まるまで畳む。
151
+ *
152
+ * 畳むと数値の行が変わるので、1 行に置ける件数も変わりうる。 畳んだ結果で割り付け直し、
153
+ * それでも収まらなければもう一度畳む。 **元の並びから畳む** = 畳んだ件を再び畳むと
154
+ * 「その他」 が入れ子になる。
155
+ */
156
+ const 出す件たち = ((): typeof data => {
157
+ let 件たち = data;
158
+ for (let i = 0; i < 4; i++) {
159
+ const { 行の件数, 段数 } = 割り付ける(件たち);
160
+ if (段数 <= 置ける段数) return 件たち;
161
+ 件たち = 畳む(Math.max(1, 置ける段数 * 行の件数));
162
+ }
163
+ return 件たち;
164
+ })();
165
+
166
+ const 内訳の数値 = 出す件たち.map((d) => 数値の行(d));
167
+ const { 行の件数, 段数: 内訳の段数 } = 割り付ける(出す件たち);
104
168
  const 内訳の高さ = 段の高さ * 内訳の段数;
105
169
 
106
170
  /**
@@ -119,7 +183,7 @@ export function ChartGaugeNode({
119
183
  const cy = PAD_TOP + radius;
120
184
 
121
185
  let cumFrac = 0;
122
- const slices = data.map((d) => {
186
+ const slices = 出す件たち.map((d) => {
123
187
  const frac = d.value / total;
124
188
  // 9 時 (π) から 3 時 (2π) へ、 上を通って進む
125
189
  const a0 = Math.PI + cumFrac * Math.PI;
@@ -139,17 +203,24 @@ export function ChartGaugeNode({
139
203
  /**
140
204
  * 合計の字の級。
141
205
  *
142
- * **内側の空きに収める**。 弧の内側は半径 `内半径` の半円で、中央に置ける横幅は
143
- * `内半径 * 2`、縦は `内半径` しかない。 桁数で必要な幅が変わるため、
144
- * 実際の文字列を `textWidth` で測って逆算する。
206
+ * **内側の空きに収める**。 弧の内側は半径 `内半径` の半円で、中央に置ける広さは
207
+ * 高さによって変わる。 桁数で必要な幅が変わるため、実際の文字列を `textWidth` で
208
+ * 測って逆算する。
209
+ *
210
+ * **上の角で測る** (cdl#735)。 中心の高さの横幅 (`内半径 * 2`) で測ると、
211
+ * 字は中心より上に置かれるぶんだけ穴からはみ出す (実測で桁の多い合計が 3.8 出た)。
212
+ * 字の上端は `cy - 級 * 1.25` (置く高さ `級 * 0.5` + 字の高さ `級 * 0.75`) なので、
213
+ * 上の角が円に入る条件は `(単位幅 * 級 / 2)^2 + (1.25 * 級)^2 <= 内半径^2`。
214
+ * 級について解くと下の式になる。
145
215
  *
146
- * 縦は見出し (「合計」) の行も入るので、2 行ぶんが半円の高さに収まる比で切る。
216
+ * 縦は見出し (「合計」) の行も入るので、2 行ぶんが半円の高さに収まる比で別に切る。
147
217
  */
148
218
  const 合計の文字 = 実値表記(Math.round(total));
149
- const 横の上限 = 内半径 * 2 * 0.86;
219
+ /** 級 1 あたりの字幅 */
220
+ const 単位幅 = Math.max(textWidth(合計の文字, 100), 1) / 100;
150
221
  const 級の候補 = Math.min(
151
- // 横 = 実際の字幅から逆算する
152
- (横の上限 / Math.max(textWidth(合計の文字, 100), 1)) * 100,
222
+ // 横 = 字の上の角が内側の円に入る大きさ
223
+ 内半径 / Math.sqrt((単位幅 / 2) ** 2 + 1.25 ** 2),
153
224
  // 縦 = 合計 1 行 + 見出し 1 行が内側の高さに収まる比
154
225
  内半径 * 0.52,
155
226
  );
@@ -239,7 +310,7 @@ export function ChartGaugeNode({
239
310
 
240
311
  字は区画の左端に揃える。 中央揃えにすると名前の長さで位置が動き、 縦の列が揃わない。
241
312
  */}
242
- {data.map((d, idx) => {
313
+ {出す件たち.map((d, idx) => {
243
314
  const 列 = idx % 行の件数;
244
315
  const 段 = Math.floor(idx / 行の件数);
245
316
  const x = PAD_SIDE + 内訳の幅 * 列;