@cardenelabs/cdl 0.82.0 → 0.102.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 +874 -1
- package/dist/index.cjs +882 -187
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.js +882 -187
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +869 -178
- 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 +869 -178
- package/dist/react.js.map +1 -1
- package/dist/{render-DfXCs3Eh.d.cts → render-oEmwYN-q.d.cts} +21 -3
- package/dist/{render-DfXCs3Eh.d.ts → render-oEmwYN-q.d.ts} +21 -3
- package/package.json +1 -1
- package/src/kinds/chart-gauge.tsx +20 -3
- package/src/kinds/chart-legend-number.tsx +33 -0
- package/src/kinds/chart-number.ts +33 -0
- package/src/kinds/chart-radial.tsx +24 -7
- package/src/kinds/chart-stat.tsx +35 -5
- package/src/kinds/chart-waffle.tsx +25 -7
- package/src/kinds/dyn-shape.tsx +72 -12
- package/src/kinds/funnel.tsx +50 -2
- package/src/kinds/mind-map.tsx +206 -64
- package/src/kinds/quadrant.tsx +318 -45
- package/src/kinds/shape-blockchain.tsx +50 -18
- package/src/kinds/shape-commerce.tsx +27 -4
- package/src/kinds/shape-region.tsx +42 -1
- package/src/kinds/shape-software.tsx +51 -12
- package/src/kinds/storage.tsx +16 -8
- package/src/kinds/tree.tsx +46 -10
- package/src/lang.ts +5 -5
- package/src/layout/nodes.ts +3 -1
- package/src/layout/spec.ts +154 -25
- package/src/layout/text-width.ts +25 -0
- package/src/layout/viewbox.ts +32 -0
- package/src/presets.ts +24 -7
- package/src/render/nodes.tsx +77 -0
- package/src/render/payload-binding.ts +108 -25
- package/src/render/stage.tsx +24 -2
- package/src/types.ts +117 -38
- package/src/visual-validate.ts +61 -1
package/src/layout/text-width.ts
CHANGED
|
@@ -219,3 +219,28 @@ export function measureTextWidth(text: string, opts: MeasureOptions = {}): numbe
|
|
|
219
219
|
const w = advanceEmSum * fontSize + letterSpacing * charCount;
|
|
220
220
|
return Math.max(w, fontSize * 1.2);
|
|
221
221
|
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 与えた幅に収めるための字の倍率を出す。 収まっているなら 1 を返す。
|
|
225
|
+
*
|
|
226
|
+
* 字間 (`letterSpacing`) を渡さないのは、字間があると倍率が 1 回の割り算で決まらないため。
|
|
227
|
+
* `measureTextWidth` の戻りは字間を除けば `fontSize` に比例する (最低幅も `fontSize × 1.2`
|
|
228
|
+
* なので同じく比例する) ので、比を 1 度取れば厳密に収まる。 字間は文字数ぶんの定数として
|
|
229
|
+
* 足されるため比例が崩れ、縮めた後にもう一度測らないと収まったか判らない。
|
|
230
|
+
*
|
|
231
|
+
* 幅か字の大きさが 0 以下なら 1 を返す。 収める先が無い形で 0 を返すと字が消えて、
|
|
232
|
+
* 「収めた」 のか「描かなかった」 のかが読めなくなる。
|
|
233
|
+
*
|
|
234
|
+
* `kinds/shape-region.tsx` の `幅の倍率` が同じ形を持つが、あちらは字種の平均で測る
|
|
235
|
+
* `kinds/text-width.ts` の `textWidth` を使う。 絵の中の字は engine の外から大きさを
|
|
236
|
+
* 与えられるため、1 文字ごとの実測表より軽い見積りで足りる。 測り方が違うので別に置く。
|
|
237
|
+
*/
|
|
238
|
+
export function 収める倍率(
|
|
239
|
+
text: string,
|
|
240
|
+
opts: MeasureOptions & { fontSize: number },
|
|
241
|
+
幅: number,
|
|
242
|
+
): number {
|
|
243
|
+
if (!(幅 > 0) || !(opts.fontSize > 0)) return 1;
|
|
244
|
+
const 実幅 = measureTextWidth(text, { ...opts, letterSpacing: 0 });
|
|
245
|
+
return 実幅 > 幅 ? 幅 / 実幅 : 1;
|
|
246
|
+
}
|
package/src/layout/viewbox.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import type { BBox, CdlViewport, LaidEdge, LaidLane, LaidNode } from "../types";
|
|
2
|
+
import { isRenderedNode } from "./spec";
|
|
2
3
|
import { VIEW_PAD } from "./tokens";
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* その帯が箱いっぱいに絵を出すか (#919)。
|
|
7
|
+
*
|
|
8
|
+
* 出さない帯が描くのは当たりを取るための矩形だけで、`fill="none" stroke="none"` で
|
|
9
|
+
* 見えない (`render/stage.tsx` の「lane bbox を <g> の boundingBox に anchor するための
|
|
10
|
+
* invisible rect」)。 見えないものを枠に入れると、宣言した帯の幅ぶん枠が広がる。
|
|
11
|
+
*
|
|
12
|
+
* 名前 (`label`) は数えない。 名前は `bboxes` に `lane-label` として既に入っており、
|
|
13
|
+
* 帯の箱ではなく名前の大きさで枠が決まるのが正しい。
|
|
14
|
+
*/
|
|
15
|
+
function 箱いっぱいに描く(lane: LaidLane): boolean {
|
|
16
|
+
return lane.contain === true || lane.lifeline === true;
|
|
17
|
+
}
|
|
18
|
+
|
|
4
19
|
export function computeViewBox(
|
|
5
20
|
lanes: LaidLane[],
|
|
6
21
|
nodes: LaidNode[],
|
|
@@ -8,21 +23,38 @@ export function computeViewBox(
|
|
|
8
23
|
bboxes: BBox[],
|
|
9
24
|
viewport?: CdlViewport,
|
|
10
25
|
): { x: number; y: number; w: number; h: number } {
|
|
26
|
+
// 書いた時点で隠れると決まっている節は枠に入れない (#923)。
|
|
27
|
+
// 判定は `isRenderedNode` が唯一持つ = ここで書き直すと規則が 2 か所に分かれる。
|
|
28
|
+
// `{signal}` を含む条件は出る側に倒すので、段で出たり消えたりする節は今までどおり
|
|
29
|
+
// 枠に入る (外すと段を進めるたびに枠が動く)。
|
|
30
|
+
const 常に隠れる節 = new Set(nodes.filter((n) => !isRenderedNode(n)).map((n) => n.id));
|
|
11
31
|
let maxX = 0;
|
|
12
32
|
let maxY = 0;
|
|
13
33
|
let minX = Number.POSITIVE_INFINITY;
|
|
14
34
|
let minY = Number.POSITIVE_INFINITY;
|
|
15
35
|
for (const bbox of bboxes) {
|
|
36
|
+
// 節は外接矩形としても出てくる。 節そのものだけ外しても、こちらが枠を作る
|
|
37
|
+
if (bbox.kind === "node" && 常に隠れる節.has(bbox.id)) continue;
|
|
16
38
|
maxX = Math.max(maxX, bbox.x + bbox.w);
|
|
17
39
|
maxY = Math.max(maxY, bbox.y + bbox.h);
|
|
18
40
|
minX = Math.min(minX, bbox.x);
|
|
19
41
|
minY = Math.min(minY, bbox.y);
|
|
20
42
|
}
|
|
43
|
+
// **遠い側の端だけを渡さない** (#919)。 帯と節は右端と下端しか渡しておらず、
|
|
44
|
+
// 左端と上端は `bboxes` 任せだった。 帯が節より広い図では右だけ枠が伸び、
|
|
45
|
+
// 左には対応する余白が付かない = 絵が左へ寄る
|
|
46
|
+
// (見本帳の絵記号 117 枚すべてで右の余白が左より広く、差の中央は枠の幅の 12 ポイント)。
|
|
21
47
|
for (const lane of lanes) {
|
|
48
|
+
if (!箱いっぱいに描く(lane)) continue;
|
|
49
|
+
minX = Math.min(minX, lane.x);
|
|
50
|
+
minY = Math.min(minY, lane.y);
|
|
22
51
|
maxX = Math.max(maxX, lane.x + lane.width);
|
|
23
52
|
maxY = Math.max(maxY, lane.y + lane.height);
|
|
24
53
|
}
|
|
25
54
|
for (const n of nodes) {
|
|
55
|
+
if (常に隠れる節.has(n.id)) continue;
|
|
56
|
+
minX = Math.min(minX, n.cx - n.w / 2);
|
|
57
|
+
minY = Math.min(minY, n.cy - n.h / 2);
|
|
26
58
|
maxX = Math.max(maxX, n.cx + n.w / 2);
|
|
27
59
|
maxY = Math.max(maxY, n.cy + n.h / 2);
|
|
28
60
|
}
|
package/src/presets.ts
CHANGED
|
@@ -1621,6 +1621,8 @@ export type TreeNode = {
|
|
|
1621
1621
|
export type TreePreset = {
|
|
1622
1622
|
id: string;
|
|
1623
1623
|
topic: string;
|
|
1624
|
+
/** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
|
|
1625
|
+
eyebrow?: string;
|
|
1624
1626
|
defaultTone?: Tone;
|
|
1625
1627
|
/** depth ごとの lane width (default 320) */
|
|
1626
1628
|
nodeWidth?: number;
|
|
@@ -1663,7 +1665,7 @@ export function tree(preset: TreePreset): TreeBuilder {
|
|
|
1663
1665
|
stack: 0,
|
|
1664
1666
|
kind: "tree-hierarchy",
|
|
1665
1667
|
title: preset.topic,
|
|
1666
|
-
eyebrow:
|
|
1668
|
+
...(preset.eyebrow ? { eyebrow: preset.eyebrow } : {}),
|
|
1667
1669
|
w: canvasW,
|
|
1668
1670
|
h: canvasH,
|
|
1669
1671
|
treeData: treeNodes.map((n) => ({
|
|
@@ -1705,6 +1707,8 @@ export type JourneyStep = {
|
|
|
1705
1707
|
export type UserJourneyPreset = {
|
|
1706
1708
|
id: string;
|
|
1707
1709
|
topic: string;
|
|
1710
|
+
/** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
|
|
1711
|
+
eyebrow?: string;
|
|
1708
1712
|
defaultTone?: Tone;
|
|
1709
1713
|
stepWidth?: number;
|
|
1710
1714
|
};
|
|
@@ -1745,7 +1749,7 @@ export function userJourney(preset: UserJourneyPreset): UserJourneyBuilder {
|
|
|
1745
1749
|
stack: 0,
|
|
1746
1750
|
kind: "journey-map",
|
|
1747
1751
|
title: preset.topic,
|
|
1748
|
-
eyebrow:
|
|
1752
|
+
...(preset.eyebrow ? { eyebrow: preset.eyebrow } : {}),
|
|
1749
1753
|
w: canvasW,
|
|
1750
1754
|
h: canvasH,
|
|
1751
1755
|
journeyData: steps.map((s) => ({
|
|
@@ -1783,6 +1787,8 @@ export type MindBranch = {
|
|
|
1783
1787
|
export type MindMapPreset = {
|
|
1784
1788
|
id: string;
|
|
1785
1789
|
topic: string;
|
|
1790
|
+
/** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
|
|
1791
|
+
eyebrow?: string;
|
|
1786
1792
|
/** 中心 topic ID + label */
|
|
1787
1793
|
rootId: string;
|
|
1788
1794
|
rootTitle: string;
|
|
@@ -1831,7 +1837,7 @@ export function mindMap(preset: MindMapPreset): MindMapBuilder {
|
|
|
1831
1837
|
stack: 0,
|
|
1832
1838
|
kind: "mind-map",
|
|
1833
1839
|
title: preset.topic,
|
|
1834
|
-
eyebrow:
|
|
1840
|
+
...(preset.eyebrow ? { eyebrow: preset.eyebrow } : {}),
|
|
1835
1841
|
w: canvasW,
|
|
1836
1842
|
h: canvasH,
|
|
1837
1843
|
mindData: {
|
|
@@ -1873,6 +1879,8 @@ export type FunnelStage = {
|
|
|
1873
1879
|
export type FunnelPreset = {
|
|
1874
1880
|
id: string;
|
|
1875
1881
|
topic: string;
|
|
1882
|
+
/** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
|
|
1883
|
+
eyebrow?: string;
|
|
1876
1884
|
defaultTone?: Tone;
|
|
1877
1885
|
stageWidth?: number;
|
|
1878
1886
|
};
|
|
@@ -1913,7 +1921,7 @@ export function funnel(preset: FunnelPreset): FunnelBuilder {
|
|
|
1913
1921
|
stack: 0,
|
|
1914
1922
|
kind: "funnel-stages",
|
|
1915
1923
|
title: preset.topic,
|
|
1916
|
-
eyebrow:
|
|
1924
|
+
...(preset.eyebrow ? { eyebrow: preset.eyebrow } : {}),
|
|
1917
1925
|
w: canvasW,
|
|
1918
1926
|
h: canvasH,
|
|
1919
1927
|
funnelData: stages.map((s) => ({
|
|
@@ -1952,6 +1960,8 @@ export type QuadrantItem = {
|
|
|
1952
1960
|
export type QuadrantPreset = {
|
|
1953
1961
|
id: string;
|
|
1954
1962
|
topic: string;
|
|
1963
|
+
/** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
|
|
1964
|
+
eyebrow?: string;
|
|
1955
1965
|
/** x 軸 label (left / right) */
|
|
1956
1966
|
xAxis: { left: string; right: string };
|
|
1957
1967
|
/** y 軸 label (bottom / top) */
|
|
@@ -2006,7 +2016,7 @@ export function quadrant(preset: QuadrantPreset): QuadrantBuilder {
|
|
|
2006
2016
|
stack: 0,
|
|
2007
2017
|
kind: "quadrant-matrix",
|
|
2008
2018
|
title: preset.topic,
|
|
2009
|
-
eyebrow:
|
|
2019
|
+
...(preset.eyebrow ? { eyebrow: preset.eyebrow } : {}),
|
|
2010
2020
|
w: canvasW,
|
|
2011
2021
|
h: canvasH,
|
|
2012
2022
|
quadrantData: {
|
|
@@ -2060,6 +2070,8 @@ export type ChartDatum = {
|
|
|
2060
2070
|
export type ChartPreset = {
|
|
2061
2071
|
id: string;
|
|
2062
2072
|
topic: string;
|
|
2073
|
+
/** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
|
|
2074
|
+
eyebrow?: string;
|
|
2063
2075
|
type: ChartType;
|
|
2064
2076
|
defaultTone?: Tone;
|
|
2065
2077
|
itemWidth?: number;
|
|
@@ -2127,7 +2139,7 @@ export function chart(preset: ChartPreset): ChartBuilder {
|
|
|
2127
2139
|
stack: 0,
|
|
2128
2140
|
kind: kindMap[preset.type],
|
|
2129
2141
|
title: preset.topic,
|
|
2130
|
-
eyebrow: preset.
|
|
2142
|
+
...(preset.eyebrow ? { eyebrow: preset.eyebrow } : {}),
|
|
2131
2143
|
w: canvasW,
|
|
2132
2144
|
h: canvasH,
|
|
2133
2145
|
chartData: data.map((d) => ({
|
|
@@ -2173,6 +2185,8 @@ export type GanttTask = {
|
|
|
2173
2185
|
export type GanttPreset = {
|
|
2174
2186
|
id: string;
|
|
2175
2187
|
topic: string;
|
|
2188
|
+
/** 図の題の上に出す小見出し (#939)。 書かなければ何も出ない */
|
|
2189
|
+
eyebrow?: string;
|
|
2176
2190
|
defaultTone?: Tone;
|
|
2177
2191
|
laneWidth?: number;
|
|
2178
2192
|
/**
|
|
@@ -2240,7 +2254,7 @@ export function gantt(preset: GanttPreset): GanttBuilder {
|
|
|
2240
2254
|
stack: 0,
|
|
2241
2255
|
kind: "gantt-timeline",
|
|
2242
2256
|
title: preset.topic,
|
|
2243
|
-
eyebrow:
|
|
2257
|
+
...(preset.eyebrow ? { eyebrow: preset.eyebrow } : {}),
|
|
2244
2258
|
w: canvasW,
|
|
2245
2259
|
h: canvasH,
|
|
2246
2260
|
// 宣言していない図には欄自体を載せない = 自動で決める経路をそのまま通す
|
|
@@ -2687,6 +2701,9 @@ export function stateMachine2(preset: StateMachine2Preset): StateMachine2Builder
|
|
|
2687
2701
|
...(subParts.length ? { sub: subParts.join(" ") } : {}),
|
|
2688
2702
|
tone: t.tone ?? tone,
|
|
2689
2703
|
style: "solid",
|
|
2704
|
+
// 遷移の印は 1 種類だけ = 実線に開いた矢 (#578)。 stateMachine と同じ値を渡す
|
|
2705
|
+
// (渡さないと EDGE_HEAD_DEFAULT の三角に落ち、入口を選び直しただけで印が変わる)
|
|
2706
|
+
head: "open",
|
|
2690
2707
|
});
|
|
2691
2708
|
phaseActivate.push(edgeId);
|
|
2692
2709
|
return api;
|
package/src/render/nodes.tsx
CHANGED
|
@@ -89,6 +89,8 @@ import {
|
|
|
89
89
|
import type { LaidNode } from "../types";
|
|
90
90
|
import type { CdlLang } from "../lang";
|
|
91
91
|
import { hasUnresolvedRef, interpolate, resolveNumericAttr } from "./utils";
|
|
92
|
+
import { drawsFigureTitle } from "../layout/spec";
|
|
93
|
+
import { textWidth } from "../kinds/text-width";
|
|
92
94
|
import type { 値の名前の表 } from "../input-option";
|
|
93
95
|
|
|
94
96
|
/**
|
|
@@ -527,6 +529,81 @@ export function CdlNodeView({
|
|
|
527
529
|
pointerEvents="none"
|
|
528
530
|
/>
|
|
529
531
|
{inner}
|
|
532
|
+
{箱の上の語(resolvedNode)}
|
|
530
533
|
</g>
|
|
531
534
|
);
|
|
532
535
|
}
|
|
536
|
+
|
|
537
|
+
/** 図の名前の下端から箱の上端までの間 (world unit)。 `dyn-*` の名前と同じ高さに並ぶ (#925) */
|
|
538
|
+
const FIGURE_TITLE_GAP = 10;
|
|
539
|
+
/** 図の名前の字の大きさ。 節の名前 (13) より 1 段大きく、図の見出しとして読める */
|
|
540
|
+
const FIGURE_TITLE_SIZE = 15;
|
|
541
|
+
|
|
542
|
+
/** 図の小見出しの字の大きさ。 題 (15) より小さくして、読む順を題から先にする (#935) */
|
|
543
|
+
const FIGURE_EYEBROW_SIZE = 11;
|
|
544
|
+
/** 小見出しの baseline から題の baseline までの間。 題の字の大きさに対する倍率 (#935) */
|
|
545
|
+
const FIGURE_EYEBROW_LIFT = 1.1;
|
|
546
|
+
|
|
547
|
+
/** 箱の幅に収める倍率。 収めないと長い語が箱の外へ出る */
|
|
548
|
+
function 幅に収めた大きさ(語: string, 素の大きさ: number, 幅: number): number {
|
|
549
|
+
const 実幅 = textWidth(語, 素の大きさ, 0);
|
|
550
|
+
return 実幅 > 幅 ? (素の大きさ * 幅) / 実幅 : 素の大きさ;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* 図表を自前で描く種別の題と小見出しを、箱の上に描く (#929 / #935)。
|
|
555
|
+
*
|
|
556
|
+
* これらの箱は図表の中身で埋まっており、中に語の場所が無い。
|
|
557
|
+
* 枠の余白 (`layout/tokens.ts` の `view_pad` = 60) の中に収まるので、枠は伸びない。
|
|
558
|
+
*
|
|
559
|
+
* **ここ 1 か所で描く**。 各種別の描画に散らすと、新しい図表の種別が増えた時に落ちる。
|
|
560
|
+
*
|
|
561
|
+
* **書かない語には要素を 1 つも足さない**。 空の `<text>` を置くと、この種別を使う
|
|
562
|
+
* 既存の図の焼き付けが理由なく落ちる。
|
|
563
|
+
*
|
|
564
|
+
* 小見出しの位置は **題の字の大きさから出す**。 素の大きさ (15) から出すと、長い題で
|
|
565
|
+
* 題が縮んだ時に 2 行の間が開く。
|
|
566
|
+
*
|
|
567
|
+
* 題を書かず小見出しだけを書いた図では、小見出しを題の位置に置く。 題の有無に紐付けて
|
|
568
|
+
* 消すと、題を書かない図で小見出しが出なくなる。
|
|
569
|
+
*/
|
|
570
|
+
function 箱の上の語(node: LaidNode): JSX.Element | null {
|
|
571
|
+
if (!drawsFigureTitle(node.kind)) return null;
|
|
572
|
+
const 名前 = (node.title ?? "").trim();
|
|
573
|
+
const 小見出し = (node.eyebrow ?? "").trim();
|
|
574
|
+
if (名前 === "" && 小見出し === "") return null;
|
|
575
|
+
const 題のy = node.cy - node.h / 2 - FIGURE_TITLE_GAP;
|
|
576
|
+
const 題の大きさ = 幅に収めた大きさ(名前, FIGURE_TITLE_SIZE, node.w);
|
|
577
|
+
const 小見出しのy = 名前 === "" ? 題のy : 題のy - 題の大きさ * FIGURE_EYEBROW_LIFT;
|
|
578
|
+
return (
|
|
579
|
+
<>
|
|
580
|
+
{小見出し !== "" && (
|
|
581
|
+
<text
|
|
582
|
+
data-cdl-role="figure-eyebrow"
|
|
583
|
+
x={node.cx}
|
|
584
|
+
y={小見出しのy}
|
|
585
|
+
textAnchor="middle"
|
|
586
|
+
fontSize={幅に収めた大きさ(小見出し, FIGURE_EYEBROW_SIZE, node.w)}
|
|
587
|
+
fontWeight={600}
|
|
588
|
+
letterSpacing={0.6}
|
|
589
|
+
fill="var(--cdl-text-dim, #5a6270)"
|
|
590
|
+
>
|
|
591
|
+
{小見出し}
|
|
592
|
+
</text>
|
|
593
|
+
)}
|
|
594
|
+
{名前 !== "" && (
|
|
595
|
+
<text
|
|
596
|
+
data-cdl-role="figure-title"
|
|
597
|
+
x={node.cx}
|
|
598
|
+
y={題のy}
|
|
599
|
+
textAnchor="middle"
|
|
600
|
+
fontSize={題の大きさ}
|
|
601
|
+
fontWeight={600}
|
|
602
|
+
fill="var(--cdl-text, #23201b)"
|
|
603
|
+
>
|
|
604
|
+
{名前}
|
|
605
|
+
</text>
|
|
606
|
+
)}
|
|
607
|
+
</>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
@@ -27,14 +27,15 @@ import { hasUnresolvedRef, interpolate } from "./utils";
|
|
|
27
27
|
* (spec § 4.2 が輪について同じ判断をしている = 1 箇所の壊れで図が消えると原因が分からない)。
|
|
28
28
|
* 印は各 kind が `data-cdl-unresolved="true"` として DOM に出す。
|
|
29
29
|
*
|
|
30
|
-
* `mind-map` / `tree-hierarchy` の 2 種は
|
|
31
|
-
*
|
|
30
|
+
* `mind-map` / `tree-hierarchy` の 2 種は **数を持たない** ため名前と補足だけを解決する
|
|
31
|
+
* (#467 / #471)。 この 2 種が持つのは数ではなく構造と語で、動かせるのは語の側だけ。
|
|
32
32
|
*
|
|
33
|
-
* 解決するのは **描かれる欄だけ** にする。
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* 解決するのは **描かれる欄だけ** にする。 描かない欄を解決すると、画面は何も変わらないのに
|
|
34
|
+
* 解決できない時だけ印が付く = 印の意味が読めない。 逆に描くようになった欄を解決しないと、
|
|
35
|
+
* 図に `{名前}` が素のまま出る。 したがって描く欄を足す時は、同じ変更で解決の側も足す
|
|
36
|
+
* (#931 で `funnel-stages` と `quadrant-matrix` の補足がこの形になった)。
|
|
36
37
|
*
|
|
37
|
-
* 親子の繋がり (`parent`)
|
|
38
|
+
* 親子の繋がり (`parent`) は解決しない = 状態で繋ぎ替えると木の形そのものが変わり、
|
|
38
39
|
* 配置の計算と印の付け方が別問題になる。
|
|
39
40
|
*/
|
|
40
41
|
|
|
@@ -49,18 +50,24 @@ export type ResolvedChartDatum = Omit<ChartDatumPayload, "value" | "previous"> &
|
|
|
49
50
|
/** 前の時点の値。 書かれていない datum では `undefined` のまま */
|
|
50
51
|
previous?: number;
|
|
51
52
|
} & UnresolvedMark;
|
|
52
|
-
export type ResolvedFunnelStage = Omit<FunnelStagePayload, "count"> & {
|
|
53
|
+
export type ResolvedFunnelStage = Omit<FunnelStagePayload, "count"> & {
|
|
54
|
+
count: number;
|
|
55
|
+
} & UnresolvedMark;
|
|
53
56
|
export type ResolvedGanttTask = Omit<GanttTaskPayload, "startIdx" | "endIdx"> & {
|
|
54
57
|
startIdx: number;
|
|
55
58
|
endIdx: number;
|
|
56
59
|
} & UnresolvedMark;
|
|
57
|
-
export type ResolvedQuadrantItem = Omit<QuadrantItemPayload, "quadrant"> & {
|
|
60
|
+
export type ResolvedQuadrantItem = Omit<QuadrantItemPayload, "quadrant" | "at"> & {
|
|
58
61
|
quadrant: QuadrantKey;
|
|
62
|
+
/** 描画域の中の位置。 `at` を書いた項目だけが持つ。 0 から 1 に丸めてある */
|
|
63
|
+
at?: { x: number; y: number };
|
|
59
64
|
} & UnresolvedMark;
|
|
60
65
|
export type ResolvedJourneyStep = Omit<JourneyStepPayload, "emotion"> & {
|
|
61
66
|
emotion: JourneyEmotion;
|
|
62
67
|
} & UnresolvedMark;
|
|
63
|
-
export type ResolvedQuadrantData = Omit<QuadrantPayload, "items"> & {
|
|
68
|
+
export type ResolvedQuadrantData = Omit<QuadrantPayload, "items"> & {
|
|
69
|
+
items: ResolvedQuadrantItem[];
|
|
70
|
+
};
|
|
64
71
|
|
|
65
72
|
export const QUADRANT_KEYS: readonly QuadrantKey[] = [
|
|
66
73
|
"topLeft",
|
|
@@ -146,7 +153,11 @@ export function resolveChartData(
|
|
|
146
153
|
//
|
|
147
154
|
// **前の時点の値も同じ判定に入れる** (cdl#551)。 入れないと `{名前}` を書いた `previous` が
|
|
148
155
|
// 近道を素通りし、状態を読まないまま文字列のまま描画へ流れる。
|
|
149
|
-
if (
|
|
156
|
+
if (
|
|
157
|
+
!data.some(
|
|
158
|
+
(d) => needsResolve(d.value) || (d.previous !== undefined && needsResolve(d.previous)),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
150
161
|
return data as ResolvedChartDatum[];
|
|
151
162
|
return data.map((d) => {
|
|
152
163
|
const v = resolveBoundNumber(d.value, values, 0);
|
|
@@ -157,14 +168,29 @@ export function resolveChartData(
|
|
|
157
168
|
});
|
|
158
169
|
}
|
|
159
170
|
|
|
171
|
+
/**
|
|
172
|
+
* 段の件数と補足を解決する。
|
|
173
|
+
*
|
|
174
|
+
* 補足を入れたのは #931 で **補足を描くようにしたため**。 近道の判定にも入れる = 件数だけを
|
|
175
|
+
* 見ると、件数を直に書いた図で `{名前}` を書いた補足が素通りして図に波括弧が出る。
|
|
176
|
+
*/
|
|
160
177
|
export function resolveFunnelData(
|
|
161
178
|
stages: FunnelStagePayload[],
|
|
162
179
|
values: Record<string, string>,
|
|
163
180
|
): ResolvedFunnelStage[] {
|
|
164
|
-
|
|
181
|
+
const 読む = stages.some(
|
|
182
|
+
(s) => needsResolve(s.count) || (s.subtitle?.includes("{") ?? false),
|
|
183
|
+
);
|
|
184
|
+
if (!読む) return stages as ResolvedFunnelStage[];
|
|
165
185
|
return stages.map((s) => {
|
|
166
186
|
const c = resolveBoundNumber(s.count, values, 0);
|
|
167
|
-
|
|
187
|
+
const sub = s.subtitle === undefined ? undefined : resolveBoundText(s.subtitle, values);
|
|
188
|
+
const 素: ResolvedFunnelStage = {
|
|
189
|
+
...s,
|
|
190
|
+
count: c.value,
|
|
191
|
+
...(sub ? { subtitle: sub.value } : {}),
|
|
192
|
+
};
|
|
193
|
+
return c.ok && (sub?.ok ?? true) ? 素 : { ...素, unresolved: true as const };
|
|
168
194
|
});
|
|
169
195
|
}
|
|
170
196
|
|
|
@@ -172,7 +198,8 @@ export function resolveGanttData(
|
|
|
172
198
|
tasks: GanttTaskPayload[],
|
|
173
199
|
values: Record<string, string>,
|
|
174
200
|
): ResolvedGanttTask[] {
|
|
175
|
-
if (!tasks.some((t) => needsResolve(t.startIdx) || needsResolve(t.endIdx)))
|
|
201
|
+
if (!tasks.some((t) => needsResolve(t.startIdx) || needsResolve(t.endIdx)))
|
|
202
|
+
return tasks as ResolvedGanttTask[];
|
|
176
203
|
return tasks.map((t) => {
|
|
177
204
|
const start = resolveBoundNumber(t.startIdx, values, 0);
|
|
178
205
|
// 終わりの既定値は始まりに合わせて 1 目盛りぶんの帯にする。 0 に落とすと、 横軸の長さが
|
|
@@ -184,21 +211,75 @@ export function resolveGanttData(
|
|
|
184
211
|
});
|
|
185
212
|
}
|
|
186
213
|
|
|
214
|
+
/**
|
|
215
|
+
* 座標から象限を決める。 中央 (0.5) は右上側に入れる。
|
|
216
|
+
*
|
|
217
|
+
* 境目をどちらかに倒すこと自体は避けられないため、 上下左右で同じ向き (大きい側) に倒す。
|
|
218
|
+
* 軸ごとに向きを変えると、 ちょうど中央に置いた 2 項目が別の象限に入る。
|
|
219
|
+
*/
|
|
220
|
+
export function 座標の象限(x: number, y: number): QuadrantKey {
|
|
221
|
+
const 右 = x >= 0.5;
|
|
222
|
+
const 上 = y >= 0.5;
|
|
223
|
+
if (上) return 右 ? "topRight" : "topLeft";
|
|
224
|
+
return 右 ? "bottomRight" : "bottomLeft";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 描画域の外に出た値を中に戻す。 外に置くと図の枠を越えて描かれる */
|
|
228
|
+
const 域に収める = (v: number): number => Math.min(1, Math.max(0, v));
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 項目の象限と座標と補足を解決する。
|
|
232
|
+
*
|
|
233
|
+
* 補足を入れたのは #931 で **補足を描くようにしたため**。 近道の判定にも入れる = 象限を
|
|
234
|
+
* 決まった語で書いた図では、`{名前}` を書いた補足が素通りして図に波括弧が出る。
|
|
235
|
+
*/
|
|
187
236
|
export function resolveQuadrantData(
|
|
188
237
|
data: QuadrantPayload,
|
|
189
238
|
values: Record<string, string>,
|
|
190
239
|
): ResolvedQuadrantData {
|
|
191
240
|
const items = data.items;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
241
|
+
// 座標を持つ項目が 1 つも無く、象限が全て決まった語で、補足も状態を読まないなら組み直さない
|
|
242
|
+
// (他の resolver と同じ近道)
|
|
243
|
+
const 近道 =
|
|
244
|
+
!items.some((i) => i.at !== undefined) &&
|
|
245
|
+
!items.some((i) => i.subtitle?.includes("{") ?? false) &&
|
|
246
|
+
items.every((i) => (QUADRANT_KEYS as readonly string[]).includes(i.quadrant ?? ""));
|
|
247
|
+
if (近道) return data as ResolvedQuadrantData;
|
|
195
248
|
return {
|
|
196
249
|
...data,
|
|
197
250
|
items: items.map((i) => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
251
|
+
// 座標を書いた項目は座標だけを読む。 象限は座標から決まるので `quadrant` は見ない
|
|
252
|
+
if (i.at !== undefined) {
|
|
253
|
+
const x = resolveBoundNumber(i.at.x, values, 0.5);
|
|
254
|
+
const y = resolveBoundNumber(i.at.y, values, 0.5);
|
|
255
|
+
const 置く = { x: 域に収める(x.value), y: 域に収める(y.value) };
|
|
256
|
+
const sub = i.subtitle === undefined ? undefined : resolveBoundText(i.subtitle, values);
|
|
257
|
+
const 素: ResolvedQuadrantItem = {
|
|
258
|
+
id: i.id,
|
|
259
|
+
title: i.title,
|
|
260
|
+
at: 置く,
|
|
261
|
+
quadrant: 座標の象限(置く.x, 置く.y),
|
|
262
|
+
...(sub ? { subtitle: sub.value } : {}),
|
|
263
|
+
};
|
|
264
|
+
// 丸めは「読めなかった」 ではない。 値は在るので印を付けない
|
|
265
|
+
return x.ok && y.ok && (sub?.ok ?? true) ? 素 : { ...素, unresolved: true as const };
|
|
266
|
+
}
|
|
267
|
+
// 座標も象限も無い項目は消さず既定の象限に置く。 消すと書いた項目が図から消える
|
|
268
|
+
const q = resolveBoundEnum<QuadrantKey>(
|
|
269
|
+
i.quadrant ?? "",
|
|
270
|
+
values,
|
|
271
|
+
QUADRANT_KEYS,
|
|
272
|
+
QUADRANT_FALLBACK,
|
|
273
|
+
);
|
|
274
|
+
const sub = i.subtitle === undefined ? undefined : resolveBoundText(i.subtitle, values);
|
|
275
|
+
// 座標の欄は組み直した側の型 (数の組) を持つ。 この枝では空なので渡さない
|
|
276
|
+
const 素: ResolvedQuadrantItem = {
|
|
277
|
+
id: i.id,
|
|
278
|
+
title: i.title,
|
|
279
|
+
quadrant: q.value,
|
|
280
|
+
...(sub ? { subtitle: sub.value } : {}),
|
|
281
|
+
};
|
|
282
|
+
return q.ok && (sub?.ok ?? true) ? 素 : { ...素, unresolved: true as const };
|
|
202
283
|
}),
|
|
203
284
|
};
|
|
204
285
|
}
|
|
@@ -212,7 +293,9 @@ export function resolveJourneyData(
|
|
|
212
293
|
}
|
|
213
294
|
return steps.map((s) => {
|
|
214
295
|
const e = resolveBoundEnum(s.emotion, values, JOURNEY_EMOTIONS, EMOTION_FALLBACK);
|
|
215
|
-
return e.ok
|
|
296
|
+
return e.ok
|
|
297
|
+
? { ...s, emotion: e.value }
|
|
298
|
+
: { ...s, emotion: e.value, unresolved: true as const };
|
|
216
299
|
});
|
|
217
300
|
}
|
|
218
301
|
|
|
@@ -234,7 +317,9 @@ function resolveBoundText(raw: string, values: Record<string, string>): BindingR
|
|
|
234
317
|
// 状態を読まない名前は組み直さない (毎 frame の割当てを避ける近道、 他の resolver と同じ)
|
|
235
318
|
if (!raw.includes("{")) return { value: raw, ok: true };
|
|
236
319
|
const resolved = interpolate(raw, values);
|
|
237
|
-
return hasUnresolvedRef(resolved)
|
|
320
|
+
return hasUnresolvedRef(resolved)
|
|
321
|
+
? { value: resolved, ok: false }
|
|
322
|
+
: { value: resolved, ok: true };
|
|
238
323
|
}
|
|
239
324
|
|
|
240
325
|
/**
|
|
@@ -296,7 +381,5 @@ export function resolveMindData(
|
|
|
296
381
|
...(rootSubtitle ? { rootSubtitle: rootSubtitle.value } : {}),
|
|
297
382
|
branches,
|
|
298
383
|
};
|
|
299
|
-
return root.ok && (rootSubtitle?.ok ?? true)
|
|
300
|
-
? 解決後
|
|
301
|
-
: { ...解決後, unresolved: true as const };
|
|
384
|
+
return root.ok && (rootSubtitle?.ok ?? true) ? 解決後 : { ...解決後, unresolved: true as const };
|
|
302
385
|
}
|
package/src/render/stage.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { 折れ線の目盛, type ChartLineScale } from "../kinds/chart-line";
|
|
|
3
3
|
import { 描く進みに直す } from "../kinds/draw-ratio";
|
|
4
4
|
import { useReducedMotion } from "../anim/react/useReducedMotion";
|
|
5
5
|
import { lifelineStartYs, uniformLifelineEndY } from "../layout/lifeline";
|
|
6
|
-
import { buildDiagramAltText } from "../layout/spec";
|
|
6
|
+
import { buildDiagramAltText, isRenderedNode } from "../layout/spec";
|
|
7
7
|
import {
|
|
8
8
|
EDGE_HEADS,
|
|
9
9
|
EDGE_HEAD_FILLS,
|
|
@@ -28,6 +28,24 @@ import { TONE, TONE_HEX } from "./tone";
|
|
|
28
28
|
import { computeStateValues } from "./utils";
|
|
29
29
|
import type { 値の名前の表 } from "../input-option";
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* その図に描くものがあるか (#923)。
|
|
33
|
+
*
|
|
34
|
+
* この `<svg>` が出すのは帯と線と節の 3 つだけで、残りは `defs` の矢頭 (見えない) になる。
|
|
35
|
+
* 3 つとも無い図は面だけを取って何も出さない = 見本帳では `874×874` の空白になっていた。
|
|
36
|
+
*
|
|
37
|
+
* 帯は `contain` (囲い) / `lifeline` (縦線) / `label` (名前) のいずれかを持つ時だけ絵を出す。
|
|
38
|
+
* 持たない帯が描くのは当たりを取るための `fill="none" stroke="none"` の矩形で、見えない。
|
|
39
|
+
*
|
|
40
|
+
* 節は `isRenderedNode` で見る。 書いた時点で隠れると決まっている節 (`visibleIf: "0"` 等) は
|
|
41
|
+
* 描かれない。 `{signal}` を含む条件は段しだいで出るので、出る側に数える。
|
|
42
|
+
*/
|
|
43
|
+
function 描くものがある(laid: LaidDiagram): boolean {
|
|
44
|
+
if (laid.edges.length > 0) return true;
|
|
45
|
+
if (laid.nodes.some((n) => isRenderedNode(n))) return true;
|
|
46
|
+
return laid.lanes.some((l) => l.contain === true || l.lifeline === true || Boolean(l.label));
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
export function CdlStage({
|
|
32
50
|
laid,
|
|
33
51
|
currentPhase,
|
|
@@ -56,7 +74,7 @@ export function CdlStage({
|
|
|
56
74
|
compact?: boolean;
|
|
57
75
|
/** interactive integration (CAR #246) 用 SVG ref、 caller が event handler attach に使う */
|
|
58
76
|
svgRef?: React.RefObject<SVGSVGElement | null>;
|
|
59
|
-
}): JSX.Element {
|
|
77
|
+
}): JSX.Element | null {
|
|
60
78
|
/**
|
|
61
79
|
* 触れている / 留めている箱 (#806)。
|
|
62
80
|
*
|
|
@@ -262,6 +280,10 @@ export function CdlStage({
|
|
|
262
280
|
);
|
|
263
281
|
}, [laid]);
|
|
264
282
|
const diagramScale = laid.diagramScale ?? 1;
|
|
283
|
+
// 描くものが 1 つも無い図は面を取らない (#923)。
|
|
284
|
+
// 札 (`interactive-panel`) は `render.tsx` が別に出すので、題と札は残る。
|
|
285
|
+
// hook は全て上で呼び終えているので、ここで返しても呼ぶ順は変わらない。
|
|
286
|
+
if (!描くものがある(laid)) return null;
|
|
265
287
|
return (
|
|
266
288
|
<div className={compact ? "px-3 py-3" : "px-6 py-6"} style={compact ? undefined : { minHeight: 460 }}>
|
|
267
289
|
<svg
|