@cardenelabs/cdl 0.40.1 → 0.40.3
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 +45 -1
- package/dist/index.cjs +87 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +87 -42
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +87 -42
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +87 -42
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
- package/src/kinds/chart-gauge.tsx +56 -11
- package/src/kinds/chart-pie.tsx +46 -4
- package/src/kinds/chart-slope.tsx +1 -65
- package/src/kinds/label-stack.ts +77 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cardenelabs/cdl",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.3",
|
|
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",
|
|
@@ -65,10 +65,43 @@ export function ChartGaugeNode({
|
|
|
65
65
|
|
|
66
66
|
const 名前の級 = 14;
|
|
67
67
|
const 数値の級 = 13;
|
|
68
|
-
/** 内訳の 1
|
|
69
|
-
const
|
|
68
|
+
/** 内訳の 1 段が縦に使う高さ (印 + 名前 + 数値) */
|
|
69
|
+
const 段の高さ = 56;
|
|
70
70
|
/** 弧と内訳の間 */
|
|
71
71
|
const 弧と内訳の間 = 16;
|
|
72
|
+
/** 色の印の幅と、区画どうしの間 */
|
|
73
|
+
const 印の幅 = 30;
|
|
74
|
+
const 区画の間 = 12;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 内訳の割付 (cdl#731)。
|
|
78
|
+
*
|
|
79
|
+
* **件数で等分するだけでは足りない**。 名前は区画に収まるまで切り詰めていたが数値は
|
|
80
|
+
* そのままだったので、件数が増えると数字が隣へはみ出してくっついた
|
|
81
|
+
* (実測で件数 10 から重なり始め、12 で 10 組)。
|
|
82
|
+
*
|
|
83
|
+
* 区画の幅を **中身から決めて**、1 行に置ける件数を出す。 余りは次の段へ送る。
|
|
84
|
+
* 段が増えたぶん弧は低くなる = 弧の高さは残りから決めているので自動で縮む。
|
|
85
|
+
*
|
|
86
|
+
* 級を下げて 1 行に収める案は採らない = 件数 12 では級 7.7 が要り、この repo が
|
|
87
|
+
* 他の図で使っている下限の 9 を下回る。
|
|
88
|
+
*/
|
|
89
|
+
const 内訳の数値 = data.map((d) => `${実値表記(d.value)} ${割合表記(d.value / total)}`);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 内訳 1 件が横に要る幅。 数値の行と色の印の広い方に、区画どうしの間を足す。
|
|
93
|
+
*
|
|
94
|
+
* **印の幅が広い方になる入力は組めない** = 数値の行は最も短い形 (`1 50%`) でも級 13 で
|
|
95
|
+
* 幅 56.7 あり、印の 30 を必ず上回る。 件数 0 の図で `Math.max` の引数が空になるのを
|
|
96
|
+
* 防ぐ下限として置いている。 万一この関係が崩れても印は区画に合わせて縮む (下の `Math.min`)。
|
|
97
|
+
*/
|
|
98
|
+
const 件に要る幅 = Math.max(印の幅, ...内訳の数値.map((t) => textWidth(t, 数値の級))) + 区画の間;
|
|
99
|
+
const 行の件数 = Math.min(
|
|
100
|
+
Math.max(1, Math.floor(使える幅 / Math.max(件に要る幅, 1))),
|
|
101
|
+
Math.max(data.length, 1),
|
|
102
|
+
);
|
|
103
|
+
const 内訳の段数 = Math.ceil(Math.max(data.length, 1) / 行の件数);
|
|
104
|
+
const 内訳の高さ = 段の高さ * 内訳の段数;
|
|
72
105
|
|
|
73
106
|
/**
|
|
74
107
|
* 弧の外半径。
|
|
@@ -123,8 +156,8 @@ export function ChartGaugeNode({
|
|
|
123
156
|
const 合計の級 = Math.max(11, Math.round(級の候補));
|
|
124
157
|
const 見出しの級 = Math.max(9, Math.round(合計の級 * 0.38));
|
|
125
158
|
|
|
126
|
-
/** 内訳 1 件あたりの幅。
|
|
127
|
-
const 内訳の幅 = 使える幅 /
|
|
159
|
+
/** 内訳 1 件あたりの幅。 1 行に置く件数で等分する */
|
|
160
|
+
const 内訳の幅 = 使える幅 / 行の件数;
|
|
128
161
|
const 内訳のy = cy + 弧と内訳の間;
|
|
129
162
|
|
|
130
163
|
/** 弧がどこまで伸びたか (0..1、 cdl#715)。 畳み方は `draw-progress.ts` が持つ */
|
|
@@ -207,22 +240,34 @@ export function ChartGaugeNode({
|
|
|
207
240
|
字は区画の左端に揃える。 中央揃えにすると名前の長さで位置が動き、 縦の列が揃わない。
|
|
208
241
|
*/}
|
|
209
242
|
{data.map((d, idx) => {
|
|
210
|
-
const
|
|
211
|
-
const
|
|
243
|
+
const 列 = idx % 行の件数;
|
|
244
|
+
const 段 = Math.floor(idx / 行の件数);
|
|
245
|
+
const x = PAD_SIDE + 内訳の幅 * 列;
|
|
246
|
+
const y = 内訳のy + 段の高さ * 段;
|
|
247
|
+
/** 区画に収まる字幅。 名前も数値も同じ幅で切る */
|
|
248
|
+
const 収まる幅 = 内訳の幅 - 区画の間;
|
|
212
249
|
return (
|
|
213
250
|
<g key={`legend-${idx}`} data-cdl-role="chart-gauge-item">
|
|
214
|
-
<rect
|
|
251
|
+
<rect
|
|
252
|
+
x={x}
|
|
253
|
+
y={y}
|
|
254
|
+
width={Math.max(4, Math.min(印の幅, 収まる幅))}
|
|
255
|
+
height={4}
|
|
256
|
+
rx={2}
|
|
257
|
+
fill={arcColor(d.tone, idx)}
|
|
258
|
+
/>
|
|
215
259
|
<text
|
|
216
260
|
x={x}
|
|
217
|
-
y={
|
|
261
|
+
y={y + 26}
|
|
218
262
|
fontSize={名前の級}
|
|
219
263
|
fontWeight={700}
|
|
220
264
|
fill="var(--cdl-text, #1a1f2a)"
|
|
221
265
|
>
|
|
222
|
-
{切り詰める(d.label ?? "",
|
|
266
|
+
{切り詰める(d.label ?? "", 収まる幅, 名前の級)}
|
|
223
267
|
</text>
|
|
224
|
-
|
|
225
|
-
|
|
268
|
+
{/* **数値も切り詰める** = 切らないと隣の区画の字とくっつく (cdl#731) */}
|
|
269
|
+
<text x={x} y={y + 46} fontSize={数値の級} fill="var(--cdl-text-dim, #5a6270)">
|
|
270
|
+
{切り詰める(内訳の数値[idx] ?? "", 収まる幅, 数値の級)}
|
|
226
271
|
</text>
|
|
227
272
|
</g>
|
|
228
273
|
);
|
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 "./label-stack";
|
|
6
7
|
import { 扇形の切り抜き } from "./draw-arc";
|
|
7
8
|
import { textWidth } from "./text-width";
|
|
8
9
|
import { 実値表記, 割合表記 } from "./chart-number";
|
|
@@ -191,6 +192,47 @@ export function ChartPieNode({
|
|
|
191
192
|
/** その扇が開き始めたか。 開き始めた扇の凡例だけを出す */
|
|
192
193
|
const 開き始めた = (startFrac: number): boolean => !drawing || 開き > startFrac;
|
|
193
194
|
|
|
195
|
+
/**
|
|
196
|
+
* 名札 1 つが縦に要する高さ。 割合と名前の 2 行ぶんに、行どうしが触れない余りを足す
|
|
197
|
+
*/
|
|
198
|
+
const 名札の行 = 割合の級 + 名前の級 + 3;
|
|
199
|
+
/**
|
|
200
|
+
* 名札の縦の位置 (cdl#727)。
|
|
201
|
+
*
|
|
202
|
+
* **角度がそのまま縦になる** ので、小さい扇が続くと名札が同じ場所に集まる
|
|
203
|
+
* (実測で 0.3% の小片 2 個が完全に重なった)。 左右それぞれで重なりをほどく。
|
|
204
|
+
*
|
|
205
|
+
* 扇の縁の点は動かさない = 名札がどの扇のものかは引き出し線が保つ。
|
|
206
|
+
*
|
|
207
|
+
* **まだ開いていない扇も数に入れる**。 出ている名札だけでほどくと、扇が開くたびに
|
|
208
|
+
* 既に出ている名札が動く = 段の進みの途中で字が跳ねる。
|
|
209
|
+
*/
|
|
210
|
+
const 名札の側 = slices.map((s) => Math.cos(s.中央角) >= 0);
|
|
211
|
+
const 名札の真のy = slices.map((s) => cy + (radius + 引き出し) * Math.sin(s.中央角));
|
|
212
|
+
const 名札のy = (() => {
|
|
213
|
+
const 出す = [...名札の真のy];
|
|
214
|
+
for (const 側 of [true, false]) {
|
|
215
|
+
const 番号 = 名札の側.flatMap((右, i) => (右 === 側 ? [i] : []));
|
|
216
|
+
if (番号.length === 0) continue;
|
|
217
|
+
const 直した = 名札の段をほどく(
|
|
218
|
+
番号.map((i) => 名札の真のy[i]!),
|
|
219
|
+
名札の行,
|
|
220
|
+
PAD_TOP + 名札の縦の張り出し,
|
|
221
|
+
node.h - PAD_BOTTOM - 名札の縦の張り出し,
|
|
222
|
+
);
|
|
223
|
+
番号.forEach((i, k) => (出す[i] = 直した[k]!));
|
|
224
|
+
}
|
|
225
|
+
return 出す;
|
|
226
|
+
})();
|
|
227
|
+
/**
|
|
228
|
+
* 名札の横の位置。 **角度から決めず、円の外の決まった位置に置く** (cdl#727)。
|
|
229
|
+
*
|
|
230
|
+
* 角度から決めたままだと、縦をずらした名札が円の上へ乗る (実測)。 肘も同じ列に
|
|
231
|
+
* 揃えるので、引き出し線が円を横切らない = 肘は必ず半径の外にある。
|
|
232
|
+
*/
|
|
233
|
+
const 肘のx = (右: boolean): number => cx + (右 ? 1 : -1) * (radius + 引き出し);
|
|
234
|
+
const 字のx = (右: boolean): number => 肘のx(右) + (右 ? 字までの間 : -字までの間);
|
|
235
|
+
|
|
194
236
|
|
|
195
237
|
return (
|
|
196
238
|
<g transform={`translate(${x0} ${y0})`}>
|
|
@@ -283,12 +325,12 @@ export function ChartPieNode({
|
|
|
283
325
|
*/}
|
|
284
326
|
{slices.map((s, idx) => {
|
|
285
327
|
if (!開き始めた(s.startFrac)) return null;
|
|
286
|
-
const 右 =
|
|
328
|
+
const 右 = 名札の側[idx]!;
|
|
287
329
|
const 縁x = cx + radius * Math.cos(s.中央角);
|
|
288
330
|
const 縁y = cy + radius * Math.sin(s.中央角);
|
|
289
|
-
const 折れx =
|
|
290
|
-
const 折れy =
|
|
291
|
-
const 字x =
|
|
331
|
+
const 折れx = 肘のx(右);
|
|
332
|
+
const 折れy = 名札のy[idx]!;
|
|
333
|
+
const 字x = 字のx(右);
|
|
292
334
|
return (
|
|
293
335
|
<g key={`callout-${idx}`}>
|
|
294
336
|
<path
|
|
@@ -5,6 +5,7 @@ import { resolveChartData } from "../render/payload-binding";
|
|
|
5
5
|
import { textWidth } from "./text-width";
|
|
6
6
|
import { 実値表記 } from "./chart-number";
|
|
7
7
|
import { 進みを畳む, 伸ばすdash } from "./draw-progress";
|
|
8
|
+
import { 名札の段をほどく } from "./label-stack";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* chart-slope kind ... 2 時点を 1 本の直線でつなぎ、増減と順位の入れ替わりを見せる図 (cdl#688)。
|
|
@@ -84,71 +85,6 @@ import { 進みを畳む, 伸ばすdash } from "./draw-progress";
|
|
|
84
85
|
/** 線を引くのに最低限要る横幅。 名札が長い図でもこれだけは残す */
|
|
85
86
|
export const 線の最小幅 = 120;
|
|
86
87
|
|
|
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
|
-
|
|
152
88
|
type 系列 = {
|
|
153
89
|
名前: string;
|
|
154
90
|
前: number;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 名札の縦の重なりをほどく (cdl#723 / cdl#727)。
|
|
3
|
+
*
|
|
4
|
+
* 字の位置を値から決める図 (傾き図 / 円グラフ) は、**値が同じ / 近い件があると名札が
|
|
5
|
+
* 重なって読めない**。 実測で傾き図は 59 と 59 の 2 件が、円グラフは 0.3% の小片 2 個が
|
|
6
|
+
* 完全に重なっていた。
|
|
7
|
+
*
|
|
8
|
+
* ## 動かすのは名札だけ
|
|
9
|
+
*
|
|
10
|
+
* 点や扇は真の位置に残す。 位置をずらすと、書いていない差を絵が主張することになる
|
|
11
|
+
* (同順位を段でずらさないのと同じ理由、cdl#707)。 名札は図の量を表さないので動かしてよい。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 名札の縦の位置をほどく。
|
|
16
|
+
*
|
|
17
|
+
* ## 重なった連なりは元の位置の中心へ置き直す
|
|
18
|
+
*
|
|
19
|
+
* 上から順に押し下げるだけだと塊が下へ寄り、点との対応が片側だけ遠くなる。
|
|
20
|
+
* **元の位置からの動きが最も小さくなる並べ方** を選ぶ = 重なった連なりごとに、元の位置の
|
|
21
|
+
* 平均を中心にして間隔ぶんずつ均等に置く。
|
|
22
|
+
*
|
|
23
|
+
* 実装は単調回帰。 `y[k+1] - y[k] >= 間隔` は `y[k] - 間隔 * k` が単調非減少と同じなので、
|
|
24
|
+
* その量に隣接違反の併合を当てて戻す。 併合した塊は二度と割れないので、件数ぶんで終わる。
|
|
25
|
+
*
|
|
26
|
+
* @param 真のy 点の縦の位置。 並びは系列の順
|
|
27
|
+
* @param 間隔 名札 1 行が縦に要する高さ
|
|
28
|
+
* @param 上限 名札を置いてよい上端
|
|
29
|
+
* @param 下限 名札を置いてよい下端
|
|
30
|
+
* @returns 系列の順に対応する名札の縦の位置
|
|
31
|
+
*/
|
|
32
|
+
export function 名札の段をほどく(
|
|
33
|
+
真のy: readonly number[],
|
|
34
|
+
間隔: number,
|
|
35
|
+
上限: number,
|
|
36
|
+
下限: number,
|
|
37
|
+
): number[] {
|
|
38
|
+
const 件数 = 真のy.length;
|
|
39
|
+
if (件数 === 0) return [];
|
|
40
|
+
// 上から順に解く。 元の並び順は最後に戻す。 同じ y の件は系列の順で上下を決める
|
|
41
|
+
const 並び = 真のy.map((y, i) => ({ y, i })).sort((a, b) => a.y - b.y || a.i - b.i);
|
|
42
|
+
const 結果 = new Array<number>(件数);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* **入り切らない図では均等に割り付ける**。 間隔を守る置き方が無いので、どれか 1 つを
|
|
46
|
+
* 枠の外へ出すのではなく全部を等しく詰める。 重なりは残るが、寄る先が偏らない。
|
|
47
|
+
*/
|
|
48
|
+
if (間隔 * (件数 - 1) > 下限 - 上限) {
|
|
49
|
+
並び.forEach((e, k) => {
|
|
50
|
+
結果[e.i] = 件数 > 1 ? 上限 + ((下限 - 上限) / (件数 - 1)) * k : (上限 + 下限) / 2;
|
|
51
|
+
});
|
|
52
|
+
return 結果;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 塊は「合計」 と「件数」 だけ持てば平均が出る
|
|
56
|
+
const 塊たち: { 合計: number; 件数: number }[] = [];
|
|
57
|
+
並び.forEach((e, k) => {
|
|
58
|
+
塊たち.push({ 合計: e.y - 間隔 * k, 件数: 1 });
|
|
59
|
+
while (塊たち.length >= 2) {
|
|
60
|
+
const 後 = 塊たち[塊たち.length - 1]!;
|
|
61
|
+
const 前 = 塊たち[塊たち.length - 2]!;
|
|
62
|
+
if (前.合計 / 前.件数 <= 後.合計 / 後.件数) break;
|
|
63
|
+
塊たち.splice(塊たち.length - 2, 2, { 合計: 前.合計 + 後.合計, 件数: 前.件数 + 後.件数 });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const ほどいた: number[] = [];
|
|
68
|
+
塊たち.forEach((塊) => {
|
|
69
|
+
const 平均 = 塊.合計 / 塊.件数;
|
|
70
|
+
for (let j = 0; j < 塊.件数; j++) ほどいた.push(平均 + 間隔 * ほどいた.length);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// 枠へ収める。 **全体を同じだけ動かす** ので、ほどいた間隔は崩れない
|
|
74
|
+
const ずれ = Math.max(0, 上限 - ほどいた[0]!) - Math.max(0, ほどいた[件数 - 1]! - 下限);
|
|
75
|
+
並び.forEach((e, k) => (結果[e.i] = ほどいた[k]! + ずれ));
|
|
76
|
+
return 結果;
|
|
77
|
+
}
|