@cardenelabs/cdl 0.121.0 → 0.123.0

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +137 -0
  3. package/SPEC.md +1 -1
  4. package/dist/index.cjs +1513 -667
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +51 -33
  7. package/dist/index.d.ts +51 -33
  8. package/dist/index.js +1513 -668
  9. package/dist/index.js.map +1 -1
  10. package/dist/react.cjs +1271 -444
  11. package/dist/react.cjs.map +1 -1
  12. package/dist/react.d.cts +1 -1
  13. package/dist/react.d.ts +1 -1
  14. package/dist/react.js +1271 -444
  15. package/dist/react.js.map +1 -1
  16. package/dist/{render-A5AKjNuy.d.cts → render-CVczwfRb.d.cts} +40 -7
  17. package/dist/{render-A5AKjNuy.d.ts → render-CVczwfRb.d.ts} +40 -7
  18. package/package.json +9 -12
  19. package/src/builder.ts +8 -2
  20. package/src/index.ts +3 -0
  21. package/src/kinds/box-lines.ts +17 -4
  22. package/src/kinds/chart-bar.tsx +4 -0
  23. package/src/kinds/chart-gauge.tsx +20 -9
  24. package/src/kinds/chart-pie.tsx +67 -17
  25. package/src/kinds/chart-radial.tsx +7 -2
  26. package/src/kinds/chart-slope.tsx +26 -7
  27. package/src/kinds/chart-stacked-bar.tsx +8 -4
  28. package/src/kinds/chart-stat.tsx +19 -4
  29. package/src/kinds/chart-waffle.tsx +7 -2
  30. package/src/kinds/dyn-shape.tsx +22 -9
  31. package/src/kinds/funnel.tsx +9 -2
  32. package/src/kinds/generic.tsx +5 -2
  33. package/src/kinds/mind-map.tsx +26 -10
  34. package/src/kinds/quadrant.tsx +14 -4
  35. package/src/kinds/row-align.ts +19 -7
  36. package/src/kinds/sequence-board-metrics.ts +13 -3
  37. package/src/kinds/sequence-board.tsx +4 -2
  38. package/src/kinds/shape-basement.tsx +57 -32
  39. package/src/kinds/shape-blockchain.tsx +83 -34
  40. package/src/kinds/shape-commerce.tsx +65 -24
  41. package/src/kinds/shape-finance.tsx +90 -26
  42. package/src/kinds/shape-hardware.tsx +48 -17
  43. package/src/kinds/shape-region.tsx +16 -6
  44. package/src/kinds/shape-software.tsx +30 -8
  45. package/src/kinds/station.tsx +29 -0
  46. package/src/kinds/text-width.ts +17 -3
  47. package/src/kinds/tree.tsx +25 -7
  48. package/src/layout/collisions.ts +50 -28
  49. package/src/layout/curve-path.ts +126 -0
  50. package/src/layout/edges.ts +271 -58
  51. package/src/layout/head-label.ts +11 -5
  52. package/src/layout/lanes.ts +3 -2
  53. package/src/layout/metro-path.ts +185 -0
  54. package/src/layout/nodes.ts +14 -6
  55. package/src/layout/predict-bbox.ts +6 -3
  56. package/src/layout/self-loop.ts +10 -4
  57. package/src/layout/spec.ts +36 -7
  58. package/src/layout/text-width.ts +25 -8
  59. package/src/layout/tokens.ts +2 -0
  60. package/src/layout-with-validation.ts +10 -5
  61. package/src/layout.ts +55 -21
  62. package/src/presets.ts +69 -43
  63. package/src/render/edges.tsx +19 -3
  64. package/src/render/interactive-panel.tsx +34 -7
  65. package/src/render/nodes.tsx +100 -48
  66. package/src/render/stage.tsx +3 -0
  67. package/src/text-metrics.ts +9 -0
  68. package/src/types.ts +41 -4
  69. package/src/visual-validate.ts +91 -21
@@ -1,5 +1,5 @@
1
1
  import { useId, type JSX } from "react";
2
- import type { ChartPieForm, LaidNode, Tone } from "../types";
2
+ import type { CdlTextMetrics, ChartPieForm, LaidNode, Tone } from "../types";
3
3
  import { TONE, chartSeriesColor } from "../render/tone";
4
4
  import { resolveChartData, type ResolvedChartDatum } from "../render/payload-binding";
5
5
  import { 進みを畳む } from "./draw-progress";
