@cardenelabs/cdl 0.40.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardenelabs/cdl",
3
- "version": "0.40.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
  >