@cardenelabs/cdl 0.37.0 → 0.39.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.
- package/CHANGELOG.md +46 -1
- package/SPEC.md +6 -1
- package/dist/index.cjs +170 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +170 -110
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +170 -110
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +170 -110
- package/dist/react.js.map +1 -1
- package/dist/{render-BYdmSviQ.d.cts → render-B1ArenZ7.d.cts} +4 -0
- package/dist/{render-BYdmSviQ.d.ts → render-B1ArenZ7.d.ts} +4 -0
- package/package.json +1 -1
- package/src/anim/core/timeline.ts +28 -10
- package/src/anim/core/types.ts +12 -0
- package/src/kinds/chart-gauge.tsx +49 -14
- package/src/kinds/chart-pie.tsx +1 -16
- package/src/kinds/chart-radial.tsx +45 -11
- package/src/kinds/chart-stacked-bar.tsx +96 -54
- package/src/kinds/draw-arc.tsx +42 -0
- package/src/render/nodes.tsx +31 -6
- package/src/render/stage.tsx +17 -3
- package/src/render.tsx +8 -1
- package/src/types.ts +4 -0
- package/src/validate.ts +6 -0
|
@@ -2347,11 +2347,15 @@ type CdlPhase = {
|
|
|
2347
2347
|
* | `chart-line` | 左端 | 線が右へ伸びる | 点と数字は線の先が届いた時 |
|
|
2348
2348
|
* | `chart-bar` | 横軸 | 棒が上へ伸びる | 数字は棒の頭に付いて動く |
|
|
2349
2349
|
* | `chart-pie` | 12 時 | 扇が時計回りに開く | 凡例はその扇が開き始めた時 |
|
|
2350
|
+
* | `chart-slope` | 左の軸 | 線が右の軸へ伸びる | 右の値は線が右の軸に着いた時 |
|
|
2350
2351
|
* | `journey-map` | 左端 | 気持ちの線が右へ伸びる | 段はその点に線の先が届いた時 |
|
|
2351
2352
|
* | `mind-map` | 中心 | 枝が段ごとに外へ伸びる | 箱はその枝が届いた時 |
|
|
2352
2353
|
* | `tree-hierarchy` | 根 | 枝が段ごとに下へ伸びる | 箱はその枝が届いた時 |
|
|
2353
2354
|
* | `gantt-timeline` | 各帯の始端 | 帯が右へ伸びる | 依存の矢印は帯が出揃った時 |
|
|
2354
2355
|
* | `funnel-stages` | 上端 | 段が上から順に積まれる | 件数と離脱率はその段が出切った時 |
|
|
2356
|
+
* | `chart-gauge` | 9 時 | 弧が上を通って 3 時へ伸びる | 合計と内訳は最初から出る |
|
|
2357
|
+
* | `chart-radial` | 12 時 | 各輪が時計回りに開く | 軌道と一覧は最初から出る |
|
|
2358
|
+
* | `chart-stacked-bar` | 左端 | 帯が右へ伸びる | 区画の字はその区画が広がった時 |
|
|
2355
2359
|
*
|
|
2356
2360
|
* 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
|
|
2357
2361
|
*
|
|
@@ -2347,11 +2347,15 @@ type CdlPhase = {
|
|
|
2347
2347
|
* | `chart-line` | 左端 | 線が右へ伸びる | 点と数字は線の先が届いた時 |
|
|
2348
2348
|
* | `chart-bar` | 横軸 | 棒が上へ伸びる | 数字は棒の頭に付いて動く |
|
|
2349
2349
|
* | `chart-pie` | 12 時 | 扇が時計回りに開く | 凡例はその扇が開き始めた時 |
|
|
2350
|
+
* | `chart-slope` | 左の軸 | 線が右の軸へ伸びる | 右の値は線が右の軸に着いた時 |
|
|
2350
2351
|
* | `journey-map` | 左端 | 気持ちの線が右へ伸びる | 段はその点に線の先が届いた時 |
|
|
2351
2352
|
* | `mind-map` | 中心 | 枝が段ごとに外へ伸びる | 箱はその枝が届いた時 |
|
|
2352
2353
|
* | `tree-hierarchy` | 根 | 枝が段ごとに下へ伸びる | 箱はその枝が届いた時 |
|
|
2353
2354
|
* | `gantt-timeline` | 各帯の始端 | 帯が右へ伸びる | 依存の矢印は帯が出揃った時 |
|
|
2354
2355
|
* | `funnel-stages` | 上端 | 段が上から順に積まれる | 件数と離脱率はその段が出切った時 |
|
|
2356
|
+
* | `chart-gauge` | 9 時 | 弧が上を通って 3 時へ伸びる | 合計と内訳は最初から出る |
|
|
2357
|
+
* | `chart-radial` | 12 時 | 各輪が時計回りに開く | 軌道と一覧は最初から出る |
|
|
2358
|
+
* | `chart-stacked-bar` | 左端 | 帯が右へ伸びる | 区画の字はその区画が広がった時 |
|
|
2355
2359
|
*
|
|
2356
2360
|
* 対象種別の一覧は `validate.ts` の `DRAW_KINDS` が持つ。
|
|
2357
2361
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardenelabs/cdl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
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",
|
|
@@ -60,6 +60,8 @@ export class Timeline {
|
|
|
60
60
|
private rafHandle: number | null = null;
|
|
61
61
|
private phaseIndex = 0;
|
|
62
62
|
private phaseProgress = 0;
|
|
63
|
+
/** 曲線を通す前の進み (cdl#711)。 経過時間そのものの割合で、 描く動きが読む */
|
|
64
|
+
private phaseLinearProgress = 0;
|
|
63
65
|
private phaseStartedAt = 0;
|
|
64
66
|
private status: TimelineStatus = "idle";
|
|
65
67
|
/**
|
|
@@ -75,7 +77,7 @@ export class Timeline {
|
|
|
75
77
|
*/
|
|
76
78
|
private readonly initialState: {
|
|
77
79
|
phaseIndex: number;
|
|
78
|
-
|
|
80
|
+
phaseLinearProgress: number;
|
|
79
81
|
status: TimelineStatus;
|
|
80
82
|
};
|
|
81
83
|
|
|
@@ -94,12 +96,12 @@ export class Timeline {
|
|
|
94
96
|
const initial = options.initialPhaseIndex;
|
|
95
97
|
if (initial != null && initial > 0 && initial < phases.length) {
|
|
96
98
|
this.phaseIndex = initial;
|
|
97
|
-
this.
|
|
99
|
+
this.setProgress(1);
|
|
98
100
|
this.status = initial === phases.length - 1 ? "settled" : "paused";
|
|
99
101
|
}
|
|
100
102
|
this.initialState = {
|
|
101
103
|
phaseIndex: this.phaseIndex,
|
|
102
|
-
|
|
104
|
+
phaseLinearProgress: this.phaseLinearProgress,
|
|
103
105
|
status: this.status,
|
|
104
106
|
};
|
|
105
107
|
}
|
|
@@ -133,6 +135,7 @@ export class Timeline {
|
|
|
133
135
|
cached.status === this.status &&
|
|
134
136
|
cached.phaseIndex === this.phaseIndex &&
|
|
135
137
|
cached.phaseProgress === this.phaseProgress &&
|
|
138
|
+
cached.phaseLinearProgress === this.phaseLinearProgress &&
|
|
136
139
|
cached.totalPhases === this.phases.length
|
|
137
140
|
) {
|
|
138
141
|
return cached;
|
|
@@ -141,6 +144,7 @@ export class Timeline {
|
|
|
141
144
|
status: this.status,
|
|
142
145
|
phaseIndex: this.phaseIndex,
|
|
143
146
|
phaseProgress: this.phaseProgress,
|
|
147
|
+
phaseLinearProgress: this.phaseLinearProgress,
|
|
144
148
|
totalPhases: this.phases.length,
|
|
145
149
|
};
|
|
146
150
|
this.cachedSnapshot = next;
|
|
@@ -168,7 +172,7 @@ export class Timeline {
|
|
|
168
172
|
const clamped = Math.max(0, Math.min(this.phases.length - 1, index));
|
|
169
173
|
this.cancelRaf();
|
|
170
174
|
this.phaseIndex = clamped;
|
|
171
|
-
this.
|
|
175
|
+
this.setProgress(1);
|
|
172
176
|
this.status = clamped === this.phases.length - 1 ? "settled" : "paused";
|
|
173
177
|
this.emit();
|
|
174
178
|
}
|
|
@@ -212,7 +216,7 @@ export class Timeline {
|
|
|
212
216
|
if (this.phaseIndex <= 0) return;
|
|
213
217
|
this.cancelRaf();
|
|
214
218
|
this.phaseIndex -= 1;
|
|
215
|
-
this.
|
|
219
|
+
this.setProgress(1);
|
|
216
220
|
this.status = "paused";
|
|
217
221
|
this.emit();
|
|
218
222
|
}
|
|
@@ -220,7 +224,7 @@ export class Timeline {
|
|
|
220
224
|
reset(): void {
|
|
221
225
|
this.cancelRaf();
|
|
222
226
|
this.phaseIndex = 0;
|
|
223
|
-
this.
|
|
227
|
+
this.setProgress(0);
|
|
224
228
|
this.status = "idle";
|
|
225
229
|
this.emit();
|
|
226
230
|
}
|
|
@@ -242,7 +246,7 @@ export class Timeline {
|
|
|
242
246
|
this.subscribers.clear();
|
|
243
247
|
this.status = this.initialState.status;
|
|
244
248
|
this.phaseIndex = this.initialState.phaseIndex;
|
|
245
|
-
this.
|
|
249
|
+
this.setProgress(this.initialState.phaseLinearProgress);
|
|
246
250
|
}
|
|
247
251
|
|
|
248
252
|
// ─── internals ───────────────────────────────────────────────
|
|
@@ -284,7 +288,7 @@ export class Timeline {
|
|
|
284
288
|
}
|
|
285
289
|
|
|
286
290
|
private beginPhaseTransition(): void {
|
|
287
|
-
this.
|
|
291
|
+
this.setProgress(0);
|
|
288
292
|
this.phaseStartedAt = performance.now();
|
|
289
293
|
this.cancelRaf();
|
|
290
294
|
const step = (now: number) => {
|
|
@@ -292,13 +296,13 @@ export class Timeline {
|
|
|
292
296
|
this.autoAdvanceMsOverride ?? this.getPhase().durationMs ?? 600;
|
|
293
297
|
const elapsed = now - this.phaseStartedAt;
|
|
294
298
|
const linearT = clamp01(elapsed / duration);
|
|
295
|
-
this.
|
|
299
|
+
this.setProgress(linearT);
|
|
296
300
|
this.emit();
|
|
297
301
|
if (linearT < 1) {
|
|
298
302
|
this.rafHandle = requestAnimationFrame(step);
|
|
299
303
|
return;
|
|
300
304
|
}
|
|
301
|
-
this.
|
|
305
|
+
this.setProgress(1);
|
|
302
306
|
this.emit();
|
|
303
307
|
if (this.status === "playing") {
|
|
304
308
|
// 自動進行中は次 Phase へ
|
|
@@ -315,6 +319,20 @@ export class Timeline {
|
|
|
315
319
|
this.rafHandle = requestAnimationFrame(step);
|
|
316
320
|
}
|
|
317
321
|
|
|
322
|
+
/**
|
|
323
|
+
* 段の進みを 2 つ同時に置く (cdl#711)。
|
|
324
|
+
*
|
|
325
|
+
* **代入する場所を 1 つに閉じる**。 曲げた進みと曲げる前の進みを別々に書くと、
|
|
326
|
+
* どちらかを更新し忘れた経路が「前の段の値のまま」 で残る。
|
|
327
|
+
*
|
|
328
|
+
* 両端 (0 と 1) は曲線を通さず、 そのまま置く。 曲線は差し替えられる欄なので、
|
|
329
|
+
* `easing(1)` が 1 を返す保証がない。 段の始まりと終わりの値は曲線に委ねない。
|
|
330
|
+
*/
|
|
331
|
+
private setProgress(linear: number): void {
|
|
332
|
+
this.phaseLinearProgress = linear;
|
|
333
|
+
this.phaseProgress = linear <= 0 ? 0 : linear >= 1 ? 1 : this.easing(linear);
|
|
334
|
+
}
|
|
335
|
+
|
|
318
336
|
private cancelRaf(): void {
|
|
319
337
|
if (this.rafHandle != null) {
|
|
320
338
|
cancelAnimationFrame(this.rafHandle);
|
package/src/anim/core/types.ts
CHANGED
|
@@ -37,6 +37,18 @@ export type TimelineState = {
|
|
|
37
37
|
phaseIndex: number;
|
|
38
38
|
/** 現在 Phase 内の進行度 (0..1)、 視覚要素の補間に使う */
|
|
39
39
|
phaseProgress: number;
|
|
40
|
+
/**
|
|
41
|
+
* 曲線を通す前の進行度 (0..1、 cdl#711)。 経過時間そのものの割合。
|
|
42
|
+
*
|
|
43
|
+
* `phaseProgress` は既定で `easeOutQuint` を通るため前半に動きが寄る。
|
|
44
|
+
* **値が落ち着く動きには合うが、 線を引く動きには合わない** =
|
|
45
|
+
* `1.2s` の段で半分に達するのが 158 ms になり、 折れ線の 5 点のうち 4 点が
|
|
46
|
+
* 292 ms の間に出てしまう (実測)。
|
|
47
|
+
*
|
|
48
|
+
* 描く動き (`draw` の node と伸ばす edge) はこちらを読む。
|
|
49
|
+
* 両端 (0 と 1) は 2 つとも同じ値になる。
|
|
50
|
+
*/
|
|
51
|
+
phaseLinearProgress: number;
|
|
40
52
|
/** 全 Phase 数 */
|
|
41
53
|
totalPhases: number;
|
|
42
54
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { type JSX } from "react";
|
|
1
|
+
import { useId, type JSX } from "react";
|
|
2
2
|
import type { 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";
|
|
6
6
|
import { 実値表記, 割合表記 } from "./chart-number";
|
|
7
|
+
import { 進みを畳む } from "./draw-progress";
|
|
8
|
+
import { 扇形の切り抜き } from "./draw-arc";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* chart-gauge kind ... 上半分の弧で内訳を示し、 合計を主役に置く図 (cdl#549)。
|
|
@@ -27,15 +29,28 @@ import { 実値表記, 割合表記 } from "./chart-number";
|
|
|
27
29
|
*
|
|
28
30
|
* 内訳には実値と割合の両方を置く。 合計が主役の図で内訳が割合しか持たないと、 「合計の何割」
|
|
29
31
|
* は分かっても「いくつ」 が分からず、 合計との関係を読み手が暗算することになる。
|
|
32
|
+
*
|
|
33
|
+
* ## 段の `draw` では 9 時から弧が伸びる (cdl#715)
|
|
34
|
+
*
|
|
35
|
+
* 伸びる向きは弧の向きそのもので、 図の意味から一意に決まる。
|
|
36
|
+
*
|
|
37
|
+
* **切り抜くのは弧だけ**。 合計の字と内訳の段は最初から出す = 合計はこの種別の主役なので、
|
|
38
|
+
* 左から半分ずつ現れると読めない。 内訳は弧の外にある別の一覧で、 弧の開きに従う理由がない。
|
|
30
39
|
*/
|
|
31
40
|
export function ChartGaugeNode({
|
|
32
41
|
node,
|
|
33
42
|
active,
|
|
34
43
|
stateValues = {},
|
|
44
|
+
drawing = false,
|
|
45
|
+
progress = 1,
|
|
35
46
|
}: {
|
|
36
47
|
node: LaidNode;
|
|
37
48
|
active: boolean;
|
|
38
49
|
stateValues?: Record<string, string>;
|
|
50
|
+
/** この phase の `draw` に載っているか (cdl#715) */
|
|
51
|
+
drawing?: boolean;
|
|
52
|
+
/** phase の進み (0→1)。 `drawing` が true の時だけ読む */
|
|
53
|
+
progress?: number;
|
|
39
54
|
}): JSX.Element {
|
|
40
55
|
const x0 = node.cx - node.w / 2;
|
|
41
56
|
const y0 = node.cy - node.h / 2;
|
|
@@ -112,6 +127,24 @@ export function ChartGaugeNode({
|
|
|
112
127
|
const 内訳の幅 = 使える幅 / Math.max(data.length, 1);
|
|
113
128
|
const 内訳のy = cy + 弧と内訳の間;
|
|
114
129
|
|
|
130
|
+
/** 弧がどこまで伸びたか (0..1、 cdl#715)。 畳み方は `draw-progress.ts` が持つ */
|
|
131
|
+
const 伸び = 進みを畳む(drawing, progress);
|
|
132
|
+
// SVG の id は document 全体の名前空間。 node id は図の中でだけ一意なので instance id で分ける
|
|
133
|
+
const 切り抜きid = `cdl-gauge-draw-${useId().replace(/[^A-Za-z0-9_-]/g, "")}`;
|
|
134
|
+
const 弧たち = slices.map((s, idx) => (
|
|
135
|
+
<path
|
|
136
|
+
key={`arc-${idx}`}
|
|
137
|
+
data-cdl-role="chart-gauge-arc"
|
|
138
|
+
data-cdl-unresolved={s.d.unresolved ? "true" : undefined}
|
|
139
|
+
d={s.path}
|
|
140
|
+
fill={arcColor(s.d.tone, idx)}
|
|
141
|
+
// 区切りは地の色の余白で取る。 線だと弧が分断されて 1 つの量に見えない
|
|
142
|
+
stroke="var(--cdl-node-fill, #ffffff)"
|
|
143
|
+
strokeWidth={3}
|
|
144
|
+
strokeLinejoin="round"
|
|
145
|
+
/>
|
|
146
|
+
));
|
|
147
|
+
|
|
115
148
|
return (
|
|
116
149
|
<g transform={`translate(${x0} ${y0})`}>
|
|
117
150
|
<rect
|
|
@@ -126,19 +159,21 @@ export function ChartGaugeNode({
|
|
|
126
159
|
strokeWidth={active ? 3 : 1.25}
|
|
127
160
|
/>
|
|
128
161
|
|
|
129
|
-
{
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
162
|
+
{drawing && (
|
|
163
|
+
<defs>
|
|
164
|
+
{/*
|
|
165
|
+
9 時から上を通って 3 時へ開く切り抜き (cdl#715)。 半径は弧の外側に少し足す =
|
|
166
|
+
弧の縁が切り抜きの縁と重なると、 開き切った時に外周が痩せて見える。
|
|
167
|
+
|
|
168
|
+
**弧の `d` は触らない** = 形から量を測る利用側が、 進みの途中で別の値を読まない。
|
|
169
|
+
*/}
|
|
170
|
+
<clipPath id={切り抜きid}>
|
|
171
|
+
{扇形の切り抜き(cx, cy, radius + 2, 伸び, Math.PI, Math.PI)}
|
|
172
|
+
</clipPath>
|
|
173
|
+
</defs>
|
|
174
|
+
)}
|
|
175
|
+
{/* 描く指定が無い図には包みを足さず、 従来の DOM 構造を保つ */}
|
|
176
|
+
{drawing ? <g clipPath={`url(#${切り抜きid})`}>{弧たち}</g> : 弧たち}
|
|
142
177
|
|
|
143
178
|
{/*
|
|
144
179
|
合計。 この種別の主役なので図の中でいちばん大きい字にするが、 **弧の内側に収める**。
|
package/src/kinds/chart-pie.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import type { 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";
|
|
6
|
+
import { 扇形の切り抜き } from "./draw-arc";
|
|
6
7
|
import { textWidth } from "./text-width";
|
|
7
8
|
import { 実値表記, 割合表記 } from "./chart-number";
|
|
8
9
|
|
|
@@ -687,22 +688,6 @@ function 輪の道(cx: number, cy: number, R: number, r: number, a0: number, a1:
|
|
|
687
688
|
);
|
|
688
689
|
}
|
|
689
690
|
|
|
690
|
-
/**
|
|
691
|
-
* 12 時から時計回りに開く扇形 (cdl#516)。 円を切り抜く形に使う。
|
|
692
|
-
*
|
|
693
|
-
* 1 周ちょうどは扇形で表せない (始点と終点が同じ点になり、 弧の向きが決まらない)。
|
|
694
|
-
* その時だけ円をそのまま返す = 進み 1 で図が消える形を避ける。
|
|
695
|
-
*/
|
|
696
|
-
function 扇形の切り抜き(cx: number, cy: number, r: number, 開き: number): JSX.Element | null {
|
|
697
|
-
if (開き <= 0) return null;
|
|
698
|
-
if (開き >= 1) return <circle cx={cx} cy={cy} r={r} />;
|
|
699
|
-
const a0 = -Math.PI / 2;
|
|
700
|
-
const a1 = a0 + 開き * Math.PI * 2;
|
|
701
|
-
const large = 開き > 0.5 ? 1 : 0;
|
|
702
|
-
const d = `M ${cx} ${cy} L ${cx + r * Math.cos(a0)} ${cy + r * Math.sin(a0)} A ${r} ${r} 0 ${large} 1 ${cx + r * Math.cos(a1)} ${cy + r * Math.sin(a1)} Z`;
|
|
703
|
-
return <path d={d} />;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
691
|
/**
|
|
707
692
|
* 扇の色 (cdl#546)。
|
|
708
693
|
*
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { type JSX } from "react";
|
|
1
|
+
import { useId, type JSX } from "react";
|
|
2
2
|
import type { 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";
|
|
6
6
|
import { 実値表記, 割合表記 } from "./chart-number";
|
|
7
|
+
import { 進みを畳む } from "./draw-progress";
|
|
8
|
+
import { 扇形の切り抜き } from "./draw-arc";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* chart-radial kind ... 1 件を 1 本の弧にして内側から重ね、 長さで比べる図 (cdl#550)。
|
|
@@ -27,15 +29,29 @@ import { 実値表記, 割合表記 } from "./chart-number";
|
|
|
27
29
|
*
|
|
28
30
|
* 弧のそばに置くと、 内側の弧ほど場所が狭くなって字が入らない。 左端に縦へ並べると件数が
|
|
29
31
|
* 増えても一覧性が落ちず、 弧の並び順と行の並び順が対応する。
|
|
32
|
+
*
|
|
33
|
+
* ## 段の `draw` では 12 時から各輪が開く (cdl#715)
|
|
34
|
+
*
|
|
35
|
+
* 弧の向きそのままで、 全部の輪が同時に開く。 外から順に開く案は採らない = 内側の輪だけ
|
|
36
|
+
* 軌道が空のまま残る時間が長くなり、 「まだ描いていない」 と「本当に 0」 が見分けられない。
|
|
37
|
+
*
|
|
38
|
+
* **切り抜くのは取り分の弧だけ**。 軌道と一覧は最初から出す = 軌道は「あとどれだけで一周か」
|
|
39
|
+
* を示す枠なので、 一緒に伸ばすと比べる相手が無いまま弧だけが伸びる。
|
|
30
40
|
*/
|
|
31
41
|
export function ChartRadialNode({
|
|
32
42
|
node,
|
|
33
43
|
active,
|
|
34
44
|
stateValues = {},
|
|
45
|
+
drawing = false,
|
|
46
|
+
progress = 1,
|
|
35
47
|
}: {
|
|
36
48
|
node: LaidNode;
|
|
37
49
|
active: boolean;
|
|
38
50
|
stateValues?: Record<string, string>;
|
|
51
|
+
/** この phase の `draw` に載っているか (cdl#715) */
|
|
52
|
+
drawing?: boolean;
|
|
53
|
+
/** phase の進み (0→1)。 `drawing` が true の時だけ読む */
|
|
54
|
+
progress?: number;
|
|
39
55
|
}): JSX.Element {
|
|
40
56
|
const x0 = node.cx - node.w / 2;
|
|
41
57
|
const y0 = node.cy - node.h / 2;
|
|
@@ -99,6 +115,22 @@ export function ChartRadialNode({
|
|
|
99
115
|
|
|
100
116
|
const 一覧の始まり = PAD + (使える高さ - 行の高さ * data.length) / 2 + 行の高さ / 2;
|
|
101
117
|
|
|
118
|
+
/** 弧がどこまで開いたか (0..1、 cdl#715)。 畳み方は `draw-progress.ts` が持つ */
|
|
119
|
+
const 開き = 進みを畳む(drawing, progress);
|
|
120
|
+
// SVG の id は document 全体の名前空間。 node id は図の中でだけ一意なので instance id で分ける
|
|
121
|
+
const 切り抜きid = `cdl-radial-draw-${useId().replace(/[^A-Za-z0-9_-]/g, "")}`;
|
|
122
|
+
const 取り分たち = rings.map((r, idx) =>
|
|
123
|
+
r.取り分 === null ? null : (
|
|
124
|
+
<path
|
|
125
|
+
key={`arc-${idx}`}
|
|
126
|
+
data-cdl-role="chart-radial-arc"
|
|
127
|
+
data-cdl-unresolved={r.d.unresolved ? "true" : undefined}
|
|
128
|
+
d={r.取り分}
|
|
129
|
+
fill={arcColor(r.d.tone, idx)}
|
|
130
|
+
/>
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
|
|
102
134
|
return (
|
|
103
135
|
<g transform={`translate(${x0} ${y0})`}>
|
|
104
136
|
<rect
|
|
@@ -127,17 +159,19 @@ export function ChartRadialNode({
|
|
|
127
159
|
/>
|
|
128
160
|
))}
|
|
129
161
|
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
d=
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
162
|
+
{drawing && (
|
|
163
|
+
<defs>
|
|
164
|
+
{/*
|
|
165
|
+
12 時から時計回りに開く切り抜き (cdl#715)。 半径はいちばん外の帯の外側まで取る =
|
|
166
|
+
全部の輪が同じ 1 つの切り抜きで開く。
|
|
167
|
+
|
|
168
|
+
**弧の `d` は触らない** = 形から量を測る利用側が、 進みの途中で別の値を読まない。
|
|
169
|
+
*/}
|
|
170
|
+
<clipPath id={切り抜きid}>{扇形の切り抜き(弧のcx, cy, 外半径 + 帯の幅, 開き)}</clipPath>
|
|
171
|
+
</defs>
|
|
140
172
|
)}
|
|
173
|
+
{/* 描く指定が無い図には包みを足さず、 従来の DOM 構造を保つ */}
|
|
174
|
+
{drawing ? <g clipPath={`url(#${切り抜きid})`}>{取り分たち}</g> : 取り分たち}
|
|
141
175
|
|
|
142
176
|
{/* 一覧。 行の並びが弧の並び (外から内へ) と対応する */}
|
|
143
177
|
{data.map((d, idx) => {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { type JSX } from "react";
|
|
1
|
+
import { useId, type JSX } from "react";
|
|
2
2
|
import type { 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";
|
|
6
6
|
import { 実値表記, 割合表記 } from "./chart-number";
|
|
7
|
+
import { 進みを畳む } from "./draw-progress";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* chart-stacked-bar kind ... 1 本の帯を割合で区切り、時点を並べて変化を見せる図 (cdl#551)。
|
|
@@ -27,6 +28,15 @@ import { 実値表記, 割合表記 } from "./chart-number";
|
|
|
27
28
|
*
|
|
28
29
|
* 細い区画に字を詰めると隣の区画へはみ出して、どちらの値か読めなくなる。
|
|
29
30
|
* 入らない区画は字を描かず、名前は下の一覧が受け持つ。
|
|
31
|
+
*
|
|
32
|
+
* ## 段の `draw` では左から帯が伸びる (cdl#715)
|
|
33
|
+
*
|
|
34
|
+
* 帯は左から右へ割合で区切る図なので、伸びる向きは並びそのもの。 棒グラフと起点を揃えている。
|
|
35
|
+
* 帯が 2 本ある図では 2 本とも同時に伸びる = 上下で同じ位置を見比べる図なので、
|
|
36
|
+
* 片方だけ先に出ると比べる相手がいない時間ができる。
|
|
37
|
+
*
|
|
38
|
+
* **区画の字は区画と一緒に切り抜く**。 字は区画の上に乗っているので、切り抜かないと
|
|
39
|
+
* まだ出ていない区画の割合だけが宙に浮く。 時点の名前と下の一覧は最初から出す。
|
|
30
40
|
*/
|
|
31
41
|
|
|
32
42
|
/** 区画に割合を描くために要る幅。 これ未満の区画は字を描かない */
|
|
@@ -39,10 +49,16 @@ export function ChartStackedBarNode({
|
|
|
39
49
|
node,
|
|
40
50
|
active,
|
|
41
51
|
stateValues = {},
|
|
52
|
+
drawing = false,
|
|
53
|
+
progress = 1,
|
|
42
54
|
}: {
|
|
43
55
|
node: LaidNode;
|
|
44
56
|
active: boolean;
|
|
45
57
|
stateValues?: Record<string, string>;
|
|
58
|
+
/** この phase の `draw` に載っているか (cdl#715) */
|
|
59
|
+
drawing?: boolean;
|
|
60
|
+
/** phase の進み (0→1)。 `drawing` が true の時だけ読む */
|
|
61
|
+
progress?: number;
|
|
46
62
|
}): JSX.Element {
|
|
47
63
|
const x0 = node.cx - node.w / 2;
|
|
48
64
|
const y0 = node.cy - node.h / 2;
|
|
@@ -67,6 +83,11 @@ export function ChartStackedBarNode({
|
|
|
67
83
|
const 全体の高さ = 帯たち.length * 帯の高さ + (帯たち.length - 1) * 帯の間 + 16 + 一覧の高さ;
|
|
68
84
|
const 上端 = Math.max(PAD, (node.h - 全体の高さ) / 2);
|
|
69
85
|
|
|
86
|
+
/** 帯がどこまで伸びたか (0..1、 cdl#715)。 畳み方は `draw-progress.ts` が持つ */
|
|
87
|
+
const 伸び = 進みを畳む(drawing, progress);
|
|
88
|
+
// SVG の id は document 全体の名前空間。 node id は図の中でだけ一意なので instance id で分ける
|
|
89
|
+
const 切り抜きid = `cdl-stacked-bar-draw-${useId().replace(/[^A-Za-z0-9_-]/g, "")}`;
|
|
90
|
+
|
|
70
91
|
return (
|
|
71
92
|
<g transform={`translate(${x0} ${y0})`}>
|
|
72
93
|
<rect
|
|
@@ -81,6 +102,23 @@ export function ChartStackedBarNode({
|
|
|
81
102
|
strokeWidth={active ? 3 : 1.25}
|
|
82
103
|
/>
|
|
83
104
|
|
|
105
|
+
{drawing && (
|
|
106
|
+
<defs>
|
|
107
|
+
{/*
|
|
108
|
+
左から右へ広がる切り抜き (cdl#715)。 帯が 2 本ある図でも 1 つで足りる =
|
|
109
|
+
2 本とも同じ左端から同じ幅で伸びる。
|
|
110
|
+
|
|
111
|
+
**右へ余白を足さない** = 伸びる向きそのものなので、足すと進み 0 で区画の左端が
|
|
112
|
+
見えてしまう。 切り抜きは境界を含むため、伸び切った時に区画の右端は欠けない。
|
|
113
|
+
|
|
114
|
+
**区画の `width` は触らない** = 形から量を測る利用側が、進みの途中で別の値を読まない。
|
|
115
|
+
*/}
|
|
116
|
+
<clipPath id={切り抜きid}>
|
|
117
|
+
<rect x={0} y={0} width={帯の左 + 帯の幅 * 伸び} height={node.h} />
|
|
118
|
+
</clipPath>
|
|
119
|
+
</defs>
|
|
120
|
+
)}
|
|
121
|
+
|
|
84
122
|
{帯たち.map((帯, 段) => {
|
|
85
123
|
const y = 上端 + 段 * (帯の高さ + 帯の間);
|
|
86
124
|
let 左 = 帯の左;
|
|
@@ -100,55 +138,59 @@ export function ChartStackedBarNode({
|
|
|
100
138
|
</text>
|
|
101
139
|
)}
|
|
102
140
|
|
|
103
|
-
{
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
141
|
+
{(() => {
|
|
142
|
+
const 区画たち = 帯.区画.map((s, idx) => {
|
|
143
|
+
const w = 帯の幅 * s.割合;
|
|
144
|
+
const x = 左;
|
|
145
|
+
左 += w;
|
|
146
|
+
const 色 = 印の色(s.tone, idx);
|
|
147
|
+
return (
|
|
148
|
+
<g key={`slice-${idx}`}>
|
|
149
|
+
<rect
|
|
150
|
+
data-cdl-role="chart-stacked-bar-slice"
|
|
151
|
+
data-cdl-unresolved={s.unresolved ? "true" : undefined}
|
|
152
|
+
x={x}
|
|
153
|
+
y={y}
|
|
154
|
+
width={Math.max(0, w)}
|
|
155
|
+
height={帯の高さ}
|
|
156
|
+
fill={色}
|
|
157
|
+
/>
|
|
158
|
+
{/*
|
|
120
159
|
区画の字。 **入る時だけ描く** = 細い区画に詰めると隣へはみ出して
|
|
121
160
|
どちらの値か読めなくなる。 名前は下の一覧が受け持つ
|
|
122
161
|
*/}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
{w >= SLICE_MIN_W_FOR_TEXT && (
|
|
163
|
+
<text
|
|
164
|
+
data-cdl-role="chart-stacked-bar-slice-text"
|
|
165
|
+
x={x + w / 2}
|
|
166
|
+
y={y + 帯の高さ / 2 + (w >= SLICE_MIN_W_FOR_VALUE ? -2 : 4)}
|
|
167
|
+
fontSize={区画の級}
|
|
168
|
+
fontWeight={700}
|
|
169
|
+
textAnchor="middle"
|
|
170
|
+
fill="#ffffff"
|
|
171
|
+
>
|
|
172
|
+
{割合表記(s.割合)}
|
|
173
|
+
</text>
|
|
174
|
+
)}
|
|
175
|
+
{w >= SLICE_MIN_W_FOR_VALUE && (
|
|
176
|
+
<text
|
|
177
|
+
data-cdl-role="chart-stacked-bar-slice-value"
|
|
178
|
+
x={x + w / 2}
|
|
179
|
+
y={y + 帯の高さ / 2 + 13}
|
|
180
|
+
fontSize={区画の級 - 1}
|
|
181
|
+
textAnchor="middle"
|
|
182
|
+
fill="#ffffff"
|
|
183
|
+
fillOpacity={0.85}
|
|
184
|
+
>
|
|
185
|
+
{実値表記(s.実値)}
|
|
186
|
+
</text>
|
|
187
|
+
)}
|
|
188
|
+
</g>
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
// 描く指定が無い図には包みを足さず、 従来の DOM 構造を保つ
|
|
192
|
+
return drawing ? <g clipPath={`url(#${切り抜きid})`}>{区画たち}</g> : 区画たち;
|
|
193
|
+
})()}
|
|
152
194
|
</g>
|
|
153
195
|
);
|
|
154
196
|
})}
|
|
@@ -156,16 +198,16 @@ export function ChartStackedBarNode({
|
|
|
156
198
|
{/* 一覧。 区画に字が入らなかった件もここで名前と色が引ける */}
|
|
157
199
|
{一覧の並び(data, 帯の左, node.w - PAD, 名前の級).map(({ d, idx, x, 行 }) => {
|
|
158
200
|
const y =
|
|
159
|
-
上端 +
|
|
201
|
+
上端 +
|
|
202
|
+
帯たち.length * (帯の高さ + 帯の間) -
|
|
203
|
+
帯の間 +
|
|
204
|
+
16 +
|
|
205
|
+
一覧の高さ / 2 +
|
|
206
|
+
行 * 一覧の高さ;
|
|
160
207
|
return (
|
|
161
208
|
<g key={`legend-${idx}`} data-cdl-role="chart-stacked-bar-legend">
|
|
162
209
|
<rect x={x} y={y - 5} width={10} height={10} rx={2} fill={印の色(d.tone, idx)} />
|
|
163
|
-
<text
|
|
164
|
-
x={x + 16}
|
|
165
|
-
y={y + 4}
|
|
166
|
-
fontSize={名前の級}
|
|
167
|
-
fill="var(--cdl-text, #1a1f2a)"
|
|
168
|
-
>
|
|
210
|
+
<text x={x + 16} y={y + 4} fontSize={名前の級} fill="var(--cdl-text, #1a1f2a)">
|
|
169
211
|
{d.label}
|
|
170
212
|
</text>
|
|
171
213
|
</g>
|