@cardenelabs/cdl 0.13.1 → 0.15.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 +250 -1
- package/README.md +20 -1
- package/dist/index.cjs +2912 -1723
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -6
- package/dist/index.d.ts +25 -6
- package/dist/index.js +2911 -1724
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1395 -230
- 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 +1395 -230
- package/dist/react.js.map +1 -1
- package/dist/{render-B5JHcUrb.d.cts → render-BS1E6Mlo.d.cts} +55 -2
- package/dist/{render-B5JHcUrb.d.ts → render-BS1E6Mlo.d.ts} +55 -2
- package/package.json +1 -1
- package/src/builder.ts +24 -0
- package/src/index.ts +2 -1
- package/src/kinds/chart-gauge.tsx +241 -0
- package/src/kinds/chart-pie.tsx +214 -53
- package/src/kinds/chart-radial.tsx +210 -0
- package/src/kinds/chart-stacked-bar.tsx +272 -0
- package/src/kinds/chart-stat.tsx +231 -0
- package/src/kinds/chart-waffle.tsx +197 -0
- package/src/kinds/draw-ratio.ts +44 -0
- package/src/kinds/dyn-shape.tsx +3 -6
- package/src/kinds/generic.tsx +18 -6
- package/src/kinds/mind-map.tsx +52 -22
- package/src/kinds/terminal-mark.tsx +96 -0
- package/src/kinds/tree.tsx +51 -21
- package/src/layout/edges.ts +30 -1
- package/src/layout/self-loop.ts +217 -0
- package/src/layout/spec.ts +81 -4
- package/src/layout/tokens.ts +15 -0
- package/src/presets.ts +33 -3
- package/src/render/edge-head.ts +45 -0
- package/src/render/edges.tsx +4 -2
- package/src/render/interactive-panel.tsx +2 -2
- package/src/render/nodes.tsx +25 -0
- package/src/render/payload-binding.ts +14 -3
- package/src/render/stage.tsx +41 -18
- package/src/render/tone.ts +52 -0
- package/src/types.ts +66 -2
- package/src/validate.ts +13 -5
- package/src/visual-validate.ts +137 -4
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { CdlEdge, LaidEdge, LaidNode, Side } from "../types";
|
|
2
|
+
import { CLEARANCE_LABEL_LABEL } from "./clearance-constants";
|
|
3
|
+
import { computeLabelBoxW, LABEL_TO_PATH_CLEARANCE, labelPillH } from "./spec";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 自分から自分へ戻る関係の経路 (#560)。
|
|
7
|
+
*
|
|
8
|
+
* 状態遷移図の自己遷移 (催促する)、シーケンス図の自分宛て (控えを書く)、ER の自己関係
|
|
9
|
+
* (REPORTS TO) の 3 図が使う。 それまで `validate` が弾いていた。
|
|
10
|
+
*
|
|
11
|
+
* 形は **箱の 1 辺から出て、外へ膨らんで、同じ辺へ戻る** 三次曲線 1 本。 折れ線ではなく
|
|
12
|
+
* 曲線にしたのは、線の傾きを見る検査 (軸 13) が曲線を対象外にしているのと同じ理由で、
|
|
13
|
+
* 輪の途中の傾きに意味が無いため。 折れ線で描くと「L 字が破綻している」 と読まれる。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 輪が辺から外へ届く距離 (world)。 制御点ではなく、曲線が実際に到達する距離 */
|
|
17
|
+
export const SELF_LOOP_OUT = 72;
|
|
18
|
+
|
|
19
|
+
/** 出口と入口の間隔 (world)。 辺のどれだけを使うか */
|
|
20
|
+
export const SELF_LOOP_SPAN = 64;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 同じ節に輪が 2 本以上ある時、 2 本目以降を外へずらす幅 (world)。
|
|
24
|
+
*
|
|
25
|
+
* `SELF_LOOP_OUT` と揃えて **3 の倍数** にしてある。 制御点の位置は 3 分の 4 倍で求めるので、
|
|
26
|
+
* 3 の倍数でないと座標が割り切れず、小数点の混じった座標を検査 (軸 33) が拾う。
|
|
27
|
+
*/
|
|
28
|
+
export const SELF_LOOP_STAGGER = 45;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 書かなかった時にどちら側へ出すかを探す順。
|
|
32
|
+
*
|
|
33
|
+
* 節は縦に積むので、上下は前後の節との線が先に使う。 右を最初に見る。
|
|
34
|
+
*
|
|
35
|
+
* **他の線が使っている辺は避ける**。 同じ辺に出すと 2 つ壊れる = 横へ抜ける線が輪の内側を
|
|
36
|
+
* 通って重なり、その線の起点と輪の起点が別の高さになって「1 つの節から出ていない」 ように
|
|
37
|
+
* 見える (検査の軸 2 と軸 61 が両方鳴る、実測した)。 輪は終点を辺の中央付近に置く必要が
|
|
38
|
+
* あるので (軸 63)、同じ辺の中で高さをずらして避けることはできない。
|
|
39
|
+
*/
|
|
40
|
+
export const SELF_LOOP_SIDE_ORDER: readonly Side[] = ["right", "top", "left", "bottom"];
|
|
41
|
+
|
|
42
|
+
/** どの辺も塞がっている時に使う辺。 避けきれないので、少なくとも同じ場所に落とす */
|
|
43
|
+
export const SELF_LOOP_SIDE_FALLBACK: Side = "right";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 三次曲線の制御点を辺からどれだけ離すか、を決める比。
|
|
47
|
+
*
|
|
48
|
+
* 両端が辺の上にあり、制御点を 2 つとも同じだけ外へ置いた三次曲線は、真ん中 (t=0.5) で
|
|
49
|
+
* 制御点の **4 分の 3** までしか届かない。 届かせたい距離から逆算する。
|
|
50
|
+
*
|
|
51
|
+
* 係数を式で持つのは、`SELF_LOOP_OUT` を変えた時に輪の実寸と語の置き場所が一緒に動く
|
|
52
|
+
* 必要があるため。 書き写すと片方だけ動いて語が輪の中に入る。
|
|
53
|
+
*/
|
|
54
|
+
export const CUBIC_APEX_RATIO = 0.75;
|
|
55
|
+
|
|
56
|
+
/** 終点が辺の中央から離れてよい上限を決める余白。 軸 63 の判定と同じ値 */
|
|
57
|
+
const ENDPOINT_CENTER_MARGIN = 8;
|
|
58
|
+
|
|
59
|
+
/** 出口と入口が重なって輪が線 1 本に潰れない最小の間隔 */
|
|
60
|
+
const SPAN_MIN = 8;
|
|
61
|
+
|
|
62
|
+
function controlOffset(out: number): number {
|
|
63
|
+
return out / CUBIC_APEX_RATIO;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 辺の中心と、その辺の外向き */
|
|
67
|
+
function sideGeometry(n: LaidNode, side: Side): { x: number; y: number; ux: number; uy: number } {
|
|
68
|
+
switch (side) {
|
|
69
|
+
case "right":
|
|
70
|
+
return { x: n.cx + n.w / 2, y: n.cy, ux: 1, uy: 0 };
|
|
71
|
+
case "left":
|
|
72
|
+
return { x: n.cx - n.w / 2, y: n.cy, ux: -1, uy: 0 };
|
|
73
|
+
case "top":
|
|
74
|
+
return { x: n.cx, y: n.cy - n.h / 2, ux: 0, uy: -1 };
|
|
75
|
+
case "bottom":
|
|
76
|
+
return { x: n.cx, y: n.cy + n.h / 2, ux: 0, uy: 1 };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 出口と入口の間隔を、その辺に収まる範囲へ丸める。
|
|
82
|
+
*
|
|
83
|
+
* 終点が辺の中央から離れすぎると軸 63 (`arrow-endpoint-center`) が「角に刺さっている」 と
|
|
84
|
+
* 判定する。 判定の境目は辺の半分から 8 を引いた値なので、そこへ収まるまで縮める。
|
|
85
|
+
* 縮めても最小値は残す = 0 にすると出口と入口が重なって輪が潰れる。
|
|
86
|
+
*/
|
|
87
|
+
export function fitSpan(sideLength: number, wanted: number): number {
|
|
88
|
+
const 限度 = sideLength - 2 * ENDPOINT_CENTER_MARGIN;
|
|
89
|
+
return Math.max(SPAN_MIN, Math.min(wanted, 限度));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 1 つの節にある輪 `count` 本に、他の線が使っていない辺を **1 本ずつ配る**。
|
|
94
|
+
*
|
|
95
|
+
* 「使っている」 は `occupied` が持つ = 呼出側が、輪でない辺の起点側と終点側の両方を入れる。
|
|
96
|
+
* 入れ忘れると輪がその線の上に重なるので、呼出側は 2 経路 (自動で引く線と、座標を固定した線)
|
|
97
|
+
* のどちらも入れる。
|
|
98
|
+
*
|
|
99
|
+
* **1 辺に 2 本まとめない**。 同じ辺に重ねると輪が入れ子になり、内側の輪の語を外側の輪の
|
|
100
|
+
* 外へ出さざるをえない = 語が自分の線から遠のく (検査の軸 7 が「どの線の語か読めない」 と
|
|
101
|
+
* 判定する。 実測で 3 本を入れ子にすると一番内側が 160 離れて error になった)。
|
|
102
|
+
*
|
|
103
|
+
* 空いている辺より輪が多い時だけ、余った分を先頭の辺へ戻して重ねる。 そこは重なる形しか
|
|
104
|
+
* 残っていないので、少なくとも同じ場所に集める。
|
|
105
|
+
*/
|
|
106
|
+
export function pickFreeSides(occupied: ReadonlySet<Side> | undefined, count: number): Side[] {
|
|
107
|
+
const 空き = SELF_LOOP_SIDE_ORDER.filter((s) => !occupied?.has(s));
|
|
108
|
+
const 使う = 空き.length > 0 ? 空き : [SELF_LOOP_SIDE_FALLBACK];
|
|
109
|
+
return Array.from({ length: count }, (_, i) => 使う[i % 使う.length]!);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 同じ節・同じ辺の輪をまとめて経路にする。
|
|
114
|
+
*
|
|
115
|
+
* **語は輪の外 (膨らんだ先) に置く**。 輪の上に載せると曲線が語で隠れ、どちら回りかが読めない。
|
|
116
|
+
*
|
|
117
|
+
* 2 本以上ある時、輪は入れ子になる = 内側の輪の外は、外側の輪の **内側** にある。
|
|
118
|
+
* 輪ごとに「自分の外」 へ語を置くと、内側の語が外側の輪の上に載る。 語は全部
|
|
119
|
+
* **一番外の輪のさらに外** に揃えて置き、辺に沿って積んで語どうしの重なりを外す。
|
|
120
|
+
*
|
|
121
|
+
* 1 本だけの時は積む必要が無いので、辺の中央の真横に 1 つ置くだけになる。
|
|
122
|
+
*/
|
|
123
|
+
export function routeSelfLoopGroup(edges: CdlEdge[], node: LaidNode, side: Side): LaidEdge[] {
|
|
124
|
+
const horizontal = side === "left" || side === "right";
|
|
125
|
+
const g = sideGeometry(node, side);
|
|
126
|
+
const span = fitSpan(horizontal ? node.h : node.w, SELF_LOOP_SPAN);
|
|
127
|
+
const half = span / 2;
|
|
128
|
+
// 辺に沿った向き (出口 → 入口)。 横向きの辺なら上から下、縦向きの辺なら左から右
|
|
129
|
+
const 沿 = horizontal ? { x: 0, y: 1 } : { x: 1, y: 0 };
|
|
130
|
+
|
|
131
|
+
// 制御点は曲線より外にあるので、path から囲みを起こす検査は輪を実寸より大きく見る。
|
|
132
|
+
// 語はその外側に置く = 大きめに見た囲みとも重ならない
|
|
133
|
+
const 最外 = controlOffset(SELF_LOOP_OUT + (edges.length - 1) * SELF_LOOP_STAGGER);
|
|
134
|
+
const 語まで = 最外 + LABEL_TO_PATH_CLEARANCE;
|
|
135
|
+
|
|
136
|
+
// 積む向きの語の大きさ。 横の辺なら縦に積むので高さ、縦の辺なら横に積むので幅
|
|
137
|
+
const 語幅 = edges.map((e) =>
|
|
138
|
+
horizontal ? labelPillH(e.sub) : computeLabelBoxW(e.label, e.sub),
|
|
139
|
+
);
|
|
140
|
+
const 位置: number[] = [];
|
|
141
|
+
let 積 = 0;
|
|
142
|
+
for (const w of 語幅) {
|
|
143
|
+
位置.push(積 + w / 2);
|
|
144
|
+
積 += w + CLEARANCE_LABEL_LABEL;
|
|
145
|
+
}
|
|
146
|
+
const 全長 = Math.max(0, 積 - CLEARANCE_LABEL_LABEL);
|
|
147
|
+
|
|
148
|
+
return edges.map((edge, index) => {
|
|
149
|
+
const c = controlOffset(SELF_LOOP_OUT + index * SELF_LOOP_STAGGER);
|
|
150
|
+
const x1 = g.x - 沿.x * half;
|
|
151
|
+
const y1 = g.y - 沿.y * half;
|
|
152
|
+
const x2 = g.x + 沿.x * half;
|
|
153
|
+
const y2 = g.y + 沿.y * half;
|
|
154
|
+
const ずれ = 位置[index]! - 全長 / 2;
|
|
155
|
+
return {
|
|
156
|
+
...edge,
|
|
157
|
+
d: `M ${x1} ${y1} C ${x1 + g.ux * c} ${y1 + g.uy * c} ${x2 + g.ux * c} ${y2 + g.uy * c} ${x2} ${y2}`,
|
|
158
|
+
fromSide: side,
|
|
159
|
+
toSide: side,
|
|
160
|
+
labelX: g.x + g.ux * 語まで + 沿.x * ずれ + (edge.labelOffsetX ?? 0),
|
|
161
|
+
labelY: g.y + g.uy * 語まで + 沿.y * ずれ + (edge.labelOffsetY ?? 0),
|
|
162
|
+
labelAnchor: side === "left" ? "end" : side === "right" ? "start" : "middle",
|
|
163
|
+
} satisfies LaidEdge;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** その辺が自分から自分へ戻るか */
|
|
168
|
+
export function isSelfLoop(e: { from: string; to: string }): boolean {
|
|
169
|
+
return e.from === e.to;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 図の中の輪をまとめて経路にする。
|
|
174
|
+
*
|
|
175
|
+
* 同じ節・同じ辺に 2 本以上あると重なるので、その並びの中の順番を渡して外へずらす。
|
|
176
|
+
* 節が見つからない辺は **落とさずに投げる** = 黙って消すと、書いたのに描かれない線になる。
|
|
177
|
+
*
|
|
178
|
+
* @param occupiedBySide 節ごとに、輪でない線が使っている辺。 これを渡さないと輪が既存の線に重なる
|
|
179
|
+
*/
|
|
180
|
+
export function layoutSelfLoops(
|
|
181
|
+
edges: CdlEdge[],
|
|
182
|
+
nodes: Map<string, LaidNode>,
|
|
183
|
+
occupiedBySide: ReadonlyMap<string, ReadonlySet<Side>>,
|
|
184
|
+
): LaidEdge[] {
|
|
185
|
+
// 辺の割り当ては節ごとに 1 度で決める。 輪を 1 本ずつ処理して選び直すと、2 本目が
|
|
186
|
+
// 1 本目の辺を「空いている」 と見て同じ場所に重なる
|
|
187
|
+
const 割当 = new Map<string, Side[]>();
|
|
188
|
+
for (const edge of edges) {
|
|
189
|
+
if (edge.side) continue;
|
|
190
|
+
const list = 割当.get(edge.from) ?? [];
|
|
191
|
+
list.push("right");
|
|
192
|
+
割当.set(edge.from, list);
|
|
193
|
+
}
|
|
194
|
+
for (const [nodeId, list] of 割当) {
|
|
195
|
+
割当.set(nodeId, pickFreeSides(occupiedBySide.get(nodeId), list.length));
|
|
196
|
+
}
|
|
197
|
+
const 消費 = new Map<string, number>();
|
|
198
|
+
|
|
199
|
+
const 束 = new Map<string, { node: LaidNode; side: Side; edges: CdlEdge[] }>();
|
|
200
|
+
for (const edge of edges) {
|
|
201
|
+
const node = nodes.get(edge.from);
|
|
202
|
+
if (!node) throw new Error(`cdl layout: edge "${edge.id}" の from "${edge.from}" が node に無い`);
|
|
203
|
+
let side = edge.side;
|
|
204
|
+
if (!side) {
|
|
205
|
+
const i = 消費.get(edge.from) ?? 0;
|
|
206
|
+
消費.set(edge.from, i + 1);
|
|
207
|
+
side = 割当.get(edge.from)![i]!;
|
|
208
|
+
}
|
|
209
|
+
const key = `${edge.from}::${side}`;
|
|
210
|
+
const 既 = 束.get(key);
|
|
211
|
+
if (既) 既.edges.push(edge);
|
|
212
|
+
else 束.set(key, { node, side, edges: [edge] });
|
|
213
|
+
}
|
|
214
|
+
// 語の置き場所は束の中の並びで決まるので、束ごとにまとめて経路にする。
|
|
215
|
+
// 返す並びは呼出側が id で引き直すため、ここでは束の順で構わない
|
|
216
|
+
return [...束.values()].flatMap((b) => routeSelfLoopGroup(b.edges, b.node, b.side));
|
|
217
|
+
}
|
package/src/layout/spec.ts
CHANGED
|
@@ -62,9 +62,10 @@ export function hasRenderedLabel(e: { label?: string; sub?: string }): boolean {
|
|
|
62
62
|
/**
|
|
63
63
|
* `title` を文字として描かない node の種別。
|
|
64
64
|
*
|
|
65
|
-
*
|
|
65
|
+
* 3 群ある。 形だけを描く `dyn-*` 5 種は `DynShapeNode` に回り、 `node.title` を一度も参照しない
|
|
66
66
|
* (描くのは形と数値の小さな label だけ)。 payload を自前 layout する chart / timeline / analytic
|
|
67
|
-
* 10 種は、 描くのが payload 側の label で `title` は使わない。
|
|
67
|
+
* 10 種は、 描くのが payload 側の label で `title` は使わない。 流れの始まりと終わりの印 2 種は
|
|
68
|
+
* 印そのものが意味を持ち、 丸の中に名前を入れても読めない (#560)。
|
|
68
69
|
*
|
|
69
70
|
* 一覧は手で持つと描画側とずれるため、 `test/node-title-render.test.tsx` が `NODE_KINDS` を
|
|
70
71
|
* **全件 render** して「title の文字が出るか」 と本述語の返り値の一致を検査する。 新しい種別を
|
|
@@ -81,12 +82,20 @@ const KINDS_WITHOUT_TITLE_TEXT: ReadonlySet<string> = new Set([
|
|
|
81
82
|
"chart-pie",
|
|
82
83
|
"chart-line",
|
|
83
84
|
"chart-bar",
|
|
85
|
+
"chart-gauge",
|
|
86
|
+
"chart-radial",
|
|
87
|
+
"chart-stat",
|
|
88
|
+
"chart-waffle",
|
|
89
|
+
"chart-stacked-bar",
|
|
84
90
|
"gantt-timeline",
|
|
85
91
|
"mind-map",
|
|
86
92
|
"funnel-stages",
|
|
87
93
|
"quadrant-matrix",
|
|
88
94
|
"tree-hierarchy",
|
|
89
95
|
"journey-map",
|
|
96
|
+
// 印そのものが意味を持つ (#560)
|
|
97
|
+
"mark-start",
|
|
98
|
+
"mark-end",
|
|
90
99
|
]);
|
|
91
100
|
|
|
92
101
|
/**
|
|
@@ -189,7 +198,45 @@ export function rowBaselineY(kind: string | undefined, index: number): number |
|
|
|
189
198
|
if (!rendersRows(kind)) return null;
|
|
190
199
|
return kind === "storage"
|
|
191
200
|
? ROW_TOP_STORAGE + index * ROW_PITCH_STORAGE
|
|
192
|
-
: ROW_TOP_GENERIC + index * ROW_PITCH_GENERIC;
|
|
201
|
+
: ROW_TOP_GENERIC + genericTextShift(kind) + index * ROW_PITCH_GENERIC;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* `kinds/generic.tsx` が字の塊を下へずらす量 (SSOT)。
|
|
206
|
+
*
|
|
207
|
+
* 円柱は上の楕円 (ry=14) の内側に字を置けないため、題も行もこの分だけ下がる。
|
|
208
|
+
* **題と行が同じ値を引く** = 別々に持つと片方だけ動いて重なる (#558 で実際に重なっていた)。
|
|
209
|
+
*/
|
|
210
|
+
export function genericTextShift(kind: string | undefined): number {
|
|
211
|
+
return KINDS_SHAPE_CYLINDER.has(kind ?? "") ? GENERIC_TEXT_SHIFT_CYLINDER : 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 円柱で描く種別。 `kinds/generic.tsx` の `STYLES` が持つ形と 1 対 1 で対応する */
|
|
215
|
+
const KINDS_SHAPE_CYLINDER: ReadonlySet<string> = new Set(["database"]);
|
|
216
|
+
|
|
217
|
+
/** 中央に字を寄せる種別 (`cloud` / `decision`)。 同じく `STYLES` の形と対応する */
|
|
218
|
+
const KINDS_TEXT_CENTERED: ReadonlySet<string> = new Set(["cloud", "decision"]);
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* その node が字を中央へ寄せるか (SSOT)。
|
|
222
|
+
*
|
|
223
|
+
* **行を持つ時は寄せない**。 題を箱の中心に置くと、上端から数える行 (`ROW_TOP_GENERIC`) と
|
|
224
|
+
* 必ず重なる = 中心に題を置いたまま行を並べる場所が無い。 行がある時だけ上揃えに落とす。
|
|
225
|
+
*/
|
|
226
|
+
export function genericTextCentered(kind: string | undefined, hasRows: boolean): boolean {
|
|
227
|
+
return !hasRows && KINDS_TEXT_CENTERED.has(kind ?? "");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* `kinds/generic.tsx` の題の baseline (SSOT)。
|
|
232
|
+
*
|
|
233
|
+
* 中央寄せの時だけ箱の高さに依るので `h` を受ける。 検査軸 (`title-row-overlap`) が
|
|
234
|
+
* 行との間隔を測るために同じ関数を引く。
|
|
235
|
+
*/
|
|
236
|
+
export function genericTitleBaselineY(kind: string | undefined, h: number, hasRows: boolean): number {
|
|
237
|
+
return genericTextCentered(kind, hasRows)
|
|
238
|
+
? h / 2 + GENERIC_TITLE_CENTER_OFFSET
|
|
239
|
+
: GENERIC_TITLE_TOP + genericTextShift(kind);
|
|
193
240
|
}
|
|
194
241
|
|
|
195
242
|
/** `kinds/storage.tsx` の 1 行目 baseline と行送り。 */
|
|
@@ -200,6 +247,25 @@ export const ROW_PITCH_STORAGE = 56;
|
|
|
200
247
|
export const ROW_TOP_GENERIC = 100;
|
|
201
248
|
export const ROW_PITCH_GENERIC = 28;
|
|
202
249
|
|
|
250
|
+
/** 円柱で字の塊を下へずらす量。 上の楕円 (ry=14) の内側を避ける */
|
|
251
|
+
export const GENERIC_TEXT_SHIFT_CYLINDER = 28;
|
|
252
|
+
|
|
253
|
+
/** `kinds/generic.tsx` の上揃え時の題の baseline と、中央寄せ時の中心からの差 */
|
|
254
|
+
export const GENERIC_TITLE_TOP = 68;
|
|
255
|
+
export const GENERIC_TITLE_CENTER_OFFSET = 4;
|
|
256
|
+
|
|
257
|
+
/** `kinds/generic.tsx` の題の字の大きさ。 題の字形がどこまで下へ出るかの見積もりに使う */
|
|
258
|
+
export const GENERIC_TITLE_FONT = 20;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 字形の上端を baseline から何割上と **見込むか** (cap 高さの目安)。
|
|
262
|
+
*
|
|
263
|
+
* `STORAGE_ROW_DIVIDER_Y` が区切り線の位置を出すのに使っていた 0.72 を、
|
|
264
|
+
* 題と行の間隔を見る軸 (`title-row-overlap`) と共有する。 別々に持つと片方だけ動く。
|
|
265
|
+
*/
|
|
266
|
+
export const GLYPH_CAP_RATIO = 0.72;
|
|
267
|
+
|
|
268
|
+
|
|
203
269
|
/**
|
|
204
270
|
* 行の文字寸法。 `kinds/storage.tsx` は 26px、 `kinds/generic.tsx` は左 18px / 右 21px。
|
|
205
271
|
* descender の見積もりには大きい方を使う。
|
|
@@ -220,7 +286,7 @@ export const STORAGE_TITLE_BASELINE = 78;
|
|
|
220
286
|
* 1 行目の字形の上端は baseline から font size の 0.72 倍だけ上と見込む (cap 高さの目安)。
|
|
221
287
|
*/
|
|
222
288
|
export const STORAGE_ROW_DIVIDER_Y = Math.round(
|
|
223
|
-
(STORAGE_TITLE_BASELINE + (ROW_TOP_STORAGE - ROW_FONT_STORAGE *
|
|
289
|
+
(STORAGE_TITLE_BASELINE + (ROW_TOP_STORAGE - ROW_FONT_STORAGE * GLYPH_CAP_RATIO)) / 2,
|
|
224
290
|
);
|
|
225
291
|
|
|
226
292
|
/**
|
|
@@ -242,6 +308,17 @@ export const STORAGE_ROW_DIVIDER_Y = Math.round(
|
|
|
242
308
|
*/
|
|
243
309
|
export const ROW_GLYPH_DEPTH_RATIO = 0.4;
|
|
244
310
|
|
|
311
|
+
/**
|
|
312
|
+
* 題の字形の下端と 1 行目の字形の上端が触れないために要ると **見込む** 間隔。
|
|
313
|
+
*
|
|
314
|
+
* 題は下へ `GENERIC_TITLE_FONT * ROW_GLYPH_DEPTH_RATIO`、行は上へ
|
|
315
|
+
* `ROW_FONT_GENERIC * GLYPH_CAP_RATIO` 伸びると見込む。 その和を下限にする。
|
|
316
|
+
*
|
|
317
|
+
* 仮定の範囲は `ROW_GLYPH_DEPTH_RATIO` の説明と同じ = preset が出す文字の範囲を押さえる。
|
|
318
|
+
*/
|
|
319
|
+
export const TITLE_ROW_MIN_GAP =
|
|
320
|
+
GENERIC_TITLE_FONT * ROW_GLYPH_DEPTH_RATIO + ROW_FONT_GENERIC * GLYPH_CAP_RATIO;
|
|
321
|
+
|
|
245
322
|
/**
|
|
246
323
|
* 最終行の baseline から下に、 字形が枠に触れないために要ると **見込む** 量。
|
|
247
324
|
*
|
package/src/layout/tokens.ts
CHANGED
|
@@ -52,6 +52,16 @@ export const TOKENS = {
|
|
|
52
52
|
"chart-line": { w: 640, h: 360 },
|
|
53
53
|
"chart-pie": { w: 640, h: 320 },
|
|
54
54
|
"chart-bar": { w: 640, h: 360 },
|
|
55
|
+
// 上半分の弧 + 下段の内訳。 弧が浅いぶん pie より低くて済む (cdl#549)
|
|
56
|
+
"chart-gauge": { w: 640, h: 320 },
|
|
57
|
+
// 左に一覧 + 右に同心の弧。 件数ぶん内側へ重なるので pie と同じ高さを取る (cdl#550)
|
|
58
|
+
"chart-radial": { w: 640, h: 320 },
|
|
59
|
+
// 件ごとに区画を割り、名前 / 数値 / 弧 / 割合 を縦に積む。 数値を大きく組むので縦が要る (cdl#553)
|
|
60
|
+
"chart-stat": { w: 640, h: 320 },
|
|
61
|
+
// 左に一覧 + 右に 10x10 の格子。 格子が正方形なので pie と同じ高さで足りる (cdl#552)
|
|
62
|
+
"chart-waffle": { w: 640, h: 320 },
|
|
63
|
+
// 帯を最大 2 本 + 下に一覧。 縦は帯 2 本ぶんで足りる (cdl#551)
|
|
64
|
+
"chart-stacked-bar": { w: 640, h: 320 },
|
|
55
65
|
// Timeline / Mind 系 (CAR-994 Phase B)
|
|
56
66
|
"gantt-timeline": { w: 720, h: 360 },
|
|
57
67
|
"mind-map": { w: 720, h: 480 },
|
|
@@ -123,6 +133,11 @@ export const TOKENS = {
|
|
|
123
133
|
"shape-lawyer": { w: 280, h: 300 },
|
|
124
134
|
"shape-trader": { w: 300, h: 300 },
|
|
125
135
|
"shape-customer-service": { w: 320, h: 300 },
|
|
136
|
+
// 流れの始まりと終わりの印 2 種 (#560) ... 正方形にして丸が縦横どちらにも歪まないようにする。
|
|
137
|
+
// 印は箱いっぱいに描く (線が箱の縁で止まるため) ので、この値が丸の直径になる。
|
|
138
|
+
// 箱としての最小 (80x40) より大きく、名前を持つ箱 (card 320x150) よりは明らかに小さい
|
|
139
|
+
"mark-start": { w: 96, h: 96 },
|
|
140
|
+
"mark-end": { w: 96, h: 96 },
|
|
126
141
|
} as Record<NodeKind, { w: number; h: number }>,
|
|
127
142
|
|
|
128
143
|
// row gap (stack 間の vertical gap、 lane 内 / 跨ぎ共通)
|
package/src/presets.ts
CHANGED
|
@@ -1538,13 +1538,23 @@ export function quadrant(preset: QuadrantPreset): QuadrantBuilder {
|
|
|
1538
1538
|
|
|
1539
1539
|
// ─── chart (Pie / Bar / Line 統合) ─────────────────────────────
|
|
1540
1540
|
|
|
1541
|
-
export type ChartType =
|
|
1541
|
+
export type ChartType =
|
|
1542
|
+
| "pie"
|
|
1543
|
+
| "bar"
|
|
1544
|
+
| "line"
|
|
1545
|
+
| "gauge"
|
|
1546
|
+
| "radial"
|
|
1547
|
+
| "stat"
|
|
1548
|
+
| "waffle"
|
|
1549
|
+
| "stacked-bar";
|
|
1542
1550
|
|
|
1543
1551
|
export type ChartDatum = {
|
|
1544
1552
|
id: string;
|
|
1545
1553
|
label: string;
|
|
1546
1554
|
/** 棒 / 折れ線 / 扇の大きさ。 `{名前}` を書くと状態から読む (#468) */
|
|
1547
1555
|
value: number | BindingRef;
|
|
1556
|
+
/** 前の時点の値 (cdl#551)。 `type: stacked-bar` で書くと帯が 2 本になる */
|
|
1557
|
+
previous?: number | BindingRef;
|
|
1548
1558
|
/** 個別 tone (default は preset の defaultTone) */
|
|
1549
1559
|
tone?: Tone;
|
|
1550
1560
|
};
|
|
@@ -1580,7 +1590,17 @@ export function chart(preset: ChartPreset): ChartBuilder {
|
|
|
1580
1590
|
const canvasW = toGridSize(preset.itemWidth ?? 640);
|
|
1581
1591
|
// 高さも格子の倍数にする。 line / bar は 360 だったが 360 % 16 = 8 で下端が格子から外れ、
|
|
1582
1592
|
// 箱の 4 辺が載る条件を満たさなかった。
|
|
1583
|
-
|
|
1593
|
+
// 円と半円は縦を使わないぶん低い。 折れ線と棒は軸のぶん高さが要る
|
|
1594
|
+
const canvasH = toGridSize(
|
|
1595
|
+
preset.type === "pie" ||
|
|
1596
|
+
preset.type === "gauge" ||
|
|
1597
|
+
preset.type === "radial" ||
|
|
1598
|
+
preset.type === "stat" ||
|
|
1599
|
+
preset.type === "waffle" ||
|
|
1600
|
+
preset.type === "stacked-bar"
|
|
1601
|
+
? 320
|
|
1602
|
+
: 360,
|
|
1603
|
+
);
|
|
1584
1604
|
const b = diagram(preset.id, { topic: preset.topic });
|
|
1585
1605
|
const data: ChartDatum[] = [];
|
|
1586
1606
|
|
|
@@ -1588,6 +1608,11 @@ export function chart(preset: ChartPreset): ChartBuilder {
|
|
|
1588
1608
|
line: "chart-line" as const,
|
|
1589
1609
|
pie: "chart-pie" as const,
|
|
1590
1610
|
bar: "chart-bar" as const,
|
|
1611
|
+
gauge: "chart-gauge" as const,
|
|
1612
|
+
radial: "chart-radial" as const,
|
|
1613
|
+
stat: "chart-stat" as const,
|
|
1614
|
+
waffle: "chart-waffle" as const,
|
|
1615
|
+
"stacked-bar": "chart-stacked-bar" as const,
|
|
1591
1616
|
};
|
|
1592
1617
|
|
|
1593
1618
|
const api: ChartBuilder = {
|
|
@@ -1606,7 +1631,12 @@ export function chart(preset: ChartPreset): ChartBuilder {
|
|
|
1606
1631
|
eyebrow: preset.type,
|
|
1607
1632
|
w: canvasW,
|
|
1608
1633
|
h: canvasH,
|
|
1609
|
-
chartData: data.map((d) => ({
|
|
1634
|
+
chartData: data.map((d) => ({
|
|
1635
|
+
label: d.label,
|
|
1636
|
+
value: d.value,
|
|
1637
|
+
...(d.previous === undefined ? {} : { previous: d.previous }),
|
|
1638
|
+
tone: d.tone,
|
|
1639
|
+
})),
|
|
1610
1640
|
});
|
|
1611
1641
|
b.phase(
|
|
1612
1642
|
"chart",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { EDGE_HEAD_DEFAULT, type EdgeHead } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 関係の端の形の **描き方と marker の名前** (SSOT、 #560)。
|
|
5
|
+
*
|
|
6
|
+
* `render/stage.tsx` が marker を定義し、 `render/edges.tsx` がそれを引く。 2 箇所が別々に
|
|
7
|
+
* 名前を組み立てると、 片方だけ変えた時に線が指す marker が消えて矢頭が出なくなる
|
|
8
|
+
* (`tone-marker-ref` 軸が拾うが、 拾うまでの間は矢頭が無い絵になる)。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type EdgeHeadShape = {
|
|
12
|
+
/** 10x10 の viewBox に描く path */
|
|
13
|
+
d: string;
|
|
14
|
+
/** path の端のどこを線の先端に合わせるか */
|
|
15
|
+
refX: number;
|
|
16
|
+
/** 塗るか。 塗らない形は線で描く */
|
|
17
|
+
filled: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 4 形の描き方。
|
|
22
|
+
*
|
|
23
|
+
* 塗る形 (三角 / 菱) は線の端を隠すので、線をその手前で止める必要は無い = marker は
|
|
24
|
+
* 線の上に重なって描かれる。 塗らない形 (開いた矢 / 三又) は隠さない。
|
|
25
|
+
*
|
|
26
|
+
* 三又だけ `refX` が 10 = 三又は **開いた側が箱に触る** 形で、他の 3 形と前後が逆になる。
|
|
27
|
+
* 9 にすると開いた側が箱から 1 だけ浮く。
|
|
28
|
+
*/
|
|
29
|
+
export const EDGE_HEAD_SHAPE: Record<EdgeHead, EdgeHeadShape> = {
|
|
30
|
+
triangle: { d: "M 0 0 L 10 5 L 0 10 z", refX: 9, filled: true },
|
|
31
|
+
diamond: { d: "M 0 5 L 5 0 L 10 5 L 5 10 z", refX: 9, filled: true },
|
|
32
|
+
open: { d: "M 1 0 L 9 5 L 1 10", refX: 9, filled: false },
|
|
33
|
+
crow: { d: "M 10 0 L 0 5 M 10 5 L 0 5 M 10 10 L 0 5", refX: 10, filled: false },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* marker の名前を組み立てる。
|
|
38
|
+
*
|
|
39
|
+
* **既定の形は名前を変えない** (`cdl-arrow-<tone>` / `cdl-arrow-<tone>-sm`)。
|
|
40
|
+
* 端の形を書いていない図はすべて既定なので、名前が変わると全図の矢頭が差し替わる。
|
|
41
|
+
*/
|
|
42
|
+
export function edgeHeadMarkerId(tone: string, head: EdgeHead, small: boolean): string {
|
|
43
|
+
const 小 = small ? "-sm" : "";
|
|
44
|
+
return head === EDGE_HEAD_DEFAULT ? `cdl-arrow-${tone}${小}` : `cdl-arrow-${tone}-${head}${小}`;
|
|
45
|
+
}
|
package/src/render/edges.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JSX } from "react";
|
|
2
|
-
import type
|
|
2
|
+
import { EDGE_HEAD_DEFAULT, type LaidEdge } from "../types";
|
|
3
|
+
import { edgeHeadMarkerId } from "./edge-head";
|
|
3
4
|
import { computeLabelBBoxWorld, hasRenderedLabel, LABEL_PILL_PAD_X } from "../layout/spec";
|
|
4
5
|
import { hasTone, toneColor } from "./tone";
|
|
5
6
|
import { EDGE_LABEL_TEXT, fontFamilyAttr, type EdgeLabelLine } from "../label-text";
|
|
@@ -43,8 +44,9 @@ export function CdlEdgeView({
|
|
|
43
44
|
// 1.0 でも 2.92-5.55)。 dragon は edge 用に濃くした色 (#0063a1 等) を CSS で当てており、
|
|
44
45
|
// その色なら 0.9 で 4.51-4.92 と 4.5:1 を満たす (0.8 では 3.69-4.04 で届かない)。
|
|
45
46
|
// 実線と点線で同じ組を使い、 光らせた側が薄くなる逆転を作らない。
|
|
47
|
+
// 端の形は線の種類と別の軸 (#560)。 書かなければ既定の塗った三角で、名前も従来のまま
|
|
46
48
|
const marker = hasTone(edge.tone)
|
|
47
|
-
?
|
|
49
|
+
? edgeHeadMarkerId(edge.tone, edge.head ?? EDGE_HEAD_DEFAULT, !active)
|
|
48
50
|
: "cdl-arrow-muted";
|
|
49
51
|
|
|
50
52
|
const style = edge.style ?? "solid";
|
|
@@ -4133,7 +4133,6 @@ function SlopeReadout({
|
|
|
4133
4133
|
}
|
|
4134
4134
|
const pad = 20;
|
|
4135
4135
|
const range = max - min || 1;
|
|
4136
|
-
const w = viewW - pad * 2;
|
|
4137
4136
|
const h = viewH - pad * 2;
|
|
4138
4137
|
const xL = pad;
|
|
4139
4138
|
const xR = viewW - pad;
|
|
@@ -8433,7 +8432,8 @@ function VoiceMessageReadout({
|
|
|
8433
8432
|
const barsX = 36;
|
|
8434
8433
|
const barsW = width - barsX - 44;
|
|
8435
8434
|
const barsH = 30;
|
|
8436
|
-
|
|
8435
|
+
// 縦位置は棒ごとに自分の高さから出す (下の `y`)。 まとめて 1 つ持つと、
|
|
8436
|
+
// 高さの違う棒が同じ位置から描かれて中央で揃わない
|
|
8437
8437
|
const bw = shown.length > 0 ? barsW / shown.length : 0;
|
|
8438
8438
|
const activeCount = Math.floor(shown.length * p);
|
|
8439
8439
|
const durMin = Math.floor(duration / 60);
|
package/src/render/nodes.tsx
CHANGED
|
@@ -5,9 +5,15 @@ import { EventNode } from "../kinds/event";
|
|
|
5
5
|
import { FunctionNode } from "../kinds/function";
|
|
6
6
|
import { StorageNode } from "../kinds/storage";
|
|
7
7
|
import { GenericNode } from "../kinds/generic";
|
|
8
|
+
import { MarkStartNode, MarkEndNode } from "../kinds/terminal-mark";
|
|
8
9
|
import { DynShapeNode } from "../kinds/dyn-shape";
|
|
9
10
|
import { ChartLineNode } from "../kinds/chart-line";
|
|
10
11
|
import { ChartPieNode } from "../kinds/chart-pie";
|
|
12
|
+
import { ChartGaugeNode } from "../kinds/chart-gauge";
|
|
13
|
+
import { ChartRadialNode } from "../kinds/chart-radial";
|
|
14
|
+
import { ChartStatNode } from "../kinds/chart-stat";
|
|
15
|
+
import { ChartWaffleNode } from "../kinds/chart-waffle";
|
|
16
|
+
import { ChartStackedBarNode } from "../kinds/chart-stacked-bar";
|
|
11
17
|
import { ChartBarNode } from "../kinds/chart-bar";
|
|
12
18
|
import { GanttNode } from "../kinds/gantt";
|
|
13
19
|
import { MindMapNode } from "../kinds/mind-map";
|
|
@@ -179,6 +185,21 @@ export function CdlNodeView({
|
|
|
179
185
|
progress={progress}
|
|
180
186
|
/>
|
|
181
187
|
);
|
|
188
|
+
case "chart-gauge":
|
|
189
|
+
// 段の進みで弧が開く動きは持たない (cdl#549 で対象外にした)
|
|
190
|
+
return <ChartGaugeNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
191
|
+
case "chart-stacked-bar":
|
|
192
|
+
// 帯を割合で区切る。 前の時点を書けば 2 本になる (cdl#551)
|
|
193
|
+
return <ChartStackedBarNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
194
|
+
case "chart-waffle":
|
|
195
|
+
// 1 個 = 1% の印を 100 個。 段の進みで印が埋まる動きは別 Issue (cdl#552 で対象外)
|
|
196
|
+
return <ChartWaffleNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
197
|
+
case "chart-stat":
|
|
198
|
+
// 数値が主役で、弧は輪郭だけの補助。 段の進みで数値が動く演出は既存の tween が担う
|
|
199
|
+
return <ChartStatNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
200
|
+
case "chart-radial":
|
|
201
|
+
// 段の進みで弧が伸びる動きは持たない (cdl#550 で対象外にした)
|
|
202
|
+
return <ChartRadialNode node={resolvedNode} active={active} stateValues={stateValues} />;
|
|
182
203
|
case "chart-bar":
|
|
183
204
|
return (
|
|
184
205
|
<ChartBarNode
|
|
@@ -339,6 +360,10 @@ export function CdlNodeView({
|
|
|
339
360
|
return <ShapeTraderNode node={resolvedNode} active={active} />;
|
|
340
361
|
case "shape-customer-service":
|
|
341
362
|
return <ShapeCustomerServiceNode node={resolvedNode} active={active} />;
|
|
363
|
+
case "mark-start":
|
|
364
|
+
return <MarkStartNode node={resolvedNode} active={active} />;
|
|
365
|
+
case "mark-end":
|
|
366
|
+
return <MarkEndNode node={resolvedNode} active={active} />;
|
|
342
367
|
default:
|
|
343
368
|
// 新 NodeKind 25 種は GenericNode で汎用描画 (kind 別 icon / color / shape)
|
|
344
369
|
return <GenericNode node={resolvedNode} active={active} value={node.value} stateValues={stateValues} />;
|
|
@@ -44,7 +44,11 @@ export type BindingResult<T> = { value: T; ok: boolean };
|
|
|
44
44
|
/** 解決できなかった欄を持つ項目に付く印。 kind 側が DOM 属性に写す */
|
|
45
45
|
export type UnresolvedMark = { unresolved?: true };
|
|
46
46
|
|
|
47
|
-
export type ResolvedChartDatum = Omit<ChartDatumPayload, "value"> & {
|
|
47
|
+
export type ResolvedChartDatum = Omit<ChartDatumPayload, "value" | "previous"> & {
|
|
48
|
+
value: number;
|
|
49
|
+
/** 前の時点の値。 書かれていない datum では `undefined` のまま */
|
|
50
|
+
previous?: number;
|
|
51
|
+
} & UnresolvedMark;
|
|
48
52
|
export type ResolvedFunnelStage = Omit<FunnelStagePayload, "count"> & { count: number } & UnresolvedMark;
|
|
49
53
|
export type ResolvedGanttTask = Omit<GanttTaskPayload, "startIdx" | "endIdx"> & {
|
|
50
54
|
startIdx: number;
|
|
@@ -139,10 +143,17 @@ export function resolveChartData(
|
|
|
139
143
|
): ResolvedChartDatum[] {
|
|
140
144
|
// 全ての値が有限な数なら配列を組み直さない。 状態を読まない図が毎 frame で項目数ぶんの
|
|
141
145
|
// 割当てを払わないようにするための近道で、 同じ配列をそのまま返すので挙動も変わらない。
|
|
142
|
-
|
|
146
|
+
//
|
|
147
|
+
// **前の時点の値も同じ判定に入れる** (cdl#551)。 入れないと `{名前}` を書いた `previous` が
|
|
148
|
+
// 近道を素通りし、状態を読まないまま文字列のまま描画へ流れる。
|
|
149
|
+
if (!data.some((d) => needsResolve(d.value) || (d.previous !== undefined && needsResolve(d.previous))))
|
|
150
|
+
return data as ResolvedChartDatum[];
|
|
143
151
|
return data.map((d) => {
|
|
144
152
|
const v = resolveBoundNumber(d.value, values, 0);
|
|
145
|
-
|
|
153
|
+
const 前 = d.previous === undefined ? undefined : resolveBoundNumber(d.previous, values, 0);
|
|
154
|
+
const 解けた = v.ok && (前 === undefined || 前.ok);
|
|
155
|
+
const 素 = { ...d, value: v.value, previous: 前?.value };
|
|
156
|
+
return 解けた ? 素 : { ...素, unresolved: true as const };
|
|
146
157
|
});
|
|
147
158
|
}
|
|
148
159
|
|