@@ -17,8 +17,14 @@ const 穴に対する上限 = 0.8;
17
17
  * 下限まで戻し、**穴に入らない時だけ下限を割る** = 入らない大きさで描くと数字が輪へ
18
18
  * 重なる (#891)。
19
19
  */
20
- function 中央の字(比例した大きさ: number, 穴の半径: number, 文: string): number {
21
- const 入る大きさ = (穴の半径 * 2 * 穴に対する上限) / Math.max(textWidth(文, 1), 1e-6);
20
+ function 中央の字(
21
+ 比例した大きさ: number,
22
+ 穴の半径: number,
23
+ 文: string,
24
+ textMetrics: CdlTextMetrics = "proportional",
25
+ ): number {
26
+ const 入る大きさ =
27
+ (穴の半径 * 2 * 穴に対する上限) / Math.max(textWidth(文, 1, 0, textMetrics), 1e-6);
22
28
  return Math.min(Math.max(比例した大きさ, 縮んだ絵の字の下限), 入る大きさ);
23
29
  }
24
30
  import { 実値表記, 割合表記 } from "./chart-number";
@@ -70,6 +76,7 @@ export function ChartPieNode({
70
76
  stateValues = {},
71
77
  drawing = false,
72
78
  progress = 1,
79
+ textMetrics = "proportional",
73
80
  }: {
74
81
  node: LaidNode;
75
82
  active: boolean;
@@ -78,6 +85,7 @@ export function ChartPieNode({
78
85
  drawing?: boolean;
79
86
  /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
80
87
  progress?: number;
88
+ textMetrics?: CdlTextMetrics;
81
89
  }): JSX.Element {
82
90
  // node id は diagram 内だけで一意。 同じ page に複数 diagram がある時も SVG の参照先が
83
91
  // ぶつからないよう、 React が component instance ごとに割り当てる id を使う。
@@ -151,7 +159,18 @@ export function ChartPieNode({
151
159
  });
152
160
 
153
161
  if (形 !== "ring") {
154
- const 共通 = { node, active, data, 割合: 今の割合, x0, y0, drawing, progress, instanceId };
162
+ const 共通 = {
163
+ node,
164
+ active,
165
+ data,
166
+ 割合: 今の割合,
167
+ x0,
168
+ y0,
169
+ drawing,
170
+ progress,
171
+ instanceId,
172
+ textMetrics,
173
+ };
155
174
  return 形 === "arcs" ? 弧の形(共通) : 表の形(共通);
156
175
  }
157
176
 
@@ -165,8 +184,8 @@ export function ChartPieNode({
165
184
  0,
166
185
  ...data.map((_d, i) =>
167
186
  Math.max(
168
- textWidth(割合の文字[i] ?? "", 割合の級),
169
- textWidth(名前の文字[i] ?? "", 名前の級),
187
+ textWidth(割合の文字[i] ?? "", 割合の級, 0, textMetrics),
188
+ textWidth(名前の文字[i] ?? "", 名前の級, 0, textMetrics),
170
189
  ),
171
190
  ),
172
191
  );
@@ -203,8 +222,8 @@ export function ChartPieNode({
203
222
  */
204
223
  const 穴 = 前あり ? 前の内半径 : 外の内半径;
205
224
  const 合計の文 = 実値表記(Math.round(前あり ? 今の合計 : total));
206
- const 合計の字 = 中央の字(Math.round(radius * 0.26), 穴, 合計の文);
207
- const 見出しの字 = 中央の字(Math.round(radius * 0.12), 穴, "合計");
225
+ const 合計の字 = 中央の字(Math.round(radius * 0.26), 穴, 合計の文, textMetrics);
226
+ const 見出しの字 = 中央の字(Math.round(radius * 0.12), 穴, "合計", textMetrics);
208
227
  const 合計の下 = Math.max(Math.round(radius * 0.2), 合計の字 * 0.25 + 見出しの字 * 0.8);
209
228
  const slices = 輪を組む(data, cx, cy, radius, 外の内半径, 今の割合);
210
229
 
@@ -450,6 +469,7 @@ type 形の引数 = {
450
469
  drawing: boolean;
451
470
  progress: number;
452
471
  instanceId: string;
472
+ textMetrics: CdlTextMetrics;
453
473
  };
454
474
 
455
475
  /** 3 つの見せ方が共通で持つ枠 (cdl#684) */
@@ -480,7 +500,17 @@ function 枠(node: LaidNode, active: boolean): JSX.Element {
480
500
  *
481
501
  * 中心に合計は置かない。 弧は合計を形で示さないので、置くと形と数が食い違う。
482
502
  */
483
- function 弧の形({ node, active, data, 割合, x0, y0, drawing, progress }: 形の引数): JSX.Element {
503
+ function 弧の形({
504
+ node,
505
+ active,
506
+ data,
507
+ 割合,
508
+ x0,
509
+ y0,
510
+ drawing,
511
+ progress,
512
+ textMetrics,
513
+ }: 形の引数): JSX.Element {
484
514
  const PAD = 20;
485
515
  const 名前の級 = 14;
486
516
  const 割合の級 = 24;
@@ -491,9 +521,9 @@ function 弧の形({ node, active, data, 割合, x0, y0, drawing, progress }:
491
521
  const 一覧幅 =
492
522
  10 +
493
523
  印から名前まで +
494
- Math.max(0, ...data.map((d) => textWidth(d.label ?? "", 名前の級))) +
524
+ Math.max(0, ...data.map((d) => textWidth(d.label ?? "", 名前の級, 0, textMetrics))) +
495
525
  名前から割合まで +
496
- Math.max(0, ...割合の文字.map((t) => textWidth(t, 割合の級)));
526
+ Math.max(0, ...割合の文字.map((t) => textWidth(t, 割合の級, 0, textMetrics)));
497
527
 
498
528
  const 縦から = (node.h - PAD * 2) / 2;
499
529
  const 横から = (node.w - PAD * 2 - 一覧幅 - 28) / 2;
@@ -596,7 +626,18 @@ function 弧の形({ node, active, data, 割合, x0, y0, drawing, progress }:
596
626
  * 引き出し線を引かない = 対応は表の色の印が担う。 線と表を両方置くと、同じ対応を
597
627
  * 2 つの仕掛けで示すことになる。
598
628
  */
599
- function 表の形({ node, active, data, 割合, x0, y0, drawing, progress, instanceId }: 形の引数): JSX.Element {
629
+ function 表の形({
630
+ node,
631
+ active,
632
+ data,
633
+ 割合,
634
+ x0,
635
+ y0,
636
+ drawing,
637
+ progress,
638
+ instanceId,
639
+ textMetrics,
640
+ }: 形の引数): JSX.Element {
600
641
  const PAD = 20;
601
642
  const 見出しの級 = 11;
602
643
  const 名前の級 = 15;
@@ -606,9 +647,18 @@ function 表の形({ node, active, data, 割合, x0, y0, drawing, progress, inst
606
647
 
607
648
  const 割合の文字 = 割合.map(割合表記);
608
649
  const 実数の文字 = data.map((d) => 実値表記(d.value));
609
- const 名前幅 = Math.max(0, ...data.map((d) => textWidth(d.label ?? "", 名前の級)));
610
- const 割合幅 = Math.max(textWidth("割合", 見出しの級), ...割合の文字.map((t) => textWidth(t, 割合の級)));
611
- const 実数幅 = Math.max(textWidth("値", 見出しの級), ...実数の文字.map((t) => textWidth(t, 実数の級)));
650
+ const 名前幅 = Math.max(
651
+ 0,
652
+ ...data.map((d) => textWidth(d.label ?? "", 名前の級, 0, textMetrics)),
653
+ );
654
+ const 割合幅 = Math.max(
655
+ textWidth("割合", 見出しの級, 0, textMetrics),
656
+ ...割合の文字.map((t) => textWidth(t, 割合の級, 0, textMetrics)),
657
+ );
658
+ const 実数幅 = Math.max(
659
+ textWidth("値", 見出しの級, 0, textMetrics),
660
+ ...実数の文字.map((t) => textWidth(t, 実数の級, 0, textMetrics)),
661
+ );
612
662
  const 表幅 = 10 + 印から名前まで + 名前幅 + 28 + 割合幅 + 24 + 実数幅;
613
663
 
614
664
  const 縦から = (node.h - PAD * 2) / 2;
@@ -619,8 +669,8 @@ function 表の形({ node, active, data, 割合, x0, y0, drawing, progress, inst
619
669
  const cy = node.h / 2;
620
670
  // 中央の 2 行。 輪の layout と同じ決め方 (#891)
621
671
  const 合計の文 = 実値表記(Math.round(data.reduce((acc, d) => acc + d.value, 0)));
622
- const 合計の字 = 中央の字(Math.round(R * 0.26), 内半径, 合計の文);
623
- const 見出しの字 = 中央の字(Math.round(R * 0.12), 内半径, "合計");
672
+ const 合計の字 = 中央の字(Math.round(R * 0.26), 内半径, 合計の文, textMetrics);
673
+ const 見出しの字 = 中央の字(Math.round(R * 0.12), 内半径, "合計", textMetrics);
624
674
  const 合計の下 = Math.max(Math.round(R * 0.2), 合計の字 * 0.25 + 見出しの字 * 0.8);
625
675
 
626
676
  const 表左 = node.w - PAD - 表幅;
@@ -1,5 +1,5 @@
1
1
  import { useId, type JSX } from "react";
2
- import type { LaidNode, Tone } from "../types";
2
+ import type { CdlTextMetrics, LaidNode, Tone } from "../types";
3
3
  import { TONE, chartSeriesColor } from "../render/tone";
4
4
  import { resolveChartData } from "../render/payload-binding";
5
5
  import { textWidth } from "./text-width";
@@ -45,6 +45,7 @@ export function ChartRadialNode({
45
45
  stateValues = {},
46
46
  drawing = false,
47
47
  progress = 1,
48
+ textMetrics = "proportional",
48
49
  }: {
49
50
  node: LaidNode;
50
51
  active: boolean;
@@ -53,6 +54,7 @@ export function ChartRadialNode({
53
54
  drawing?: boolean;
54
55
  /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
55
56
  progress?: number;
57
+ textMetrics?: CdlTextMetrics;
56
58
  }): JSX.Element {
57
59
  const x0 = node.cx - node.w / 2;
58
60
  const y0 = node.cy - node.h / 2;
@@ -78,7 +80,10 @@ export function ChartRadialNode({
78
80
  Math.max(
79
81
  0,
80
82
  ...data.map((d, i) =>
81
- Math.max(textWidth(d.label ?? "", 名前の級), textWidth(数値の文字[i] ?? "", 数値の級)),
83
+ Math.max(
84
+ textWidth(d.label ?? "", 名前の級, 0, textMetrics),
85
+ textWidth(数値の文字[i] ?? "", 数値の級, 0, textMetrics),
86
+ ),
82
87
  ),
83
88
  );
84
89
 
@@ -1,5 +1,5 @@
1
1
  import { type JSX } from "react";
2
- import type { ChartSlopeForm, LaidNode, Tone } from "../types";
2
+ import type { CdlTextMetrics, ChartSlopeForm, LaidNode, Tone } from "../types";
3
3
  import { TONE, chartSeriesColor } from "../render/tone";
4
4
  import { resolveChartData } from "../render/payload-binding";
5
5
  import { textWidth } from "./text-width";
@@ -99,6 +99,7 @@ export function ChartSlopeNode({
99
99
  stateValues = {},
100
100
  drawing = false,
101
101
  progress = 1,
102
+ textMetrics = "proportional",
102
103
  }: {
103
104
  node: LaidNode;
104
105
  active: boolean;
@@ -106,6 +107,7 @@ export function ChartSlopeNode({
106
107
  /** 段の `draw` にこの node が載っている間だけ true */
107
108
  drawing?: boolean;
108
109
  progress?: number;
110
+ textMetrics?: CdlTextMetrics;
109
111
  }): JSX.Element {
110
112
  const x0 = node.cx - node.w / 2;
111
113
  const y0 = node.cy - node.h / 2;
@@ -193,8 +195,8 @@ export function ChartSlopeNode({
193
195
  const 右の字 = 系列たち.map((s) =>
194
196
  形 === "delta" ? 増減表記(s.前, s.今) : 実値表記(s.今),
195
197
  );
196
- const 左値幅 = Math.max(0, ...左の字.map((t) => textWidth(t, 値の級)));
197
- const 右値幅 = Math.max(0, ...右の字.map((t) => textWidth(t, 値の級)));
198
+ const 左値幅 = Math.max(0, ...左の字.map((t) => textWidth(t, 値の級, 0, textMetrics)));
199
+ const 右値幅 = Math.max(0, ...右の字.map((t) => textWidth(t, 値の級, 0, textMetrics)));
198
200
 
199
201
  /** 名札に回せる横幅。 線の幅を先に取り分けてから、残りを左右へ半分ずつ配る */
200
202
  const 名前に使える = Math.max(
@@ -206,10 +208,16 @@ export function ChartSlopeNode({
206
208
  (右値幅 + 軸から値まで + 値から名前まで)) /
207
209
  2,
208
210
  );
209
- const 名前の級 = 収める級(系列たち.map((s) => s.名前), 名前に使える, 名前の上限級, 名前の下限級);
211
+ const 名前の級 = 収める級(
212
+ 系列たち.map((s) => s.名前),
213
+ 名前に使える,
214
+ 名前の上限級,
215
+ 名前の下限級,
216
+ textMetrics,
217
+ );
210
218
  const 名前幅 = Math.min(
211
219
  名前に使える,
212
- Math.max(0, ...系列たち.map((s) => textWidth(s.名前, 名前の級))),
220
+ Math.max(0, ...系列たち.map((s) => textWidth(s.名前, 名前の級, 0, textMetrics))),
213
221
  );
214
222
 
215
223
  const 左の余白 = 名前幅 + 値から名前まで + 左値幅 + 軸から値まで;
@@ -427,9 +435,20 @@ function 印の色(tone: Tone | undefined, idx: number): string {
427
435
  * 最長の名前が枠に収まるまで 1 ずつ下げ、下限で止める。 下限でも収まらない図では
428
436
  * 名札が枠から出るが、線と軸は箱の内側に残る。
429
437
  */
430
- function 収める級(名前たち: readonly string[], 幅: number, 上限: number, 下限: number): number {
438
+ function 収める級(
439
+ 名前たち: readonly string[],
440
+ 幅: number,
441
+ 上限: number,
442
+ 下限: number,
443
+ textMetrics: CdlTextMetrics = "proportional",
444
+ ): number {
431
445
  if (名前たち.length === 0) return 上限;
432
446
  let 級 = 上限;
433
- while (級 > 下限 && Math.max(...名前たち.map((n) => textWidth(n, 級))) > 幅) 級 -= 1;
447
+ while (
448
+ 級 > 下限 &&
449
+ Math.max(...名前たち.map((n) => textWidth(n, 級, 0, textMetrics))) > 幅
450
+ ) {
451
+ 級 -= 1;
452
+ }
434
453
  return 級;
435
454
  }
@@ -1,5 +1,5 @@
1
1
  import { useId, type JSX } from "react";
2
- import type { LaidNode, Tone } from "../types";
2
+ import type { CdlTextMetrics, LaidNode, Tone } from "../types";
3
3
  import { TONE, chartSeriesColor } from "../render/tone";
4
4
  import { resolveChartData, type ResolvedChartDatum } from "../render/payload-binding";
5
5
  import { textWidth } from "./text-width";
@@ -51,6 +51,7 @@ export function ChartStackedBarNode({
51
51
  stateValues = {},
52
52
  drawing = false,
53
53
  progress = 1,
54
+ textMetrics = "proportional",
54
55
  }: {
55
56
  node: LaidNode;
56
57
  active: boolean;
@@ -59,6 +60,7 @@ export function ChartStackedBarNode({
59
60
  drawing?: boolean;
60
61
  /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
61
62
  progress?: number;
63
+ textMetrics?: CdlTextMetrics;
62
64
  }): JSX.Element {
63
65
  const x0 = node.cx - node.w / 2;
64
66
  const y0 = node.cy - node.h / 2;
@@ -72,7 +74,8 @@ export function ChartStackedBarNode({
72
74
  const 時点の級 = 13;
73
75
  /** 時点の名前を置く左の幅 */
74
76
  const 時点の幅 =
75
- Math.max(0, ...帯たち.map((b) => textWidth(b.名前, 時点の級))) + (帯たち.length > 1 ? 16 : 0);
77
+ Math.max(0, ...帯たち.map((b) => textWidth(b.名前, 時点の級, 0, textMetrics))) +
78
+ (帯たち.length > 1 ? 16 : 0);
76
79
  const 帯の左 = PAD + 時点の幅;
77
80
  const 帯の幅 = Math.max(40, node.w - 帯の左 - PAD);
78
81
  const 帯の高さ = 44;
@@ -196,7 +199,7 @@ export function ChartStackedBarNode({
196
199
  })}
197
200
 
198
201
  {/* 一覧。 区画に字が入らなかった件もここで名前と色が引ける */}
199
- {一覧の並び(data, 帯の左, node.w - PAD, 名前の級).map(({ d, idx, x, 行 }) => {
202
+ {一覧の並び(data, 帯の左, node.w - PAD, 名前の級, textMetrics).map(({ d, idx, x, 行 }) => {
200
203
  const y =
201
204
  上端 +
202
205
  帯たち.length * (帯の高さ + 帯の間) -
@@ -284,12 +287,13 @@ export function 一覧の並び(
284
287
  左: number,
285
288
  右端: number,
286
289
  級: number,
290
+ textMetrics: CdlTextMetrics = "proportional",
287
291
  ): Array<{ d: ResolvedChartDatum; idx: number; x: number; 行: number }> {
288
292
  const out: Array<{ d: ResolvedChartDatum; idx: number; x: number; 行: number }> = [];
289
293
  let x = 左;
290
294
  let 行 = 0;
291
295
  data.forEach((d, idx) => {
292
- const 幅 = 印から名前まで + textWidth(d.label ?? "", 級);
296
+ const 幅 = 印から名前まで + textWidth(d.label ?? "", 級, 0, textMetrics);
293
297
  // 行の先頭では折り返さない = 折り返しても同じ位置に戻るだけで、空の行が増える
294
298
  if (x > 左 && x + 幅 > 右端) {
295
299
  行 += 1;
@@ -1,5 +1,5 @@
1
1
  import { type JSX } from "react";
2
- import type { LaidNode, Tone } from "../types";
2
+ import type { CdlTextMetrics, LaidNode, Tone } from "../types";
3
3
  import { TONE, chartSeriesColor } from "../render/tone";
4
4
  import { resolveChartData } from "../render/payload-binding";
5
5
  import { textWidth } from "./text-width";
@@ -35,10 +35,12 @@ export function ChartStatNode({
35
35
  node,
36
36
  active,
37
37
  stateValues = {},
38
+ textMetrics = "proportional",
38
39
  }: {
39
40
  node: LaidNode;
40
41
  active: boolean;
41
42
  stateValues?: Record<string, string>;
43
+ textMetrics?: CdlTextMetrics;
42
44
  }): JSX.Element {
43
45
  const x0 = node.cx - node.w / 2;
44
46
  const y0 = node.cy - node.h / 2;
@@ -87,7 +89,13 @@ export function ChartStatNode({
87
89
  const 色 = 印の色(d.tone, idx);
88
90
  const 数値 = 実値表記(d.value);
89
91
  // 数値は区画に収まるまで小さくする。 収まらないまま描くと隣の区画へはみ出す
90
- const 数値の級 = 収める級(数値, 枠.w - 区画.余白 * 2, 区画.数値の級, 区画.名前の級 + 1);
92
+ const 数値の級 = 収める級(
93
+ 数値,
94
+ 枠.w - 区画.余白 * 2,
95
+ 区画.数値の級,
96
+ 区画.名前の級 + 1,
97
+ textMetrics,
98
+ );
91
99
  return (
92
100
  <g
93
101
  key={`stat-${idx}`}
@@ -147,6 +155,7 @@ export function ChartStatNode({
147
155
  枠.w - 区画.余白 * 2,
148
156
  区画.前の級,
149
157
  8,
158
+ textMetrics,
150
159
  )}
151
160
  textAnchor="middle"
152
161
  fill="var(--cdl-text-dim, #5a6270)"
@@ -291,9 +300,15 @@ function 区画の割付(
291
300
  * 中身 (数値が主役) が崩れるため。 下限でも入らない場合は入らないまま描く = 勝手に字を
292
301
  * 落とすと数値が別の値に読める。
293
302
  */
294
- export function 収める級(字: string, 使える幅: number, 初期: number, 下限: number): number {
303
+ export function 収める級(
304
+ 字: string,
305
+ 使える幅: number,
306
+ 初期: number,
307
+ 下限: number,
308
+ textMetrics: CdlTextMetrics = "proportional",
309
+ ): number {
295
310
  let 級 = 初期;
296
- while (級 > 下限 && textWidth(字, 級) > 使える幅) 級 -= 1;
311
+ while (級 > 下限 && textWidth(字, 級, 0, textMetrics) > 使える幅) 級 -= 1;
297
312
  return 級;
298
313
  }
299
314
 
@@ -1,5 +1,5 @@
1
1
  import { type JSX } from "react";
2
- import type { LaidNode, Tone } from "../types";
2
+ import type { CdlTextMetrics, LaidNode, Tone } from "../types";
3
3
  import { TONE, chartSeriesColor } from "../render/tone";
4
4
  import { resolveChartData } from "../render/payload-binding";
5
5
  import { textWidth } from "./text-width";
@@ -84,6 +84,7 @@ export function ChartWaffleNode({
84
84
  stateValues = {},
85
85
  drawing = false,
86
86
  progress = 1,
87
+ textMetrics = "proportional",
87
88
  }: {
88
89
  node: LaidNode;
89
90
  active: boolean;
@@ -92,6 +93,7 @@ export function ChartWaffleNode({
92
93
  drawing?: boolean;
93
94
  /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
94
95
  progress?: number;
96
+ textMetrics?: CdlTextMetrics;
95
97
  }): JSX.Element {
96
98
  const x0 = node.cx - node.w / 2;
97
99
  const y0 = node.cy - node.h / 2;
@@ -116,7 +118,10 @@ export function ChartWaffleNode({
116
118
  Math.max(
117
119
  0,
118
120
  ...data.map((d, i) =>
119
- Math.max(textWidth(d.label ?? "", 名前の級), textWidth(数値の文字[i] ?? "", 数値の級)),
121
+ Math.max(
122
+ textWidth(d.label ?? "", 名前の級, 0, textMetrics),
123
+ textWidth(数値の文字[i] ?? "", 数値の級, 0, textMetrics),
124
+ ),
120
125
  ),
121
126
  );
122
127
 
@@ -75,24 +75,37 @@ export function DynShapeNode({
75
75
  }
76
76
  })();
77
77
 
78
- // 名前と値は箱の外に置く (#925)。 中に置くと、内側の塗りが値で動くたびに文字と重なる。
79
- //
80
- // **差し込みはここで通さない**。 `render/nodes.tsx` が `{名前}` を解決した節を渡してくるので、
81
- // ここで通しても何も変わらない (実測 = 通さない形に変えても検査が 1 件も落ちなかった)。
82
- // しかも向こうは値ごとの呼び名の表を持っており、ここには無い = 二重に通すと、
83
- // 呼び名を持つ図だけ結果が変わる余地を作る。
78
+ const 揃え = 箱いっぱいに描く(shape.kind) ? "start" : "middle";
79
+ return renderShapeWithOutsideLabels(形, node, 揃え);
80
+ }
81
+
82
+ /**
83
+ * 名前と値は箱の外に置く (#925)。 中に置くと、内側の塗りが値で動くたびに文字と重なる。
84
+ * 駅は塗りが動かないが、丸が小さく名前が丸より広いので外に置く。
85
+ *
86
+ * **差し込みはここで通さない**。 `render/nodes.tsx` が `{名前}` を解決した節を渡してくるので、
87
+ * ここで通しても何も変わらない (実測 = 通さない形に変えても検査が 1 件も落ちなかった)。
88
+ * しかも向こうは値ごとの呼び名の表を持っており、ここには無い = 二重に通すと、
89
+ * 呼び名を持つ図だけ結果が変わる余地を作る。
90
+ */
91
+ export function renderShapeWithOutsideLabels(
92
+ 形: JSX.Element,
93
+ node: LaidNode,
94
+ 揃え: "start" | "middle",
95
+ ): JSX.Element {
96
+ const cx = node.cx;
97
+ const y = node.cy - node.h / 2;
84
98
  const 名前 = (node.title ?? "").trim();
85
99
  const 値 = (node.subtitle ?? "").trim();
86
100
  // どちらも書かれていない節では包む `<g>` も足さない = 書かない図の DOM を 1 要素も変えない
87
101
  if (名前 === "" && 値 === "") return 形;
88
- const 揃え = 箱いっぱいに描く(shape.kind) ? "start" : "middle";
89
102
  return (
90
103
  <g>
91
104
  {形}
92
105
  {名前 !== "" && (
93
106
  <text
94
107
  data-cdl-role="shape-name"
95
- x={揃え === "start" ? x : cx}
108
+ x={揃え === "start" ? node.cx - node.w / 2 : cx}
96
109
  y={y - NAME_GAP}
97
110
  textAnchor={揃え}
98
111
  fontSize={13}
@@ -106,7 +119,7 @@ export function DynShapeNode({
106
119
  <text
107
120
  data-cdl-role="shape-value"
108
121
  x={cx}
109
- y={y + h + VALUE_GAP}
122
+ y={y + node.h + VALUE_GAP}
110
123
  textAnchor="middle"
111
124
  fontSize={12}
112
125
  fill="var(--cdl-text-dim, #5a6270)"
@@ -1,5 +1,5 @@
1
1
  import { useId, type JSX } from "react";
2
- import type { LaidNode, Tone } from "../types";
2
+ import type { CdlTextMetrics, LaidNode, Tone } from "../types";
3
3
  import { TONE } from "../render/tone";
4
4
  import { resolveFunnelData } from "../render/payload-binding";
5
5
  import { 収める倍率 } from "../layout/text-width";
@@ -18,6 +18,7 @@ export function FunnelNode({
18
18
  stateValues = {},
19
19
  drawing = false,
20
20
  progress = 1,
21
+ textMetrics = "proportional",
21
22
  }: {
22
23
  node: LaidNode;
23
24
  active: boolean;
@@ -26,6 +27,7 @@ export function FunnelNode({
26
27
  drawing?: boolean;
27
28
  /** phase の進み (0→1)。 `drawing` が true の時だけ読む */
28
29
  progress?: number;
30
+ textMetrics?: CdlTextMetrics;
29
31
  }): JSX.Element {
30
32
  const x0 = node.cx - node.w / 2;
31
33
  const y0 = node.cy - node.h / 2;
@@ -109,7 +111,12 @@ export function FunnelNode({
109
111
  const 補足の枠字 = 補足の字 * 縦倍;
110
112
  const 収まる幅 = Math.max(0, bottomW - 16);
111
113
  const 補足の実字 =
112
- 補足の枠字 * 収める倍率(補足, { fontSize: 補足の枠字, fontWeight: 500 }, 収まる幅);
114
+ 補足の枠字 *
115
+ 収める倍率(
116
+ 補足,
117
+ { fontSize: 補足の枠字, fontWeight: 500, textMetrics },
118
+ 収まる幅,
119
+ );
113
120
  // 2 行の塊を段の中央に積む。 縦の位置は枠の字 (縮める前) で決める = 幅で縮めた分だけ
114
121
  // 行が上下に動くと、段ごとに行の高さが揃わない
115
122
  const 塊の上 = y + stageH / 2 - (名前の実字 * 1.2 + 補足の枠字 * 1.25) / 2;
@@ -1,6 +1,6 @@
1
1
  import { 等幅の字 } from "../render/mono-text";
2
2
  import type { JSX } from "react";
3
- import type { LaidNode, NodeKind } from "../types";
3
+ import type { CdlTextMetrics, LaidNode, NodeKind } from "../types";
4
4
  import { toneColor } from "./node-tone";
5
5
  import { interpolate } from "../render/utils";
6
6
  import type { 値の名前の表 } from "../input-option";
@@ -237,6 +237,7 @@ export function GenericNode({
237
237
  value,
238
238
  stateValues,
239
239
  値の名前,
240
+ textMetrics = "proportional",
240
241
  }: {
241
242
  node: LaidNode;
242
243
  active: boolean;
@@ -244,6 +245,7 @@ export function GenericNode({
244
245
  stateValues: Record<string, string>;
245
246
  /** 入力欄の値ごとの名前 (#855)。 値と行の差し込みに使う */
246
247
  値の名前?: 値の名前の表;
248
+ textMetrics?: CdlTextMetrics;
247
249
  }): JSX.Element {
248
250
  const base = STYLES[node.kind] ?? FALLBACK;
249
251
  // `tone` 指定があれば kind の既定色を置き換える。 色を決める式は `node-tone.ts` に集約。
@@ -458,7 +460,8 @@ export function GenericNode({
458
460
  // 左列は比例の書体で描く (`font-family` を指定していないので継承する)。 字数に一定を
459
461
  // 掛ける形では実物に届かず、 右列が左列の字に重なる (`#425`)。 字ごとの幅で見積もる。
460
462
  const leftMaxWidth = splits.reduce(
461
- (m, s) => Math.max(m, rowColumnWidth(s.left, GENERIC_ROW_LEFT_SIZE_PX, false)),
463
+ (m, s) =>
464
+ Math.max(m, rowColumnWidth(s.left, GENERIC_ROW_LEFT_SIZE_PX, false, textMetrics)),
462
465
  0,
463
466
  );
464
467
  const rightColX = ROW_PAD_X_PX + leftMaxWidth + ROW_COL_GAP_PX;