@cardenelabs/cdl 0.40.10 → 0.40.11

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.10",
3
+ "version": "0.40.11",
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",
@@ -67,13 +67,19 @@ export function ChartBarNode({
67
67
  const canvasH = node.h - PAD_TOP - PAD_BOTTOM;
68
68
 
69
69
  const vMax = data.length > 0 ? Math.max(...data.map((d) => d.value)) : 1;
70
+ /**
71
+ * 縦軸を決める元になる高さ。 **前の時点も含める** (cdl#767)。
72
+ * 含めないと、前が今より大きい件で印が枠の外へ出る。
73
+ */
74
+ const 軸のもと =
75
+ data.length > 0 ? Math.max(vMax, ...data.map((d) => d.previous ?? 0)) : vMax;
70
76
 
71
77
  const 目盛りの目安 = 4;
72
78
  /**
73
79
  * 目盛りを引く元の高さ。 値が 0 以下しか無い図 (と値を読めない図) では 1 を使う =
74
80
  * 最大値をそのまま使うと 0 が 5 つ並ぶ縦軸になり、何も読めない。
75
81
  */
76
- const 天井のもと = vMax > 0 ? vMax : 1;
82
+ const 天井のもと = 軸のもと > 0 ? 軸のもと : 1;
77
83
  // 棒は床が必ず 0 なので、範囲 = 天井のもと
78
84
  const 刻み = 目盛りの刻み(天井のもと, 目盛りの目安);
79
85
  const 刻みの桁 = 小数の桁(刻み);
@@ -193,6 +199,37 @@ export function ChartBarNode({
193
199
  fillOpacity={濃さ(d.value)}
194
200
  />
195
201
  );
202
+ /**
203
+ * 前の時点の印 (cdl#767)。
204
+ *
205
+ * **重ねた棒ではなく線にする** = 同じ幅の棒を後ろに置くと、前 < 今 の件では
206
+ * 今の棒に完全に隠れて何も見えない。 線なら前後どちらでも見える。
207
+ *
208
+ * **棒より少し広く引く** = 棒と同じ幅だと、前と今が同じ値の時に棒の頭と
209
+ * 重なって 1 本に見える。 左右にはみ出させて別のものだと分かる形にする。
210
+ *
211
+ * **伸びの包みに入れない** = 印は「どこから動いたか」 を示す目盛りで、
212
+ * 棒と一緒に伸びると比べる相手が動いてしまう。
213
+ *
214
+ * **件ごとに出す**。 円グラフ (cdl#679) は書かない件を 0 として扱うが、あちらは
215
+ * 輪の内外で色の並びを対応させる必要があるため。 棒の印は 1 本ずつ独立しており、
216
+ * 書かない件に 0 の印を引くと「前は 0 だった」 と読めてしまう。
217
+ */
218
+ const 印のはみ出し = Math.min(barW * 0.18, 8);
219
+ const 前の印 =
220
+ d.previous === undefined ? null : (
221
+ <line
222
+ data-cdl-role="chart-bar-previous"
223
+ data-cdl-unresolved={d.unresolved ? "true" : undefined}
224
+ x1={bx - 印のはみ出し}
225
+ y1={yAt(Math.max(0, d.previous))}
226
+ x2={bx + barW + 印のはみ出し}
227
+ y2={yAt(Math.max(0, d.previous))}
228
+ stroke="var(--cdl-text-mute, #8a8678)"
229
+ strokeWidth={1.5}
230
+ strokeDasharray="5 3"
231
+ />
232
+ );
196
233
  return (
197
234
  <g key={`bar-${idx}`}>
198
235
  {/* 描く指定が無い図には包みを足さない = 既存の図の DOM を 1 要素も変えない */}
@@ -203,6 +240,7 @@ export function ChartBarNode({
203
240
  ) : (
204
241
  棒
205
242
  )}
243
+ {前の印}
206
244
  {/* 値のほうが読ませたい数。 横軸の名前より大きく太くする (cdl#747) */}
207
245
  <text
208
246
  data-cdl-role="chart-bar-value"
@@ -182,22 +182,60 @@ export function ChartGaugeNode({
182
182
  /** 弧の中心。 上半分だけを描くので、 中心は弧の下端に来る */
183
183
  const cy = PAD_TOP + radius;
184
184
 
185
+ /**
186
+ * 前の時点を書いた件があるか (cdl#767)。
187
+ *
188
+ * **1 件でも書けば輪が 2 つになる**。 持たない件は前の時点で 0 として扱う =
189
+ * 件を落とすと内外で色の並びが対応しなくなる (円グラフ cdl#679 と同じ判断)。
190
+ *
191
+ * 半円は **針を持つ図ではなく、半分の円グラフ** (件を積んで合計を中央に出す形) なので、
192
+ * 棒や弧のような 1 件 1 本の印ではなく輪にする。
193
+ */
194
+ const 前あり = 出す件たち.some((d) => d.previous !== undefined);
195
+ const 前の値 = 出す件たち.map((d) => Math.max(0, d.previous ?? 0));
196
+ const 前の合計 = 前の値.reduce((acc, v) => acc + v, 0);
197
+
198
+ /**
199
+ * 輪の割り付け。 **前を書かない図では今の輪を 1 mm も動かさない** =
200
+ * 書いていない図の DOM を変えない。
201
+ *
202
+ * 前を書いた図では、帯を 内 (前) と 外 (今) に分ける。 並びは 前 → 今、
203
+ * すなわち 内 → 外 で円グラフ (cdl#679) と揃える。
204
+ */
205
+ const 帯 = radius - 内半径;
206
+ const 今の内半径 = 前あり ? 内半径 + 帯 * 0.42 : 内半径;
207
+ const 前の内半径 = 内半径;
208
+ const 前の外半径 = 内半径 + 帯 * 0.32;
209
+
210
+ /** 9 時 (π) から 3 時 (2π) へ、 上を通って進む扇 1 つ分の道 */
211
+ const 扇の道 = (a0: number, a1: number, 内: number, 外: number): string => {
212
+ const 外1 = 極(cx, cy, 外, a0);
213
+ const 外2 = 極(cx, cy, 外, a1);
214
+ const 内2 = 極(cx, cy, 内, a1);
215
+ const 内1 = 極(cx, cy, 内, a0);
216
+ // 半円の中では 1 つの扇が半周を超えないため、 大きい弧の指定は常に 0
217
+ return (
218
+ `M ${外1.x} ${外1.y} A ${外} ${外} 0 0 1 ${外2.x} ${外2.y} ` +
219
+ `L ${内2.x} ${内2.y} A ${内} ${内} 0 0 0 ${内1.x} ${内1.y} Z`
220
+ );
221
+ };
222
+
185
223
  let cumFrac = 0;
186
224
  const slices = 出す件たち.map((d) => {
187
225
  const frac = d.value / total;
188
- // 9 時 (π) から 3 時 (2π) へ、 上を通って進む
189
226
  const a0 = Math.PI + cumFrac * Math.PI;
190
227
  const a1 = a0 + frac * Math.PI;
191
228
  cumFrac += frac;
192
- const 外1 = 極(cx, cy, radius, a0);
193
- const 外2 = 極(cx, cy, radius, a1);
194
- const 内2 = 極(cx, cy, 内半径, a1);
195
- const 内1 = 極(cx, cy, 内半径, a0);
196
- // 半円の中では 1 つの扇が半周を超えないため、 大きい弧の指定は常に 0
197
- const path =
198
- `M ${外1.x} ${外1.y} A ${radius} ${radius} 0 0 1 ${外2.x} ${外2.y} ` +
199
- `L ${内2.x} ${内2.y} A ${内半径} ${内半径} 0 0 0 ${内1.x} ${内1.y} Z`;
200
- return { d, frac, path };
229
+ return { d, frac, path: 扇の道(a0, a1, 今の内半径, radius) };
230
+ });
231
+
232
+ let 前のcum = 0;
233
+ const 前の扇たち = 出す件たち.map((d, idx) => {
234
+ const frac = 前の合計 > 0 ? (前の値[idx] ?? 0) / 前の合計 : 0;
235
+ const a0 = Math.PI + 前のcum * Math.PI;
236
+ const a1 = a0 + frac * Math.PI;
237
+ 前のcum += frac;
238
+ return { d, frac, path: 扇の道(a0, a1, 前の内半径, 前の外半径) };
201
239
  });
202
240
 
203
241
  /**
@@ -249,6 +287,28 @@ export function ChartGaugeNode({
249
287
  />
250
288
  ));
251
289
 
290
+ /**
291
+ * 前の時点の輪 (cdl#767)。 **書かない図では 1 要素も出さない**。
292
+ *
293
+ * 今の輪と同じ色の並びで、内側に細く置く。 区切りの余白は今の輪より細くする =
294
+ * 帯そのものが細いので、同じ太さで抜くと扇が分断されて見える。
295
+ */
296
+ const 前の弧たち = 前あり
297
+ ? 前の扇たち.map((s, idx) => (
298
+ <path
299
+ key={`prev-arc-${idx}`}
300
+ data-cdl-role="chart-gauge-previous"
301
+ data-cdl-unresolved={s.d.unresolved ? "true" : undefined}
302
+ d={s.path}
303
+ fill={arcColor(s.d.tone, idx)}
304
+ fillOpacity={0.45}
305
+ stroke="var(--cdl-node-fill, #ffffff)"
306
+ strokeWidth={1.5}
307
+ strokeLinejoin="round"
308
+ />
309
+ ))
310
+ : null;
311
+
252
312
  return (
253
313
  <g transform={`translate(${x0} ${y0})`}>
254
314
  <rect
@@ -277,7 +337,17 @@ export function ChartGaugeNode({
277
337
  </defs>
278
338
  )}
279
339
  {/* 描く指定が無い図には包みを足さず、 従来の DOM 構造を保つ */}
280
- {drawing ? <g clipPath={`url(#${切り抜きid})`}>{弧たち}</g> : 弧たち}
340
+ {drawing ? (
341
+ <g clipPath={`url(#${切り抜きid})`}>
342
+ {前の弧たち}
343
+ {弧たち}
344
+ </g>
345
+ ) : (
346
+ <>
347
+ {前の弧たち}
348
+ {弧たち}
349
+ </>
350
+ )}
281
351
 
282
352
  {/*
283
353
  合計。 この種別の主役なので図の中でいちばん大きい字にするが、 **弧の内側に収める**。
@@ -103,9 +103,15 @@ export function ChartRadialNode({
103
103
  const rings = data.map((d, idx) => {
104
104
  const frac = d.value / total;
105
105
  const 中心半径 = 外半径 - 帯1本 * idx - 帯1本 / 2;
106
+ /**
107
+ * 前の時点の取り分 (cdl#767)。 **今の合計で割る** = 前の合計は書けないので、
108
+ * 「今の全体で見ると前はどこにいたか」 を示す印になる。
109
+ */
110
+ const 前の割合 = d.previous === undefined ? null : Math.max(0, d.previous) / total;
106
111
  return {
107
112
  d,
108
113
  frac,
114
+ 前の割合,
109
115
  中心半径,
110
116
  // 軌道は全周。 1 周ちょうどは弧で表せないので、 わずかに欠かして描く
111
117
  軌道: 弧の道(弧のcx, cy, 中心半径, 帯の幅, 0.9999),
@@ -173,6 +179,31 @@ export function ChartRadialNode({
173
179
  {/* 描く指定が無い図には包みを足さず、 従来の DOM 構造を保つ */}
174
180
  {drawing ? <g clipPath={`url(#${切り抜きid})`}>{取り分たち}</g> : 取り分たち}
175
181
 
182
+ {/*
183
+ 前の時点の印 (cdl#767)。
184
+
185
+ **弧ではなく帯を横切る線にする** = 同じ帯に薄い弧を重ねると、前 < 今 の件では
186
+ 今の弧に完全に隠れて何も見えない。 線なら前後どちらでも見える。
187
+
188
+ **切り抜きに入れない** = 印は「どこから動いたか」 を示す目盛りで、弧と一緒に
189
+ 開くと比べる相手が動いてしまう。
190
+
191
+ **件ごとに出す**。 書かない件に 0 の印を置くと「前は 0 だった」 と読めてしまう。
192
+ */}
193
+ {rings.map((r, idx) =>
194
+ r.前の割合 === null ? null : (
195
+ <line
196
+ key={`prev-${idx}`}
197
+ data-cdl-role="chart-radial-previous"
198
+ data-cdl-unresolved={r.d.unresolved ? "true" : undefined}
199
+ {...帯を横切る印(弧のcx, cy, r.中心半径, 帯の幅, Math.min(r.前の割合, 0.9999))}
200
+ stroke="var(--cdl-text-mute, #8a8678)"
201
+ strokeWidth={2}
202
+ strokeLinecap="round"
203
+ />
204
+ ),
205
+ )}
206
+
176
207
  {/* 一覧。 行の並びが弧の並び (外から内へ) と対応する */}
177
208
  {data.map((d, idx) => {
178
209
  const y = 一覧の始まり + 行の高さ * idx;
@@ -225,6 +256,34 @@ function 弧の道(cx: number, cy: number, 中心半径: number, 幅: number,
225
256
  );
226
257
  }
227
258
 
259
+ /**
260
+ * 帯を横切る印の両端 (cdl#767)。
261
+ *
262
+ * `弧の道` と同じ起点 (12 時) と向き (時計回り) を使う = 印と弧が別の角度を指すと、
263
+ * どちらが正しいのか読み手に決められない。
264
+ *
265
+ * **帯より少し長く引く** = 帯と同じ長さだと、前と今が同じ割合の時に弧の端と重なって
266
+ * 区別が付かない。 内外へはみ出させて別のものだと分かる形にする。
267
+ */
268
+ function 帯を横切る印(
269
+ cx: number,
270
+ cy: number,
271
+ 中心半径: number,
272
+ 幅: number,
273
+ 割合: number,
274
+ ): { x1: number; y1: number; x2: number; y2: number } {
275
+ const はみ出し = Math.min(幅 * 0.3, 5);
276
+ const 外 = 中心半径 + 幅 / 2 + はみ出し;
277
+ const 内 = Math.max(0.5, 中心半径 - 幅 / 2 - はみ出し);
278
+ const a = -Math.PI / 2 + 割合 * Math.PI * 2;
279
+ return {
280
+ x1: cx + 内 * Math.cos(a),
281
+ y1: cy + 内 * Math.sin(a),
282
+ x2: cx + 外 * Math.cos(a),
283
+ y2: cy + 外 * Math.sin(a),
284
+ };
285
+ }
286
+
228
287
  /** 弧の色。 明示した色が優先され、 無ければ図表の系列の色を順に配る */
229
288
  function arcColor(tone: Tone | undefined, idx: number): string {
230
289
  if (tone) return TONE[tone];