@cardenelabs/cdl 0.39.0 → 0.40.1

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.
@@ -2356,6 +2356,7 @@ type CdlPhase = {
2356
2356
  * | `chart-gauge` | 9 時 | 弧が上を通って 3 時へ伸びる | 合計と内訳は最初から出る |
2357
2357
  * | `chart-radial` | 12 時 | 各輪が時計回りに開く | 軌道と一覧は最初から出る |
2358
2358
  * | `chart-stacked-bar` | 左端 | 帯が右へ伸びる | 区画の字はその区画が広がった時 |
2359
+ * | `chart-waffle` | 左上 | 印が読む向きに 1 個ずつ埋まる | 一覧は最初から出る |
2359
2360
  *
2360
2361
  * 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
2361
2362
  *
@@ -2356,6 +2356,7 @@ type CdlPhase = {
2356
2356
  * | `chart-gauge` | 9 時 | 弧が上を通って 3 時へ伸びる | 合計と内訳は最初から出る |
2357
2357
  * | `chart-radial` | 12 時 | 各輪が時計回りに開く | 軌道と一覧は最初から出る |
2358
2358
  * | `chart-stacked-bar` | 左端 | 帯が右へ伸びる | 区画の字はその区画が広がった時 |
2359
+ * | `chart-waffle` | 左上 | 印が読む向きに 1 個ずつ埋まる | 一覧は最初から出る |
2359
2360
  *
2360
2361
  * 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
2361
2362
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.39.0",
3
+ "version": "0.40.1",
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",
@@ -68,11 +68,87 @@ import { 進みを畳む, 伸ばすdash } from "./draw-progress";
68
68
  *
69
69
  * **この形では「線の傾きが動いた量」 が成立しない**。 `+40` と `+30` がどちらも水平線に
70
70
  * なる。 書いた図だけに効く割り切りで、量は字を読む。
71
+ *
72
+ * ## 名札は重ならないようにほどく (cdl#723)
73
+ *
74
+ * 名札を点と同じ y に置くと、**値が同じ / 近い系列があると名札どうしが重なって読めない**。
75
+ * 実測で 59 と 59 の 2 件が同じ y になり、値も名前も二重に印字されていた。
76
+ *
77
+ * 動かすのは **名札だけ**。 点と線は真の位置に残す = 位置をずらすと、書いていない差を
78
+ * 絵が主張することになる (同順位を段でずらさないのと同じ理由)。 同じ値の点は重なったまま
79
+ * でよく、どの名札がどの系列かは名前の色が担う。
80
+ *
81
+ * ほどき方は `名札の段をほどく` が持つ。
71
82
  */
72
83
 
73
84
  /** 線を引くのに最低限要る横幅。 名札が長い図でもこれだけは残す */
74
85
  export const 線の最小幅 = 120;
75
86
 
87
+ /**
88
+ * 名札の縦の位置をほどく (cdl#723)。
89
+ *
90
+ * ## 重なった連なりは元の位置の中心へ置き直す
91
+ *
92
+ * 上から順に押し下げるだけだと塊が下へ寄り、点との対応が片側だけ遠くなる。
93
+ * **元の位置からの動きが最も小さくなる並べ方** を選ぶ = 重なった連なりごとに、元の位置の
94
+ * 平均を中心にして間隔ぶんずつ均等に置く。
95
+ *
96
+ * 実装は単調回帰。 `y[k+1] - y[k] >= 間隔` は `y[k] - 間隔 * k` が単調非減少と同じなので、
97
+ * その量に隣接違反の併合を当てて戻す。 併合した塊は二度と割れないので、件数ぶんで終わる。
98
+ *
99
+ * @param 真のy 点の縦の位置。 並びは系列の順
100
+ * @param 間隔 名札 1 行が縦に要する高さ
101
+ * @param 上限 名札を置いてよい上端
102
+ * @param 下限 名札を置いてよい下端
103
+ * @returns 系列の順に対応する名札の縦の位置
104
+ */
105
+ export function 名札の段をほどく(
106
+ 真のy: readonly number[],
107
+ 間隔: number,
108
+ 上限: number,
109
+ 下限: number,
110
+ ): number[] {
111
+ const 件数 = 真のy.length;
112
+ if (件数 === 0) return [];
113
+ // 上から順に解く。 元の並び順は最後に戻す。 同じ y の件は系列の順で上下を決める
114
+ const 並び = 真のy.map((y, i) => ({ y, i })).sort((a, b) => a.y - b.y || a.i - b.i);
115
+ const 結果 = new Array<number>(件数);
116
+
117
+ /**
118
+ * **入り切らない図では均等に割り付ける**。 間隔を守る置き方が無いので、どれか 1 つを
119
+ * 枠の外へ出すのではなく全部を等しく詰める。 重なりは残るが、寄る先が偏らない。
120
+ */
121
+ if (間隔 * (件数 - 1) > 下限 - 上限) {
122
+ 並び.forEach((e, k) => {
123
+ 結果[e.i] = 件数 > 1 ? 上限 + ((下限 - 上限) / (件数 - 1)) * k : (上限 + 下限) / 2;
124
+ });
125
+ return 結果;
126
+ }
127
+
128
+ // 塊は「合計」 と「件数」 だけ持てば平均が出る
129
+ const 塊たち: { 合計: number; 件数: number }[] = [];
130
+ 並び.forEach((e, k) => {
131
+ 塊たち.push({ 合計: e.y - 間隔 * k, 件数: 1 });
132
+ while (塊たち.length >= 2) {
133
+ const 後 = 塊たち[塊たち.length - 1]!;
134
+ const 前 = 塊たち[塊たち.length - 2]!;
135
+ if (前.合計 / 前.件数 <= 後.合計 / 後.件数) break;
136
+ 塊たち.splice(塊たち.length - 2, 2, { 合計: 前.合計 + 後.合計, 件数: 前.件数 + 後.件数 });
137
+ }
138
+ });
139
+
140
+ const ほどいた: number[] = [];
141
+ 塊たち.forEach((塊) => {
142
+ const 平均 = 塊.合計 / 塊.件数;
143
+ for (let j = 0; j < 塊.件数; j++) ほどいた.push(平均 + 間隔 * ほどいた.length);
144
+ });
145
+
146
+ // 枠へ収める。 **全体を同じだけ動かす** ので、ほどいた間隔は崩れない
147
+ const ずれ = Math.max(0, 上限 - ほどいた[0]!) - Math.max(0, ほどいた[件数 - 1]! - 下限);
148
+ 並び.forEach((e, k) => (結果[e.i] = ほどいた[k]! + ずれ));
149
+ return 結果;
150
+ }
151
+
76
152
  type 系列 = {
77
153
  名前: string;
78
154
  前: number;
@@ -218,6 +294,20 @@ export function ChartSlopeNode({
218
294
  /** 右の点と右の名札は、線が届いてから出る */
219
295
  const 右を出す = !drawing || 伸び >= 1;
220
296
 
297
+ // `rank` は縦の位置を順位の段にする (cdl#707)。 値の尺は使わない
298
+ const 左の点y = 系列たち.map((s, i) =>
299
+ 形 === "rank" ? 段のyを取る(前の順位[i]!) : yを取る(s.前),
300
+ );
301
+ const 右の点y = 系列たち.map((s, i) =>
302
+ 形 === "rank" ? 段のyを取る(今の順位[i]!) : yを取る(s.今),
303
+ );
304
+
305
+ /** 名札 1 行が縦に要する高さ。 大きい方の字に、行どうしが触れない余りを足す */
306
+ const 名札の行 = Math.max(値の級, 名前の級) + 4;
307
+ /** 名札の縦の位置 (cdl#723)。 点は真の位置のまま、字だけを重ならない段へ移す */
308
+ const 左の名札y = 前あり ? 名札の段をほどく(左の点y, 名札の行, 上端, 下端) : 左の点y;
309
+ const 右の名札y = 名札の段をほどく(右の点y, 名札の行, 上端, 下端);
310
+
221
311
  return (
222
312
  <g transform={`translate(${x0} ${y0})`}>
223
313
  <rect
@@ -276,9 +366,11 @@ export function ChartSlopeNode({
276
366
  )}
277
367
 
278
368
  {系列たち.map((s, i) => {
279
- // `rank` は縦の位置を順位の段にする (cdl#707)。 値の尺は使わない
280
- const 左y = 形 === "rank" ? 段のyを取る(前の順位[i]!) : yを取る(s.前);
281
- const 右y = 形 === "rank" ? 段のyを取る(今の順位[i]!) : yを取る(s.今);
369
+ const 左y = 左の点y[i]!;
370
+ const 右y = 右の点y[i]!;
371
+ // 名札は重なりをほどいた段に置く (cdl#723)
372
+ const 左札y = 左の名札y[i]!;
373
+ const 右札y = 右の名札y[i]!;
282
374
  const 未解決 = s.未解決 ? "true" : undefined;
283
375
  return (
284
376
  <g key={`slope-${i}`}>
@@ -321,7 +413,7 @@ export function ChartSlopeNode({
321
413
  <text
322
414
  data-cdl-role="chart-slope-value"
323
415
  x={左軸x - 軸から値まで}
324
- y={左y + 5}
416
+ y={左札y + 5}
325
417
  textAnchor="end"
326
418
  fontSize={値の級}
327
419
  fontWeight={600}
@@ -332,7 +424,7 @@ export function ChartSlopeNode({
332
424
  <text
333
425
  data-cdl-role="chart-slope-name"
334
426
  x={左軸x - 軸から値まで - 左値幅 - 値から名前まで}
335
- y={左y + 5}
427
+ y={左札y + 5}
336
428
  textAnchor="end"
337
429
  fontSize={名前の級}
338
430
  fill={s.色}
@@ -344,7 +436,7 @@ export function ChartSlopeNode({
344
436
  <text
345
437
  data-cdl-role="chart-slope-name"
346
438
  x={左軸x - 軸から値まで}
347
- y={右y + 5}
439
+ y={右札y + 5}
348
440
  textAnchor="end"
349
441
  fontSize={名前の級}
350
442
  fill={s.色}
@@ -360,7 +452,7 @@ export function ChartSlopeNode({
360
452
  // 消費側が増減だけに色を当てたくなる経路がある
361
453
  data-cdl-role={形 === "delta" ? "chart-slope-delta" : "chart-slope-value"}
362
454
  x={右軸x + 軸から値まで}
363
- y={右y + 5}
455
+ y={右札y + 5}
364
456
  fontSize={値の級}
365
457
  fontWeight={600}
366
458
  fill="var(--cdl-text, #1a1f2a)"
@@ -371,7 +463,7 @@ export function ChartSlopeNode({
371
463
  <text
372
464
  data-cdl-role="chart-slope-name"
373
465
  x={右軸x + 軸から値まで + 右値幅 + 値から名前まで}
374
- y={右y + 5}
466
+ y={右札y + 5}
375
467
  fontSize={名前の級}
376
468
  fill={s.色}
377
469
  >
@@ -4,6 +4,7 @@ import { TONE, chartSeriesColor } from "../render/tone";
4
4
  import { resolveChartData } from "../render/payload-binding";
5
5
  import { textWidth } from "./text-width";
6
6
  import { 実値表記 } from "./chart-number";
7
+ import { 進みを畳む } from "./draw-progress";
7
8
 
8
9
  /**
9
10
  * chart-waffle kind ... 100 個の印で割合を **数えて確かめられる** 図 (cdl#552)。
@@ -24,6 +25,16 @@ import { 実値表記 } from "./chart-number";
24
25
  *
25
26
  * `印の割付` が最大剰余法で配る = まず切り捨てで配り、余った分を小数部の大きい件から 1 個ずつ
26
27
  * 足す。 合計は定義上ちょうど 100 になる。
28
+ *
29
+ * ## 段の `draw` では印が読む向きに埋まる (cdl#719)
30
+ *
31
+ * 印は上の行から左詰めで配るので、埋まる順もそのまま読む向きになる。
32
+ *
33
+ * **切り抜きでは表せない**。 弧の 3 種 (cdl#715) は切り抜きで隠したが、この図に当てると
34
+ * 升の途中が斜めや半分で切れる = 1 個 = 1% の印が半分だけ出て、数えられない図になる。
35
+ *
36
+ * 代わりに **塗りだけを変える**。 印は 100 個とも最初から描き、進みまで届いていない印を
37
+ * 持ち主のいない印と同じ地の色にする。 升の形も持ち主の欄も触らない。
27
38
  */
28
39
 
29
40
  /** 印の総数。 1 個 = 1% にするための値で、変えると「数えれば分かる」 が成り立たない */
@@ -70,10 +81,16 @@ export function ChartWaffleNode({
70
81
  node,
71
82
  active,
72
83
  stateValues = {},
84
+ drawing = false,
85
+ progress = 1,
73
86
  }: {
74
87
  node: LaidNode;
75
88
  active: boolean;
76
89
  stateValues?: Record<string, string>;
90
+ /** この phase の `draw` に載っているか (cdl#719) */
91
+ drawing?: boolean;
92
+ /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
93
+ progress?: number;
77
94
  }): JSX.Element {
78
95
  const x0 = node.cx - node.w / 2;
79
96
  const y0 = node.cy - node.h / 2;
@@ -112,6 +129,14 @@ export function ChartWaffleNode({
112
129
  for (let k = 0; k < n; k++) 持ち主.push(idx);
113
130
  });
114
131
 
132
+ /**
133
+ * 埋まった印の数 (cdl#719)。 畳み方は `draw-progress.ts` が持つ。
134
+ *
135
+ * **四捨五入する**。 切り捨てると進み 1 の手前で 1 個足りない図が続き、切り上げると
136
+ * 進み 0 の時点で 1 個埋まる。 どちらも「数えれば分かる」 が崩れる。
137
+ */
138
+ const 埋まった = Math.round(進みを畳む(drawing, progress) * WAFFLE_TOTAL);
139
+
115
140
  const 一覧の始まり = PAD + (使える高さ - 行の高さ * data.length) / 2 + 行の高さ / 2;
116
141
 
117
142
  return (
@@ -136,6 +161,11 @@ export function ChartWaffleNode({
136
161
  const 行 = Math.floor(k / WAFFLE_COLS);
137
162
  const 列 = k % WAFFLE_COLS;
138
163
  const owner = 持ち主[k];
164
+ /**
165
+ * 色を付けるか (cdl#719)。 **持ち主の欄は触らない** = 印から持ち主を読む利用側が、
166
+ * 進みの途中で別の値を読まないようにする (形を表す属性と同じ扱い)。
167
+ */
168
+ const 埋まっている = owner !== undefined && k < 埋まった;
139
169
  return (
140
170
  <rect
141
171
  key={`cell-${k}`}
@@ -146,8 +176,8 @@ export function ChartWaffleNode({
146
176
  width={印}
147
177
  height={印}
148
178
  rx={2}
149
- fill={owner === undefined ? "var(--cdl-divider, #d4d4d8)" : 印の色(data[owner]!.tone, owner)}
150
- fillOpacity={owner === undefined ? 0.35 : 1}
179
+ fill={埋まっている ? 印の色(data[owner]!.tone, owner) : "var(--cdl-divider, #d4d4d8)"}
180
+ fillOpacity={埋まっている ? 1 : 0.35}
151
181
  />
152
182
  );
153
183
  })}
@@ -228,8 +228,16 @@ export function CdlNodeView({
228
228
  />
229
229
  );
230
230
  case "chart-waffle":
231
- // 1 個 = 1% の印を 100 個。 段の進みで印が埋まる動きは別 Issue (cdl#552 で対象外)
232
- return <ChartWaffleNode node={resolvedNode} active={active} stateValues={stateValues} />;
231
+ // 1 個 = 1% の印を 100 個。 段の進みでは読む向きに 1 個ずつ埋まる (cdl#719)
232
+ return (
233
+ <ChartWaffleNode
234
+ node={resolvedNode}
235
+ active={active}
236
+ stateValues={stateValues}
237
+ drawing={drawing}
238
+ progress={progress}
239
+ />
240
+ );
233
241
  case "chart-stat":
234
242
  // 数値が主役で、弧は輪郭だけの補助。 段の進みで数値が動く演出は既存の tween が担う
235
243
  return <ChartStatNode node={resolvedNode} active={active} stateValues={stateValues} />;
package/src/types.ts CHANGED
@@ -2433,6 +2433,7 @@ export type CdlPhase = {
2433
2433
  * | `chart-gauge` | 9 時 | 弧が上を通って 3 時へ伸びる | 合計と内訳は最初から出る |
2434
2434
  * | `chart-radial` | 12 時 | 各輪が時計回りに開く | 軌道と一覧は最初から出る |
2435
2435
  * | `chart-stacked-bar` | 左端 | 帯が右へ伸びる | 区画の字はその区画が広がった時 |
2436
+ * | `chart-waffle` | 左上 | 印が読む向きに 1 個ずつ埋まる | 一覧は最初から出る |
2436
2437
  *
2437
2438
  * 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
2438
2439
  *
package/src/validate.ts CHANGED
@@ -26,6 +26,7 @@ const TONES = new Set<string>(TONE_NAMES);
26
26
  * | `chart-gauge` | 9 時 | 弧が上を通って 3 時へ伸びる |
27
27
  * | `chart-radial` | 12 時 | 各輪が時計回りに開く |
28
28
  * | `chart-stacked-bar` | 左端 | 帯が右へ伸びる |
29
+ * | `chart-waffle` | 左上 | 印が読む向きに 1 個ずつ埋まる |
29
30
  */
30
31
  export const DRAW_KINDS: ReadonlySet<string> = new Set([
31
32
  "chart-line",
@@ -40,6 +41,7 @@ export const DRAW_KINDS: ReadonlySet<string> = new Set([
40
41
  "chart-gauge",
41
42
  "chart-radial",
42
43
  "chart-stacked-bar",
44
+ "chart-waffle",
43
45
  ]);
44
46
  const KINDS = new Set<string>(NODE_KINDS);
45
47
  const ENG_BANNED = /\b(FUNCTION|STORAGE|EVENT LOG|BALANCES MAPPING|READ|WRITE|EMIT|CALL)\b/;