@formicoidea/labre-framework-wardley 0.23.0 → 0.23.2
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/package.json +2 -2
- package/src/element-renderer.ts +242 -242
- package/src/element-view.ts +143 -143
- package/src/gradient.ts +137 -137
- package/src/label-layout.ts +126 -126
- package/src/legend.ts +438 -438
- package/src/node/consts.ts +109 -109
- package/src/node/node-renderer.ts +142 -142
- package/src/node/node-view.ts +11 -11
- package/src/toolbar/config.ts +280 -280
- package/src/toolbar/node-config.ts +28 -28
- package/src/toolbar/senior-tool.ts +11 -11
- package/src/toolbar/wardley-menu.ts +552 -552
- package/src/toolbar/wardley-senior-button.ts +154 -154
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formicoidea/labre-framework-wardley",
|
|
3
3
|
"description": "Labre wardley framework for @formicoidea/labre-core.",
|
|
4
|
-
"version": "0.23.
|
|
4
|
+
"version": "0.23.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"author": "lajola",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"src"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@formicoidea/labre-core": "0.23.
|
|
21
|
+
"@formicoidea/labre-core": "0.23.2",
|
|
22
22
|
"lit": "^3.2.0"
|
|
23
23
|
}
|
|
24
24
|
}
|
package/src/element-renderer.ts
CHANGED
|
@@ -1,242 +1,242 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ElementRenderer,
|
|
3
|
-
ElementRendererExtension,
|
|
4
|
-
} from '@formicoidea/labre-core/blocks/surface';
|
|
5
|
-
import type { WardleyBackgroundElementModel } from '@formicoidea/labre-core/model';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
ARROW,
|
|
9
|
-
CARD_RADIUS,
|
|
10
|
-
COLORS,
|
|
11
|
-
EVOLUTION_BOUNDARIES,
|
|
12
|
-
FONT_FAMILY,
|
|
13
|
-
FONTS,
|
|
14
|
-
LINE,
|
|
15
|
-
MARGIN,
|
|
16
|
-
OFFSETS,
|
|
17
|
-
} from './consts';
|
|
18
|
-
import { BENEFIT_ZERO_FRAC, paintGradientBackground } from './gradient';
|
|
19
|
-
|
|
20
|
-
function roundRectPath(
|
|
21
|
-
ctx: CanvasRenderingContext2D,
|
|
22
|
-
x: number,
|
|
23
|
-
y: number,
|
|
24
|
-
w: number,
|
|
25
|
-
h: number,
|
|
26
|
-
r: number
|
|
27
|
-
) {
|
|
28
|
-
const rr = Math.min(r, w / 2, h / 2);
|
|
29
|
-
ctx.beginPath();
|
|
30
|
-
ctx.moveTo(x + rr, y);
|
|
31
|
-
ctx.arcTo(x + w, y, x + w, y + h, rr);
|
|
32
|
-
ctx.arcTo(x + w, y + h, x, y + h, rr);
|
|
33
|
-
ctx.arcTo(x, y + h, x, y, rr);
|
|
34
|
-
ctx.arcTo(x, y, x + w, y, rr);
|
|
35
|
-
ctx.closePath();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Canvas renderer for the Wardley map background — reproduces mockup C:
|
|
40
|
-
* an L-shaped axes frame (no top/right border), dashed evolution dividers and
|
|
41
|
-
* the symmetric axis labels.
|
|
42
|
-
*
|
|
43
|
-
* All sizes (fonts, margins, offsets, strokes) are FIXED model units — they do
|
|
44
|
-
* not scale with the element size. Only the plot interior scales.
|
|
45
|
-
*/
|
|
46
|
-
export const wardley: ElementRenderer<WardleyBackgroundElementModel> = (
|
|
47
|
-
model,
|
|
48
|
-
ctx,
|
|
49
|
-
matrix
|
|
50
|
-
) => {
|
|
51
|
-
const [, , w, h] = model.deserializedXYWH;
|
|
52
|
-
const cx = w / 2;
|
|
53
|
-
const cy = h / 2;
|
|
54
|
-
ctx.setTransform(
|
|
55
|
-
matrix.translateSelf(cx, cy).rotateSelf(model.rotate).translateSelf(-cx, -cy)
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
const px0 = MARGIN.left;
|
|
59
|
-
const px1 = w - MARGIN.right;
|
|
60
|
-
const py0 = MARGIN.top;
|
|
61
|
-
const py1 = h - MARGIN.bottom;
|
|
62
|
-
const pw = px1 - px0;
|
|
63
|
-
const ph = py1 - py0;
|
|
64
|
-
const ex = (r: number) => px0 + r * pw;
|
|
65
|
-
|
|
66
|
-
const line = (x1: number, y1: number, x2: number, y2: number) => {
|
|
67
|
-
ctx.beginPath();
|
|
68
|
-
ctx.moveTo(x1, y1);
|
|
69
|
-
ctx.lineTo(x2, y2);
|
|
70
|
-
ctx.stroke();
|
|
71
|
-
};
|
|
72
|
-
const vtext = (
|
|
73
|
-
text: string,
|
|
74
|
-
x: number,
|
|
75
|
-
y: number,
|
|
76
|
-
fontSize: number,
|
|
77
|
-
color: string
|
|
78
|
-
) => {
|
|
79
|
-
ctx.save();
|
|
80
|
-
ctx.translate(x, y);
|
|
81
|
-
ctx.rotate(-Math.PI / 2);
|
|
82
|
-
ctx.font = `${fontSize}px ${FONT_FAMILY}`;
|
|
83
|
-
ctx.fillStyle = color;
|
|
84
|
-
ctx.textAlign = 'center';
|
|
85
|
-
ctx.textBaseline = 'alphabetic';
|
|
86
|
-
ctx.fillText(text, 0, 0);
|
|
87
|
-
ctx.restore();
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// ── Card (element bounds) ───────────────────────────────────────────
|
|
91
|
-
const inset = LINE.card / 2;
|
|
92
|
-
roundRectPath(ctx, inset, inset, w - inset * 2, h - inset * 2, CARD_RADIUS);
|
|
93
|
-
ctx.fillStyle = COLORS.card;
|
|
94
|
-
ctx.fill();
|
|
95
|
-
ctx.strokeStyle = COLORS.cardBorder;
|
|
96
|
-
ctx.lineWidth = LINE.card;
|
|
97
|
-
ctx.stroke();
|
|
98
|
-
|
|
99
|
-
// ── Curve-driven gradient variants (inscribed in the frame) ─────────
|
|
100
|
-
// Hidden when `showGradient` is false → plain white background.
|
|
101
|
-
if (model.variant !== 'classic' && model.showGradient) {
|
|
102
|
-
paintGradientBackground(ctx, model.variant, px0, px1, py0, py1);
|
|
103
|
-
if (model.variant === 'benefit') {
|
|
104
|
-
const zy = py1 - BENEFIT_ZERO_FRAC * ph;
|
|
105
|
-
ctx.strokeStyle = COLORS.axis;
|
|
106
|
-
ctx.lineWidth = 1.2;
|
|
107
|
-
ctx.beginPath();
|
|
108
|
-
ctx.moveTo(px0, zy);
|
|
109
|
-
ctx.lineTo(px1, zy);
|
|
110
|
-
ctx.stroke();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ── Optional evolution band tints ───────────────────────────────────
|
|
115
|
-
if (model.banded) {
|
|
116
|
-
const starts = [0, 0.175, 0.4, 0.7];
|
|
117
|
-
const ends = [0.175, 0.4, 0.7, 1];
|
|
118
|
-
for (let i = 0; i < 4; i++) {
|
|
119
|
-
ctx.fillStyle = COLORS.band[i];
|
|
120
|
-
ctx.fillRect(ex(starts[i]), py0, ex(ends[i]) - ex(starts[i]), ph);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ── Evolution phase dividers (dashed) ───────────────────────────────
|
|
125
|
-
if (model.showColumnDividers) {
|
|
126
|
-
ctx.strokeStyle = COLORS.divider;
|
|
127
|
-
ctx.lineWidth = LINE.divider;
|
|
128
|
-
ctx.setLineDash([5, 5]);
|
|
129
|
-
for (const r of EVOLUTION_BOUNDARIES) {
|
|
130
|
-
line(ex(r), py0, ex(r), py1);
|
|
131
|
-
}
|
|
132
|
-
ctx.setLineDash([]);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ── Axes (L shape) + arrowheads ─────────────────────────────────────
|
|
136
|
-
// X and Y axes are independently toggleable. Each line stops at the base of
|
|
137
|
-
// its arrowhead (1px overlap, hidden under the triangle) so the line never
|
|
138
|
-
// pokes past the tip on zoom.
|
|
139
|
-
ctx.strokeStyle = COLORS.axis;
|
|
140
|
-
ctx.lineWidth = LINE.axis;
|
|
141
|
-
ctx.fillStyle = COLORS.axis;
|
|
142
|
-
if (model.showXAxis) {
|
|
143
|
-
line(px0, py1, px1 - ARROW + 1, py1); // X axis (arrow tip at px1)
|
|
144
|
-
ctx.beginPath(); // X arrow (points right)
|
|
145
|
-
ctx.moveTo(px1, py1);
|
|
146
|
-
ctx.lineTo(px1 - ARROW, py1 - ARROW / 2);
|
|
147
|
-
ctx.lineTo(px1 - ARROW, py1 + ARROW / 2);
|
|
148
|
-
ctx.closePath();
|
|
149
|
-
ctx.fill();
|
|
150
|
-
}
|
|
151
|
-
if (model.showYAxis) {
|
|
152
|
-
line(px0, py1, px0, py0 + ARROW - 1); // Y axis (arrow tip at py0)
|
|
153
|
-
ctx.beginPath(); // Y arrow (points up)
|
|
154
|
-
ctx.moveTo(px0, py0);
|
|
155
|
-
ctx.lineTo(px0 - ARROW / 2, py0 + ARROW);
|
|
156
|
-
ctx.lineTo(px0 + ARROW / 2, py0 + ARROW);
|
|
157
|
-
ctx.closePath();
|
|
158
|
-
ctx.fill();
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ── Horizontal labels ───────────────────────────────────────────────
|
|
162
|
-
ctx.textBaseline = 'alphabetic';
|
|
163
|
-
// Phase (column) labels (left-aligned at each zone start)
|
|
164
|
-
if (model.showColumnLabels) {
|
|
165
|
-
ctx.font = `${FONTS.phase}px ${FONT_FAMILY}`;
|
|
166
|
-
ctx.fillStyle = COLORS.label;
|
|
167
|
-
ctx.textAlign = 'left';
|
|
168
|
-
const phases: Array<[string, number]> = [
|
|
169
|
-
[model.phase0, 0],
|
|
170
|
-
[model.phase1, 0.175],
|
|
171
|
-
[model.phase2, 0.4],
|
|
172
|
-
[model.phase3, 0.7],
|
|
173
|
-
];
|
|
174
|
-
for (const [label, start] of phases) {
|
|
175
|
-
ctx.fillText(
|
|
176
|
-
label,
|
|
177
|
-
ex(start) + OFFSETS.phasePad,
|
|
178
|
-
py1 + OFFSETS.phaseBaseline
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// "Evolution" title near the X arrow (tied to the X axis)
|
|
183
|
-
if (model.showXAxis) {
|
|
184
|
-
ctx.font = `${FONTS.axis}px ${FONT_FAMILY}`;
|
|
185
|
-
ctx.fillStyle = COLORS.axis;
|
|
186
|
-
ctx.textAlign = 'right';
|
|
187
|
-
ctx.fillText(
|
|
188
|
-
model.xAxisTitle,
|
|
189
|
-
px1 - OFFSETS.evolutionPadRight,
|
|
190
|
-
py1 + OFFSETS.phaseBaseline
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
// Direction indicators (Uncharted / Industrialized, top corners)
|
|
194
|
-
if (model.showCornerLabels) {
|
|
195
|
-
ctx.font = `${FONTS.direction}px ${FONT_FAMILY}`;
|
|
196
|
-
ctx.fillStyle = COLORS.label;
|
|
197
|
-
ctx.textAlign = 'left';
|
|
198
|
-
ctx.fillText(
|
|
199
|
-
model.evolutionStart,
|
|
200
|
-
px0 + OFFSETS.directionPadLeft,
|
|
201
|
-
py0 + OFFSETS.directionTop
|
|
202
|
-
);
|
|
203
|
-
ctx.textAlign = 'right';
|
|
204
|
-
ctx.fillText(
|
|
205
|
-
model.evolutionEnd,
|
|
206
|
-
px1 - OFFSETS.directionPadRight,
|
|
207
|
-
py0 + OFFSETS.directionTop
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// ── Rotated Y labels (hugging the axis, symmetric with the X labels) ─
|
|
212
|
-
if (model.showYAxis) {
|
|
213
|
-
vtext(
|
|
214
|
-
model.yAxisTitle,
|
|
215
|
-
px0 - OFFSETS.yHug,
|
|
216
|
-
(py0 + py1) / 2,
|
|
217
|
-
FONTS.axis,
|
|
218
|
-
COLORS.axis
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
if (model.showVisibilityLabels) {
|
|
222
|
-
vtext(
|
|
223
|
-
model.visibilityHigh,
|
|
224
|
-
px0 - OFFSETS.yHug,
|
|
225
|
-
py0 + OFFSETS.visibleTop,
|
|
226
|
-
FONTS.visibility,
|
|
227
|
-
COLORS.label
|
|
228
|
-
);
|
|
229
|
-
vtext(
|
|
230
|
-
model.visibilityLow,
|
|
231
|
-
px0 - OFFSETS.yHug,
|
|
232
|
-
py1 - OFFSETS.invisibleBottom,
|
|
233
|
-
FONTS.visibility,
|
|
234
|
-
COLORS.label
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
export const WardleyElementRendererExtension = ElementRendererExtension(
|
|
240
|
-
'wardley',
|
|
241
|
-
wardley
|
|
242
|
-
);
|
|
1
|
+
import {
|
|
2
|
+
type ElementRenderer,
|
|
3
|
+
ElementRendererExtension,
|
|
4
|
+
} from '@formicoidea/labre-core/blocks/surface';
|
|
5
|
+
import type { WardleyBackgroundElementModel } from '@formicoidea/labre-core/model';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
ARROW,
|
|
9
|
+
CARD_RADIUS,
|
|
10
|
+
COLORS,
|
|
11
|
+
EVOLUTION_BOUNDARIES,
|
|
12
|
+
FONT_FAMILY,
|
|
13
|
+
FONTS,
|
|
14
|
+
LINE,
|
|
15
|
+
MARGIN,
|
|
16
|
+
OFFSETS,
|
|
17
|
+
} from './consts';
|
|
18
|
+
import { BENEFIT_ZERO_FRAC, paintGradientBackground } from './gradient';
|
|
19
|
+
|
|
20
|
+
function roundRectPath(
|
|
21
|
+
ctx: CanvasRenderingContext2D,
|
|
22
|
+
x: number,
|
|
23
|
+
y: number,
|
|
24
|
+
w: number,
|
|
25
|
+
h: number,
|
|
26
|
+
r: number
|
|
27
|
+
) {
|
|
28
|
+
const rr = Math.min(r, w / 2, h / 2);
|
|
29
|
+
ctx.beginPath();
|
|
30
|
+
ctx.moveTo(x + rr, y);
|
|
31
|
+
ctx.arcTo(x + w, y, x + w, y + h, rr);
|
|
32
|
+
ctx.arcTo(x + w, y + h, x, y + h, rr);
|
|
33
|
+
ctx.arcTo(x, y + h, x, y, rr);
|
|
34
|
+
ctx.arcTo(x, y, x + w, y, rr);
|
|
35
|
+
ctx.closePath();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Canvas renderer for the Wardley map background — reproduces mockup C:
|
|
40
|
+
* an L-shaped axes frame (no top/right border), dashed evolution dividers and
|
|
41
|
+
* the symmetric axis labels.
|
|
42
|
+
*
|
|
43
|
+
* All sizes (fonts, margins, offsets, strokes) are FIXED model units — they do
|
|
44
|
+
* not scale with the element size. Only the plot interior scales.
|
|
45
|
+
*/
|
|
46
|
+
export const wardley: ElementRenderer<WardleyBackgroundElementModel> = (
|
|
47
|
+
model,
|
|
48
|
+
ctx,
|
|
49
|
+
matrix
|
|
50
|
+
) => {
|
|
51
|
+
const [, , w, h] = model.deserializedXYWH;
|
|
52
|
+
const cx = w / 2;
|
|
53
|
+
const cy = h / 2;
|
|
54
|
+
ctx.setTransform(
|
|
55
|
+
matrix.translateSelf(cx, cy).rotateSelf(model.rotate).translateSelf(-cx, -cy)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const px0 = MARGIN.left;
|
|
59
|
+
const px1 = w - MARGIN.right;
|
|
60
|
+
const py0 = MARGIN.top;
|
|
61
|
+
const py1 = h - MARGIN.bottom;
|
|
62
|
+
const pw = px1 - px0;
|
|
63
|
+
const ph = py1 - py0;
|
|
64
|
+
const ex = (r: number) => px0 + r * pw;
|
|
65
|
+
|
|
66
|
+
const line = (x1: number, y1: number, x2: number, y2: number) => {
|
|
67
|
+
ctx.beginPath();
|
|
68
|
+
ctx.moveTo(x1, y1);
|
|
69
|
+
ctx.lineTo(x2, y2);
|
|
70
|
+
ctx.stroke();
|
|
71
|
+
};
|
|
72
|
+
const vtext = (
|
|
73
|
+
text: string,
|
|
74
|
+
x: number,
|
|
75
|
+
y: number,
|
|
76
|
+
fontSize: number,
|
|
77
|
+
color: string
|
|
78
|
+
) => {
|
|
79
|
+
ctx.save();
|
|
80
|
+
ctx.translate(x, y);
|
|
81
|
+
ctx.rotate(-Math.PI / 2);
|
|
82
|
+
ctx.font = `${fontSize}px ${FONT_FAMILY}`;
|
|
83
|
+
ctx.fillStyle = color;
|
|
84
|
+
ctx.textAlign = 'center';
|
|
85
|
+
ctx.textBaseline = 'alphabetic';
|
|
86
|
+
ctx.fillText(text, 0, 0);
|
|
87
|
+
ctx.restore();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ── Card (element bounds) ───────────────────────────────────────────
|
|
91
|
+
const inset = LINE.card / 2;
|
|
92
|
+
roundRectPath(ctx, inset, inset, w - inset * 2, h - inset * 2, CARD_RADIUS);
|
|
93
|
+
ctx.fillStyle = COLORS.card;
|
|
94
|
+
ctx.fill();
|
|
95
|
+
ctx.strokeStyle = COLORS.cardBorder;
|
|
96
|
+
ctx.lineWidth = LINE.card;
|
|
97
|
+
ctx.stroke();
|
|
98
|
+
|
|
99
|
+
// ── Curve-driven gradient variants (inscribed in the frame) ─────────
|
|
100
|
+
// Hidden when `showGradient` is false → plain white background.
|
|
101
|
+
if (model.variant !== 'classic' && model.showGradient) {
|
|
102
|
+
paintGradientBackground(ctx, model.variant, px0, px1, py0, py1);
|
|
103
|
+
if (model.variant === 'benefit') {
|
|
104
|
+
const zy = py1 - BENEFIT_ZERO_FRAC * ph;
|
|
105
|
+
ctx.strokeStyle = COLORS.axis;
|
|
106
|
+
ctx.lineWidth = 1.2;
|
|
107
|
+
ctx.beginPath();
|
|
108
|
+
ctx.moveTo(px0, zy);
|
|
109
|
+
ctx.lineTo(px1, zy);
|
|
110
|
+
ctx.stroke();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Optional evolution band tints ───────────────────────────────────
|
|
115
|
+
if (model.banded) {
|
|
116
|
+
const starts = [0, 0.175, 0.4, 0.7];
|
|
117
|
+
const ends = [0.175, 0.4, 0.7, 1];
|
|
118
|
+
for (let i = 0; i < 4; i++) {
|
|
119
|
+
ctx.fillStyle = COLORS.band[i];
|
|
120
|
+
ctx.fillRect(ex(starts[i]), py0, ex(ends[i]) - ex(starts[i]), ph);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Evolution phase dividers (dashed) ───────────────────────────────
|
|
125
|
+
if (model.showColumnDividers) {
|
|
126
|
+
ctx.strokeStyle = COLORS.divider;
|
|
127
|
+
ctx.lineWidth = LINE.divider;
|
|
128
|
+
ctx.setLineDash([5, 5]);
|
|
129
|
+
for (const r of EVOLUTION_BOUNDARIES) {
|
|
130
|
+
line(ex(r), py0, ex(r), py1);
|
|
131
|
+
}
|
|
132
|
+
ctx.setLineDash([]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Axes (L shape) + arrowheads ─────────────────────────────────────
|
|
136
|
+
// X and Y axes are independently toggleable. Each line stops at the base of
|
|
137
|
+
// its arrowhead (1px overlap, hidden under the triangle) so the line never
|
|
138
|
+
// pokes past the tip on zoom.
|
|
139
|
+
ctx.strokeStyle = COLORS.axis;
|
|
140
|
+
ctx.lineWidth = LINE.axis;
|
|
141
|
+
ctx.fillStyle = COLORS.axis;
|
|
142
|
+
if (model.showXAxis) {
|
|
143
|
+
line(px0, py1, px1 - ARROW + 1, py1); // X axis (arrow tip at px1)
|
|
144
|
+
ctx.beginPath(); // X arrow (points right)
|
|
145
|
+
ctx.moveTo(px1, py1);
|
|
146
|
+
ctx.lineTo(px1 - ARROW, py1 - ARROW / 2);
|
|
147
|
+
ctx.lineTo(px1 - ARROW, py1 + ARROW / 2);
|
|
148
|
+
ctx.closePath();
|
|
149
|
+
ctx.fill();
|
|
150
|
+
}
|
|
151
|
+
if (model.showYAxis) {
|
|
152
|
+
line(px0, py1, px0, py0 + ARROW - 1); // Y axis (arrow tip at py0)
|
|
153
|
+
ctx.beginPath(); // Y arrow (points up)
|
|
154
|
+
ctx.moveTo(px0, py0);
|
|
155
|
+
ctx.lineTo(px0 - ARROW / 2, py0 + ARROW);
|
|
156
|
+
ctx.lineTo(px0 + ARROW / 2, py0 + ARROW);
|
|
157
|
+
ctx.closePath();
|
|
158
|
+
ctx.fill();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Horizontal labels ───────────────────────────────────────────────
|
|
162
|
+
ctx.textBaseline = 'alphabetic';
|
|
163
|
+
// Phase (column) labels (left-aligned at each zone start)
|
|
164
|
+
if (model.showColumnLabels) {
|
|
165
|
+
ctx.font = `${FONTS.phase}px ${FONT_FAMILY}`;
|
|
166
|
+
ctx.fillStyle = COLORS.label;
|
|
167
|
+
ctx.textAlign = 'left';
|
|
168
|
+
const phases: Array<[string, number]> = [
|
|
169
|
+
[model.phase0, 0],
|
|
170
|
+
[model.phase1, 0.175],
|
|
171
|
+
[model.phase2, 0.4],
|
|
172
|
+
[model.phase3, 0.7],
|
|
173
|
+
];
|
|
174
|
+
for (const [label, start] of phases) {
|
|
175
|
+
ctx.fillText(
|
|
176
|
+
label,
|
|
177
|
+
ex(start) + OFFSETS.phasePad,
|
|
178
|
+
py1 + OFFSETS.phaseBaseline
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// "Evolution" title near the X arrow (tied to the X axis)
|
|
183
|
+
if (model.showXAxis) {
|
|
184
|
+
ctx.font = `${FONTS.axis}px ${FONT_FAMILY}`;
|
|
185
|
+
ctx.fillStyle = COLORS.axis;
|
|
186
|
+
ctx.textAlign = 'right';
|
|
187
|
+
ctx.fillText(
|
|
188
|
+
model.xAxisTitle,
|
|
189
|
+
px1 - OFFSETS.evolutionPadRight,
|
|
190
|
+
py1 + OFFSETS.phaseBaseline
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
// Direction indicators (Uncharted / Industrialized, top corners)
|
|
194
|
+
if (model.showCornerLabels) {
|
|
195
|
+
ctx.font = `${FONTS.direction}px ${FONT_FAMILY}`;
|
|
196
|
+
ctx.fillStyle = COLORS.label;
|
|
197
|
+
ctx.textAlign = 'left';
|
|
198
|
+
ctx.fillText(
|
|
199
|
+
model.evolutionStart,
|
|
200
|
+
px0 + OFFSETS.directionPadLeft,
|
|
201
|
+
py0 + OFFSETS.directionTop
|
|
202
|
+
);
|
|
203
|
+
ctx.textAlign = 'right';
|
|
204
|
+
ctx.fillText(
|
|
205
|
+
model.evolutionEnd,
|
|
206
|
+
px1 - OFFSETS.directionPadRight,
|
|
207
|
+
py0 + OFFSETS.directionTop
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Rotated Y labels (hugging the axis, symmetric with the X labels) ─
|
|
212
|
+
if (model.showYAxis) {
|
|
213
|
+
vtext(
|
|
214
|
+
model.yAxisTitle,
|
|
215
|
+
px0 - OFFSETS.yHug,
|
|
216
|
+
(py0 + py1) / 2,
|
|
217
|
+
FONTS.axis,
|
|
218
|
+
COLORS.axis
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (model.showVisibilityLabels) {
|
|
222
|
+
vtext(
|
|
223
|
+
model.visibilityHigh,
|
|
224
|
+
px0 - OFFSETS.yHug,
|
|
225
|
+
py0 + OFFSETS.visibleTop,
|
|
226
|
+
FONTS.visibility,
|
|
227
|
+
COLORS.label
|
|
228
|
+
);
|
|
229
|
+
vtext(
|
|
230
|
+
model.visibilityLow,
|
|
231
|
+
px0 - OFFSETS.yHug,
|
|
232
|
+
py1 - OFFSETS.invisibleBottom,
|
|
233
|
+
FONTS.visibility,
|
|
234
|
+
COLORS.label
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export const WardleyElementRendererExtension = ElementRendererExtension(
|
|
240
|
+
'wardley',
|
|
241
|
+
wardley
|
|
242
|
+
);
|