@cardenelabs/cdl 0.31.1 → 0.33.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.
@@ -1,7 +1,7 @@
1
1
  import { useId, type JSX } from "react";
2
- import type { LaidNode, Tone } from "../types";
2
+ import type { ChartPieForm, LaidNode, Tone } from "../types";
3
3
  import { TONE, chartSeriesColor } from "../render/tone";
4
- import { resolveChartData } from "../render/payload-binding";
4
+ import { resolveChartData, type ResolvedChartDatum } from "../render/payload-binding";
5
5
  import { 進みを畳む } from "./draw-progress";
6
6
  import { textWidth } from "./text-width";
7
7
 
@@ -35,6 +35,13 @@ import { textWidth } from "./text-width";
35
35
  *
36
36
  * 各 value は数でも `{signal}` でもよく、 後者は状態が動くたび扇の角度が追随する。
37
37
  *
38
+ * ## 前の時点を内側の輪に重ねる (cdl#679)
39
+ *
40
+ * **1 件でも `previous` を持てば輪が 2 つになる**。 持たない件は前の時点で 0 として扱う =
41
+ * 件を落とすと内外で色の並びが対応しなくなる。
42
+ *
43
+ * 並びは 前 → 今、すなわち 内 → 外。 3 時点以上は持たない。
44
+ *
38
45
  * phase の `draw` にこの node の id が載っている間は、 円が 12 時から時計回りに開く (cdl#516)。
39
46
  * 開き方は扇形の切り抜き (`clipPath`) で、 各扇の `d` は **進みに関わらず実値のまま**。
40
47
  * 名札はその扇が開き始めた時に出る。
@@ -61,6 +68,24 @@ export function ChartPieNode({
61
68
  const y0 = node.cy - node.h / 2;
62
69
  const data = resolveChartData(node.chartData ?? [], stateValues);
63
70
  const total = data.reduce((acc, d) => acc + d.value, 0) || 1;
71
+ const 前あり = data.some((d) => d.previous !== undefined);
72
+ const 今の値 = data.map((d) => Math.max(0, d.value));
73
+ const 前の値 = data.map((d) => Math.max(0, d.previous ?? 0));
74
+ const 今の合計 = 今の値.reduce((acc, value) => acc + value, 0);
75
+ const 前の合計 = 前の値.reduce((acc, value) => acc + value, 0);
76
+ // `previous` を書かない図は、既存の扇の `d` を 1 byte も変えない。
77
+ const 今の割合 = 前あり
78
+ ? 今の値.map((value) => (今の合計 > 0 ? value / 今の合計 : 0))
79
+ : data.map((d) => d.value / total);
80
+ const 前の割合 = 前の値.map((value) => (前の合計 > 0 ? value / 前の合計 : 0));
81
+
82
+ /**
83
+ * 見せ方 (cdl#684)。 **型の外の値は `ring` に落とす** = 見せ方が決まらないより、
84
+ * 既定の形が出る方がよい (`rules` の「判定できなかったことを値に潰さない」 は判定の話で、
85
+ * ここは描くか描かないかなので安全側 = 描ける形へ倒す)。
86
+ */
87
+ const 形: ChartPieForm =
88
+ node.chartPieForm === "arcs" || node.chartPieForm === "table" ? node.chartPieForm : "ring";
64
89
 
65
90
  const PAD_TOP = 20;
66
91
  const PAD_BOTTOM = 20;
@@ -68,8 +93,16 @@ export function ChartPieNode({
68
93
  const PAD_SIDE = 20;
69
94
  const chartAreaH = node.h - PAD_TOP - PAD_BOTTOM;
70
95
 
71
- const 名札の級 = 14;
72
- const 数値の級 = 13;
96
+ /**
97
+ * 名札の 2 行の級 (cdl#682)。
98
+ *
99
+ * **割合を主役にする**。 扇の大きさは値で決まるのに、以前は名前が級 14 の太字、
100
+ * 値が級 13 の薄い色で、字の重みと図の重みが逆を向いていた。
101
+ *
102
+ * 名前は縁の色の点で扇と既に結ばれているので、従に落として困らない。
103
+ */
104
+ const 割合の級 = 19;
105
+ const 名前の級 = 12;
73
106
 
74
107
  /** 円の縁から引き出し線が折れるまで */
75
108
  const 引き出し = 18;
@@ -78,24 +111,45 @@ export function ChartPieNode({
78
111
  /**
79
112
  * 名札が折れ点から縦に張り出す量。
80
113
  *
81
- * 名前は `折れy - 3` の位置に級 14 で置くので上へ約 17、数値は `折れy + 15` に級 13 で
82
- * 置くので下へ約 19 伸びる。 大きい方を採る = 上下どちらに出ても収まる。
114
+ * 割合は `折れy - 1` の位置に級 19 で置くので上へ約 15、名前は `折れy + 15` に級 12 で
115
+ * 置くので下へ約 18 伸びる。 大きい方を採る = 上下どちらに出ても収まる。
83
116
  */
84
117
  const 名札の縦の張り出し = 20;
85
118
 
86
- const 割合の文字 = data.map((d) => 割合表記(d.value / total));
87
- const 数値の文字 = data.map((d, i) => `${実値表記(d.value)} ${割合の文字[i]}`);
119
+ const 割合の文字 = 今の割合.map(割合表記);
120
+ /**
121
+ * 名札の 2 行目 (cdl#682)。 名前を置き、**割合と違う数になる時だけ** 実数を添える。
122
+ *
123
+ * 合計が 100 の図では実値と割合が同じ数になる。 以前は `30 30%` と 2 度出ており、
124
+ * SVG は続く空白を 1 つに畳むので `30 30%` と描かれて、どこまでが実数か読めなかった。
125
+ *
126
+ * 判定は **表記どうしの比較** で行う。 合計を別に数えると、丸めた後で一致する形
127
+ * (`44.96` が `45%` になる等) を取りこぼす。
128
+ */
129
+ const 名前の文字 = data.map((d, i) => {
130
+ const 名 = d.label ?? "";
131
+ if (前あり) return `${名} ${実値表記(前の値[i]!)} → ${実値表記(今の値[i]!)}`;
132
+ return `${実値表記(d.value)}%` === 割合の文字[i] ? 名 : `${名} ${実値表記(d.value)}`;
133
+ });
134
+
135
+ if (形 !== "ring") {
136
+ const 共通 = { node, active, data, 割合: 今の割合, x0, y0, drawing, progress, instanceId };
137
+ return 形 === "arcs" ? 弧の形(共通) : 表の形(共通);
138
+ }
88
139
 
89
140
  /**
90
141
  * 名札 1 つが横に使う幅。 **中身から決める** = 割り当てを固定すると使わない分が死ぬ
91
142
  * (cdl#542 で凡例について踏んだのと同じ)。
92
143
  *
93
- * 名前の行と数値の行のうち長い方を採る。 短い方に合わせると長い行が枠から出る。
144
+ * 割合の行と名前の行のうち長い方を採る。 短い方に合わせると長い行が枠から出る。
94
145
  */
95
146
  const 名札幅 = Math.max(
96
147
  0,
97
- ...data.map((d, i) =>
98
- Math.max(textWidth(d.label ?? "", 名札の級), textWidth(数値の文字[i] ?? "", 数値の級)),
148
+ ...data.map((_d, i) =>
149
+ Math.max(
150
+ textWidth(割合の文字[i] ?? "", 割合の級),
151
+ textWidth(名前の文字[i] ?? "", 名前の級),
152
+ ),
99
153
  ),
100
154
  );
101
155
 
@@ -115,42 +169,17 @@ export function ChartPieNode({
115
169
  const 縦から = chartAreaH / 2 - 引き出し - 名札の縦の張り出し;
116
170
  const 横から = (node.w - PAD_SIDE * 2) / 2 - 引き出し - 字までの間 - 名札幅;
117
171
  const radius = Math.max(24, Math.min(縦から, 横から));
118
- /** 中央を抜く半径。 合計を置ける広さを残しつつ、輪が細くなりすぎない比 */
119
- const 内半径 = radius * 0.58;
172
+ /** 中央を抜く半径。 2 輪では前の時点との間にも余白を残す */
173
+ const 外の内半径 = radius * (前あり ? 0.75 : 0.58);
174
+ const 前の外半径 = radius * 0.67;
175
+ const 前の内半径 = radius * 0.42;
120
176
 
121
177
  /** 円は枠の中央に置く。 名札が左右へ均等に出る */
122
178
  const cx = node.w / 2;
123
179
  const cy = PAD_TOP + chartAreaH / 2;
124
180
 
125
- let cumAngle = -Math.PI / 2;
126
- /** その扇が始まるまでの取り分の累計。 開き始めたかの判定に使う (cdl#516) */
127
- let cumFrac = 0;
128
- const slices = data.map((d) => {
129
- const frac = d.value / total;
130
- const angle = frac * Math.PI * 2;
131
- const a0 = cumAngle;
132
- const a1 = cumAngle + angle;
133
- cumAngle = a1;
134
- const startFrac = cumFrac;
135
- cumFrac += frac;
136
-
137
- const x1 = cx + radius * Math.cos(a0);
138
- const y1 = cy + radius * Math.sin(a0);
139
- const x2 = cx + radius * Math.cos(a1);
140
- const y2 = cy + radius * Math.sin(a1);
141
- const x3 = cx + 内半径 * Math.cos(a1);
142
- const y3 = cy + 内半径 * Math.sin(a1);
143
- const x4 = cx + 内半径 * Math.cos(a0);
144
- const y4 = cy + 内半径 * Math.sin(a0);
145
- const large = angle > Math.PI ? 1 : 0;
146
- // 外周を時計回りに描き、内周を逆向きに戻って閉じる = 中央が抜けた輪になる
147
- const path =
148
- `M ${x1} ${y1} A ${radius} ${radius} 0 ${large} 1 ${x2} ${y2} ` +
149
- `L ${x3} ${y3} A ${内半径} ${内半径} 0 ${large} 0 ${x4} ${y4} Z`;
150
- /** 引き出し線の起点。 扇の真ん中の向き */
151
- const 中央角 = (a0 + a1) / 2;
152
- return { d, frac, path, startFrac, 中央角 };
153
- });
181
+ const 前のslices = 前あり ? 輪を組む(data, cx, cy, 前の外半径, 前の内半径, 前の割合) : [];
182
+ const slices = 輪を組む(data, cx, cy, radius, 外の内半径, 今の割合);
154
183
 
155
184
  /** 円がどこまで開いたか (0..1、 cdl#516)。 畳み方は `draw-progress.ts` が持つ */
156
185
  const 開き = 進みを畳む(drawing, progress);
@@ -190,6 +219,18 @@ export function ChartPieNode({
190
219
  描く指定が無い図には包みも `clip-path` も足さない = 既存の図の DOM を変えない
191
220
  */}
192
221
  <g {...(drawing ? { clipPath: `url(#${切り抜きid})` } : {})} filter={`url(#${影id})`}>
222
+ {前のslices.map((s, idx) => (
223
+ <path
224
+ key={`slice-previous-${idx}`}
225
+ data-cdl-role="chart-pie-slice-previous"
226
+ data-cdl-unresolved={s.d.unresolved ? "true" : undefined}
227
+ d={s.path}
228
+ fill={sliceColor(s.d.tone, idx)}
229
+ stroke="var(--cdl-node-fill, #ffffff)"
230
+ strokeWidth={3}
231
+ strokeLinejoin="round"
232
+ />
233
+ ))}
193
234
  {slices.map((s, idx) => (
194
235
  <path
195
236
  key={`slice-${idx}`}
@@ -220,7 +261,7 @@ export function ChartPieNode({
220
261
  fontWeight={600}
221
262
  fill="var(--cdl-text, #1a1f2a)"
222
263
  >
223
- {実値表記(Math.round(total))}
264
+ {実値表記(Math.round(前あり ? 今の合計 : total))}
224
265
  </text>
225
266
  <text
226
267
  x={cx}
@@ -256,25 +297,365 @@ export function ChartPieNode({
256
297
  />
257
298
  {/* 起点の点。 どの扇から伸びた線かを色で示す */}
258
299
  <circle cx={縁x} cy={縁y} r={3} fill={sliceColor(s.d.tone, idx)} />
300
+ {/* 1 行目は割合。 読ませたい数を大きく置く (cdl#682) */}
259
301
  <text
260
302
  x={字x}
261
- y={折れy - 3}
303
+ y={折れy - 1}
262
304
  textAnchor={右 ? "start" : "end"}
263
- fontSize={名札の級}
305
+ fontSize={割合の級}
264
306
  fontWeight={600}
265
307
  fill="var(--cdl-text, #1a1f2a)"
266
308
  >
267
- {s.d.label}
309
+ {割合の文字[idx]}
268
310
  </text>
269
311
  <text
270
312
  x={字x}
271
313
  y={折れy + 15}
272
314
  textAnchor={右 ? "start" : "end"}
273
- fontSize={数値の級}
315
+ fontSize={名前の級}
274
316
  fill="var(--cdl-text-dim, #5a6270)"
275
317
  >
276
- {数値の文字[idx]}
318
+ {名前の文字[idx]}
319
+ </text>
320
+ </g>
321
+ );
322
+ })}
323
+ </g>
324
+ );
325
+ }
326
+
327
+ type PieSlice = {
328
+ d: ResolvedChartDatum;
329
+ frac: number;
330
+ path: string;
331
+ startFrac: number;
332
+ 中央角: number;
333
+ };
334
+
335
+ /** 半径と取り分から、12 時始まり・時計回りの 1 輪を組む。 */
336
+ function 輪を組む(
337
+ data: readonly ResolvedChartDatum[],
338
+ cx: number,
339
+ cy: number,
340
+ 外半径: number,
341
+ 内半径: number,
342
+ 割合: readonly number[],
343
+ ): PieSlice[] {
344
+ let cumAngle = -Math.PI / 2;
345
+ /** その扇が始まるまでの取り分の累計。 開き始めたかの判定に使う (cdl#516) */
346
+ let cumFrac = 0;
347
+ return data.map((d, idx) => {
348
+ const frac = 割合[idx] ?? 0;
349
+ const angle = frac * Math.PI * 2;
350
+ const a0 = cumAngle;
351
+ const a1 = cumAngle + angle;
352
+ cumAngle = a1;
353
+ const startFrac = cumFrac;
354
+ cumFrac += frac;
355
+
356
+ const x1 = cx + 外半径 * Math.cos(a0);
357
+ const y1 = cy + 外半径 * Math.sin(a0);
358
+ const x2 = cx + 外半径 * Math.cos(a1);
359
+ const y2 = cy + 外半径 * Math.sin(a1);
360
+ const x3 = cx + 内半径 * Math.cos(a1);
361
+ const y3 = cy + 内半径 * Math.sin(a1);
362
+ const x4 = cx + 内半径 * Math.cos(a0);
363
+ const y4 = cy + 内半径 * Math.sin(a0);
364
+ const large = angle > Math.PI ? 1 : 0;
365
+ // 外周を時計回りに描き、内周を逆向きに戻って閉じる = 中央が抜けた輪になる
366
+ const path =
367
+ `M ${x1} ${y1} A ${外半径} ${外半径} 0 ${large} 1 ${x2} ${y2} ` +
368
+ `L ${x3} ${y3} A ${内半径} ${内半径} 0 ${large} 0 ${x4} ${y4} Z`;
369
+ return { d, frac, path, startFrac, 中央角: (a0 + a1) / 2 };
370
+ });
371
+ }
372
+
373
+ /** `ring` 以外の見せ方が受け取るもの */
374
+ type 形の引数 = {
375
+ node: LaidNode;
376
+ active: boolean;
377
+ data: ResolvedChartDatum[];
378
+ /** その系列の取り分 (0..1)。 並びは `data` と同じ */
379
+ 割合: number[];
380
+ x0: number;
381
+ y0: number;
382
+ drawing: boolean;
383
+ progress: number;
384
+ instanceId: string;
385
+ };
386
+
387
+ /** 3 つの見せ方が共通で持つ枠 (cdl#684) */
388
+ function 枠(node: LaidNode, active: boolean): JSX.Element {
389
+ return (
390
+ <rect
391
+ data-cdl-role="node-body"
392
+ x={0}
393
+ y={0}
394
+ width={node.w}
395
+ height={node.h}
396
+ rx={16}
397
+ fill="var(--cdl-node-fill, #ffffff)"
398
+ stroke={active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)"}
399
+ strokeWidth={active ? 3 : 1.25}
400
+ />
401
+ );
402
+ }
403
+
404
+ /**
405
+ * `arcs` = 系列ごとに 1 本の弧 (cdl#684)。
406
+ *
407
+ * 円を切り分けないので、**割合を角度ではなく弧の長さで読む**。 長さの比較は角度の比較より
408
+ * 速く、合計が 100 にならない図 (系列ごとの達成率を並べる等) でも形が壊れない。
409
+ *
410
+ * 背後に同じ太さの薄い輪を敷いて全体 (100%) を示す = 弧だけだと「どこまでで 1 周か」 が
411
+ * 図から消える。
412
+ *
413
+ * 中心に合計は置かない。 弧は合計を形で示さないので、置くと形と数が食い違う。
414
+ */
415
+ function 弧の形({ node, active, data, 割合, x0, y0, drawing, progress }: 形の引数): JSX.Element {
416
+ const PAD = 20;
417
+ const 名前の級 = 14;
418
+ const 割合の級 = 24;
419
+ const 印から名前まで = 18;
420
+ const 名前から割合まで = 24;
421
+
422
+ const 割合の文字 = 割合.map(割合表記);
423
+ const 一覧幅 =
424
+ 10 +
425
+ 印から名前まで +
426
+ Math.max(0, ...data.map((d) => textWidth(d.label ?? "", 名前の級))) +
427
+ 名前から割合まで +
428
+ Math.max(0, ...割合の文字.map((t) => textWidth(t, 割合の級)));
429
+
430
+ const 縦から = (node.h - PAD * 2) / 2;
431
+ const 横から = (node.w - PAD * 2 - 一覧幅 - 28) / 2;
432
+ const R = Math.max(24, Math.min(縦から, 横から));
433
+ const cx = PAD + R;
434
+ const cy = node.h / 2;
435
+
436
+ /**
437
+ * 弧 1 本の太さと、外から内への刻み。
438
+ *
439
+ * **中心に穴を残す** = 内側まで詰めると、いちばん内の弧が円に潰れて長さを読めなくなる
440
+ * (実測で外半径 140 / 3 本のとき、穴を残さない配り方では内の弧の半径が 37 に対し
441
+ * 太さが 30 になった)。 穴は外半径の 34%。
442
+ *
443
+ * 太さは刻みの 60% にして、弧と弧の間に地を残す。
444
+ */
445
+ const 穴 = R * 0.34;
446
+ const 刻み = (R - 穴) / Math.max(1, data.length);
447
+ const 太さ = Math.max(3, 刻み * 0.6);
448
+
449
+ const 開き = 進みを畳む(drawing, progress);
450
+
451
+ const 一覧左 = node.w - PAD - 一覧幅;
452
+ const 行の間 = Math.min(56, (node.h - PAD * 2) / Math.max(1, data.length));
453
+ const 一覧上 = node.h / 2 - (行の間 * (data.length - 1)) / 2;
454
+
455
+ return (
456
+ <g transform={`translate(${x0} ${y0})`}>
457
+ {枠(node, active)}
458
+ {data.map((d, idx) => {
459
+ const r = R - 太さ / 2 - idx * 刻み;
460
+ if (r <= 太さ / 2) return null;
461
+ /** 描いている間は弧も同じ進みで伸びる = 3 本が揃って回る */
462
+ const 取り分 = (割合[idx] ?? 0) * (drawing ? 開き : 1);
463
+ return (
464
+ <g key={`arc-${idx}`}>
465
+ <circle
466
+ data-cdl-role="chart-pie-arc-track"
467
+ cx={cx}
468
+ cy={cy}
469
+ r={r}
470
+ fill="none"
471
+ stroke="var(--cdl-divider, #d4d4d8)"
472
+ strokeWidth={太さ}
473
+ />
474
+ <path
475
+ data-cdl-role="chart-pie-arc"
476
+ data-cdl-unresolved={d.unresolved ? "true" : undefined}
477
+ d={弧の道(cx, cy, r, 取り分)}
478
+ fill="none"
479
+ stroke={sliceColor(d.tone, idx)}
480
+ strokeWidth={太さ}
481
+ strokeLinecap="round"
482
+ />
483
+ </g>
484
+ );
485
+ })}
486
+ {data.map((d, idx) => {
487
+ const y = 一覧上 + 行の間 * idx;
488
+ return (
489
+ <g key={`row-${idx}`}>
490
+ <rect
491
+ x={一覧左}
492
+ y={y - 9}
493
+ width={10}
494
+ height={10}
495
+ rx={2}
496
+ fill={sliceColor(d.tone, idx)}
497
+ />
498
+ <text
499
+ x={一覧左 + 印から名前まで}
500
+ y={y}
501
+ fontSize={名前の級}
502
+ fill="var(--cdl-text, #1a1f2a)"
503
+ >
504
+ {d.label}
505
+ </text>
506
+ <text
507
+ x={node.w - PAD}
508
+ y={y + 4}
509
+ textAnchor="end"
510
+ fontSize={割合の級}
511
+ fontWeight={600}
512
+ fill="var(--cdl-text, #1a1f2a)"
513
+ >
514
+ {割合の文字[idx]}
515
+ </text>
516
+ </g>
517
+ );
518
+ })}
519
+ </g>
520
+ );
521
+ }
522
+
523
+ /**
524
+ * `table` = 図と表を左右に据える (cdl#684)。
525
+ *
526
+ * 割合と実数の両方を表で読ませる。 件数や金額の図で、割合だけでは足りない時に効く。
527
+ *
528
+ * 引き出し線を引かない = 対応は表の色の印が担う。 線と表を両方置くと、同じ対応を
529
+ * 2 つの仕掛けで示すことになる。
530
+ */
531
+ function 表の形({ node, active, data, 割合, x0, y0, drawing, progress, instanceId }: 形の引数): JSX.Element {
532
+ const PAD = 20;
533
+ const 見出しの級 = 11;
534
+ const 名前の級 = 15;
535
+ const 割合の級 = 17;
536
+ const 実数の級 = 15;
537
+ const 印から名前まで = 18;
538
+
539
+ const 割合の文字 = 割合.map(割合表記);
540
+ const 実数の文字 = data.map((d) => 実値表記(d.value));
541
+ const 名前幅 = Math.max(0, ...data.map((d) => textWidth(d.label ?? "", 名前の級)));
542
+ const 割合幅 = Math.max(textWidth("割合", 見出しの級), ...割合の文字.map((t) => textWidth(t, 割合の級)));
543
+ const 実数幅 = Math.max(textWidth("値", 見出しの級), ...実数の文字.map((t) => textWidth(t, 実数の級)));
544
+ const 表幅 = 10 + 印から名前まで + 名前幅 + 28 + 割合幅 + 24 + 実数幅;
545
+
546
+ const 縦から = (node.h - PAD * 2) / 2;
547
+ const 横から = (node.w - PAD * 2 - 表幅 - 28) / 2;
548
+ const R = Math.max(24, Math.min(縦から, 横から));
549
+ const 内半径 = R * 0.58;
550
+ const cx = PAD + R;
551
+ const cy = node.h / 2;
552
+
553
+ const 表左 = node.w - PAD - 表幅;
554
+ const 右端 = node.w - PAD;
555
+ const 割合右 = 右端 - 実数幅 - 24;
556
+
557
+ const 行の間 = Math.min(52, (node.h - PAD * 2 - 40) / Math.max(1, data.length));
558
+ const 見出しのy = node.h / 2 - (行の間 * data.length) / 2 - 14;
559
+
560
+ const 開き = 進みを畳む(drawing, progress);
561
+ const 切り抜きid = `cdl-pie-draw-${instanceId}`;
562
+ const 開き始めた = (始まり: number): boolean => !drawing || 開き > 始まり;
563
+
564
+ let 角 = -Math.PI / 2;
565
+ let 累計 = 0;
566
+ const 扇たち = data.map((d, idx) => {
567
+ const frac = 割合[idx] ?? 0;
568
+ const a0 = 角;
569
+ const a1 = 角 + frac * Math.PI * 2;
570
+ 角 = a1;
571
+ const 始まり = 累計;
572
+ 累計 += frac;
573
+ return { d, idx, 始まり, path: 輪の道(cx, cy, R, 内半径, a0, a1) };
574
+ });
575
+
576
+ return (
577
+ <g transform={`translate(${x0} ${y0})`}>
578
+ {枠(node, active)}
579
+ {drawing && (
580
+ <defs>
581
+ <clipPath id={切り抜きid}>{扇形の切り抜き(cx, cy, R + 2, 開き)}</clipPath>
582
+ </defs>
583
+ )}
584
+ <g {...(drawing ? { clipPath: `url(#${切り抜きid})` } : {})}>
585
+ {扇たち.map((s) => (
586
+ <path
587
+ key={`slice-${s.idx}`}
588
+ data-cdl-role="chart-pie-slice"
589
+ data-cdl-unresolved={s.d.unresolved ? "true" : undefined}
590
+ d={s.path}
591
+ fill={sliceColor(s.d.tone, s.idx)}
592
+ stroke="var(--cdl-node-fill, #ffffff)"
593
+ strokeWidth={3}
594
+ strokeLinejoin="round"
595
+ />
596
+ ))}
597
+ </g>
598
+ <text
599
+ x={cx}
600
+ y={cy - 2}
601
+ textAnchor="middle"
602
+ fontSize={Math.round(R * 0.26)}
603
+ fontWeight={600}
604
+ fill="var(--cdl-text, #1a1f2a)"
605
+ >
606
+ {実値表記(Math.round(data.reduce((acc, d) => acc + d.value, 0)))}
607
+ </text>
608
+ <text
609
+ x={cx}
610
+ y={cy + Math.round(R * 0.2)}
611
+ textAnchor="middle"
612
+ fontSize={Math.round(R * 0.12)}
613
+ fill="var(--cdl-text-dim, #5a6270)"
614
+ >
615
+ 合計
616
+ </text>
617
+ <text x={表左} y={見出しのy} fontSize={見出しの級} fill="var(--cdl-text-dim, #5a6270)">
618
+ 内訳
619
+ </text>
620
+ <text x={割合右} y={見出しのy} textAnchor="end" fontSize={見出しの級} fill="var(--cdl-text-dim, #5a6270)">
621
+ 割合
622
+ </text>
623
+ <text x={右端} y={見出しのy} textAnchor="end" fontSize={見出しの級} fill="var(--cdl-text-dim, #5a6270)">
624
+ 値
625
+ </text>
626
+ <line
627
+ data-cdl-role="chart-pie-table-rule"
628
+ x1={表左}
629
+ y1={見出しのy + 10}
630
+ x2={右端}
631
+ y2={見出しのy + 10}
632
+ stroke="var(--cdl-text, #1a1f2a)"
633
+ strokeWidth={1.2}
634
+ />
635
+ {扇たち.map((s) => {
636
+ if (!開き始めた(s.始まり)) return null;
637
+ const y = 見出しのy + 40 + 行の間 * s.idx;
638
+ return (
639
+ <g key={`row-${s.idx}`}>
640
+ <rect x={表左} y={y - 10} width={10} height={10} rx={2} fill={sliceColor(s.d.tone, s.idx)} />
641
+ <text x={表左 + 印から名前まで} y={y} fontSize={名前の級} fill="var(--cdl-text, #1a1f2a)">
642
+ {s.d.label}
643
+ </text>
644
+ <text x={割合右} y={y} textAnchor="end" fontSize={割合の級} fontWeight={600} fill="var(--cdl-text, #1a1f2a)">
645
+ {割合の文字[s.idx]}
646
+ </text>
647
+ <text x={右端} y={y} textAnchor="end" fontSize={実数の級} fill="var(--cdl-text-dim, #5a6270)">
648
+ {実数の文字[s.idx]}
277
649
  </text>
650
+ <line
651
+ data-cdl-role="chart-pie-table-rule"
652
+ x1={表左}
653
+ y1={y + 16}
654
+ x2={右端}
655
+ y2={y + 16}
656
+ stroke="var(--cdl-divider, #d4d4d8)"
657
+ strokeWidth={1}
658
+ />
278
659
  </g>
279
660
  );
280
661
  })}
@@ -282,6 +663,28 @@ export function ChartPieNode({
282
663
  );
283
664
  }
284
665
 
666
+ /** 12 時から時計回りに、取り分ぶんの弧を引く道 (cdl#684) */
667
+ function 弧の道(cx: number, cy: number, r: number, 取り分: number): string {
668
+ const 実効 = Math.max(0, Math.min(0.9999, 取り分));
669
+ const a0 = -Math.PI / 2;
670
+ const a1 = a0 + 実効 * Math.PI * 2;
671
+ const large = 実効 > 0.5 ? 1 : 0;
672
+ return (
673
+ `M ${cx + r * Math.cos(a0)} ${cy + r * Math.sin(a0)} ` +
674
+ `A ${r} ${r} 0 ${large} 1 ${cx + r * Math.cos(a1)} ${cy + r * Math.sin(a1)}`
675
+ );
676
+ }
677
+
678
+ /** 中央が抜けた扇 1 枚の道。 外周を時計回りに描き、内周を逆向きに戻って閉じる */
679
+ function 輪の道(cx: number, cy: number, R: number, r: number, a0: number, a1: number): string {
680
+ const large = a1 - a0 > Math.PI ? 1 : 0;
681
+ return (
682
+ `M ${cx + R * Math.cos(a0)} ${cy + R * Math.sin(a0)} ` +
683
+ `A ${R} ${R} 0 ${large} 1 ${cx + R * Math.cos(a1)} ${cy + R * Math.sin(a1)} ` +
684
+ `L ${cx + r * Math.cos(a1)} ${cy + r * Math.sin(a1)} ` +
685
+ `A ${r} ${r} 0 ${large} 0 ${cx + r * Math.cos(a0)} ${cy + r * Math.sin(a0)} Z`
686
+ );
687
+ }
285
688
 
286
689
  /**
287
690
  * 12 時から時計回りに開く扇形 (cdl#516)。 円を切り抜く形に使う。