@peter.prib/renderengine 0.14.0 → 0.15.1
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/README.md +45 -19
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49641 -39747
- package/dist/renderengine.umd.js +25366 -15650
- package/documentation/REFERENCE.md +206 -0
- package/examples/decision-workbench.html +26 -0
- package/package.json +7 -1
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +219 -0
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +3 -0
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +114 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +52 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +5 -3
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +23 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +105 -2
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +4 -1
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +2 -2
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/RadarChart.js +7 -1
- package/src/charting/charts/RadialBarChart.js +5 -2
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +127 -4
- package/src/charting/charts/ScatterChart.js +13 -0
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +48 -5
- package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
- package/src/charting/charts/StreamGraphChart.js +10 -3
- package/src/charting/charts/SunburstChart.js +27 -7
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +28 -8
- package/src/charting/charts/ViolinChart.js +18 -0
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +109 -4
- package/src/charting/pivot/PivotGridChart.js +321 -33
- package/src/charting/pivot/PivotView.js +82 -0
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2995
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/index.js +3 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/parts/DecisionWorkbenchPart.js +58 -0
- package/src/workspace/parts/ViewsPart.js +196 -0
- package/src/workspace/styles.js +36 -1
package/src/SVGRenderer.js
CHANGED
|
@@ -1,455 +1,477 @@
|
|
|
1
|
-
import { BaseRenderer } from './BaseRenderer.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @class SVGRenderer
|
|
5
|
-
* Renders shapes by creating and appending SVG elements to an <svg> tag.
|
|
6
|
-
*/
|
|
7
|
-
export class SVGRenderer extends BaseRenderer {
|
|
8
|
-
get type() { return 'svg'; }
|
|
9
|
-
get supportsSceneGraph() { return true; }
|
|
10
|
-
|
|
11
|
-
static #instances = 0;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* The id of a line-end marker in this renderer's <defs>. Each renderer has
|
|
15
|
-
* its own prefix: ids are page-wide, and `url(#arrow)` would otherwise find
|
|
16
|
-
* the first SVG's marker, which may be hidden (another tab, another editor).
|
|
17
|
-
* @param {string} name - marker name, e.g. 'arrow', 'diamond-hollow'
|
|
18
|
-
*/
|
|
19
|
-
markerId(name) { return `${this.markerPrefix}${name}`; }
|
|
20
|
-
|
|
21
|
-
/** The `marker-start`/`marker-end` value for a marker name. */
|
|
22
|
-
markerUrl(name) { return `url(#${this.markerId(name)})`; }
|
|
23
|
-
|
|
24
|
-
_createElement(tag,attributes = {},stroke = null) {
|
|
25
|
-
const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
26
|
-
this._setAttributes(el, attributes);
|
|
27
|
-
if (stroke) {
|
|
28
|
-
this._setAttributes(el, { stroke: stroke.color || 'none', 'stroke-width': stroke.width || 1 });
|
|
29
|
-
// Used by the gauge arcs' rounded end-caps ({cap: 'round'}) — was
|
|
30
|
-
// silently dropped, so gauges always got default (butt) caps.
|
|
31
|
-
if (stroke.cap) this._setAttributes(el, { 'stroke-linecap': stroke.cap });
|
|
32
|
-
}
|
|
33
|
-
// Do not append here; let the caller decide.
|
|
34
|
-
// if (this.svg) this.svg.appendChild(el);
|
|
35
|
-
return el;
|
|
36
|
-
}
|
|
37
|
-
_elMap = new Map();
|
|
38
|
-
constructor(container, width, height) {
|
|
39
|
-
super(container, width, height);
|
|
40
|
-
this.markerPrefix = `re${SVGRenderer.#instances++}-`;
|
|
41
|
-
this.svg = this._createElement("svg",{ width, height, viewBox: `0 0 ${width} ${height}` });
|
|
42
|
-
// Inline elements (the default for <svg>) leave a few px of baseline
|
|
43
|
-
// whitespace below them inside a block container, making the
|
|
44
|
-
// container's scrollHeight exceed its clientHeight for every chart.
|
|
45
|
-
this.svg.style.display = 'block';
|
|
46
|
-
|
|
47
|
-
// Add a style tag for hover effects
|
|
48
|
-
const defs = this._createElement('defs');
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
arrowMarker
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
circleMarker.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
teeMarker
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
diamondMarker
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
diamondHollowMarker
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
triangleHollowMarker
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
openArrowMarker
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
halfArrowMarker
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
crowsfootOneMarker
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// "
|
|
112
|
-
|
|
113
|
-
crowsfootOneOptionalMarker
|
|
114
|
-
crowsfootOneOptionalMarker.appendChild(this._createElement('
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
crowsfootManyMarker
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
crowsfootManyMandatoryMarker
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
style
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
stroke
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
.selected >
|
|
140
|
-
.selected >
|
|
141
|
-
.selected >
|
|
142
|
-
.selected >
|
|
143
|
-
|
|
144
|
-
stroke
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
stroke
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
stroke
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
stroke
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this.svg.setAttribute('
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
this.
|
|
230
|
-
this.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
ctx
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
if (options.
|
|
252
|
-
if (options.
|
|
253
|
-
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
el
|
|
267
|
-
|
|
268
|
-
this.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
this.
|
|
283
|
-
this.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
this.
|
|
291
|
-
this.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
// the
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
'font-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
leaves.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (item.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
pathEl.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1
|
+
import { BaseRenderer } from './BaseRenderer.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class SVGRenderer
|
|
5
|
+
* Renders shapes by creating and appending SVG elements to an <svg> tag.
|
|
6
|
+
*/
|
|
7
|
+
export class SVGRenderer extends BaseRenderer {
|
|
8
|
+
get type() { return 'svg'; }
|
|
9
|
+
get supportsSceneGraph() { return true; }
|
|
10
|
+
|
|
11
|
+
static #instances = 0;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The id of a line-end marker in this renderer's <defs>. Each renderer has
|
|
15
|
+
* its own prefix: ids are page-wide, and `url(#arrow)` would otherwise find
|
|
16
|
+
* the first SVG's marker, which may be hidden (another tab, another editor).
|
|
17
|
+
* @param {string} name - marker name, e.g. 'arrow', 'diamond-hollow'
|
|
18
|
+
*/
|
|
19
|
+
markerId(name) { return `${this.markerPrefix}${name}`; }
|
|
20
|
+
|
|
21
|
+
/** The `marker-start`/`marker-end` value for a marker name. */
|
|
22
|
+
markerUrl(name) { return `url(#${this.markerId(name)})`; }
|
|
23
|
+
|
|
24
|
+
_createElement(tag,attributes = {},stroke = null) {
|
|
25
|
+
const el = document.createElementNS("http://www.w3.org/2000/svg", tag);
|
|
26
|
+
this._setAttributes(el, attributes);
|
|
27
|
+
if (stroke) {
|
|
28
|
+
this._setAttributes(el, { stroke: stroke.color || 'none', 'stroke-width': stroke.width || 1 });
|
|
29
|
+
// Used by the gauge arcs' rounded end-caps ({cap: 'round'}) — was
|
|
30
|
+
// silently dropped, so gauges always got default (butt) caps.
|
|
31
|
+
if (stroke.cap) this._setAttributes(el, { 'stroke-linecap': stroke.cap });
|
|
32
|
+
}
|
|
33
|
+
// Do not append here; let the caller decide.
|
|
34
|
+
// if (this.svg) this.svg.appendChild(el);
|
|
35
|
+
return el;
|
|
36
|
+
}
|
|
37
|
+
_elMap = new Map();
|
|
38
|
+
constructor(container, width, height) {
|
|
39
|
+
super(container, width, height);
|
|
40
|
+
this.markerPrefix = `re${SVGRenderer.#instances++}-`;
|
|
41
|
+
this.svg = this._createElement("svg",{ width, height, viewBox: `0 0 ${width} ${height}` });
|
|
42
|
+
// Inline elements (the default for <svg>) leave a few px of baseline
|
|
43
|
+
// whitespace below them inside a block container, making the
|
|
44
|
+
// container's scrollHeight exceed its clientHeight for every chart.
|
|
45
|
+
this.svg.style.display = 'block';
|
|
46
|
+
|
|
47
|
+
// Add a style tag for hover effects
|
|
48
|
+
const defs = this._createElement('defs');
|
|
49
|
+
this.defs = defs;
|
|
50
|
+
|
|
51
|
+
// Default arrow marker
|
|
52
|
+
const arrowMarker = this._createElement('marker', { id: this.markerId('arrow'), viewBox: '0 0 10 10', refX: 8, refY: 5, markerWidth: 6, markerHeight: 6, orient: 'auto-start-reverse' });
|
|
53
|
+
arrowMarker.appendChild(this._createElement('path', { d: 'M 0 0 L 10 5 L 0 10 z', class: 'arrowhead-path' }));
|
|
54
|
+
defs.appendChild(arrowMarker);
|
|
55
|
+
|
|
56
|
+
// Circle marker
|
|
57
|
+
const circleMarker = this._createElement('marker', { id: this.markerId('circle'), viewBox: '0 0 10 10', refX: 5, refY: 5, markerWidth: 6, markerHeight: 6, orient: 'auto-start-reverse' });
|
|
58
|
+
circleMarker.appendChild(this._createElement('circle', { cx: 5, cy: 5, r: 4, class: 'arrowhead-path' }));
|
|
59
|
+
defs.appendChild(circleMarker);
|
|
60
|
+
|
|
61
|
+
// Tee marker
|
|
62
|
+
const teeMarker = this._createElement('marker', { id: this.markerId('tee'), viewBox: '0 0 10 10', refX: 1, refY: 5, markerWidth: 6, markerHeight: 6, orient: 'auto-start-reverse' });
|
|
63
|
+
teeMarker.appendChild(this._createElement('path', { d: 'M 0 0 L 0 10', 'stroke-width': 2, class: 'arrowhead-path' }));
|
|
64
|
+
defs.appendChild(teeMarker);
|
|
65
|
+
|
|
66
|
+
// The four markers below exist for graphDiagram's relationship-style
|
|
67
|
+
// notation (composition/aggregation/realization/specialization/
|
|
68
|
+
// serving/access/influence — see _archimateRelationshipStyle) but
|
|
69
|
+
// aren't ArchiMate-specific themselves: any edge can use them via
|
|
70
|
+
// `arrowhead`/`arrowheadStart`, same as 'arrow'/'circle'/'tee' above.
|
|
71
|
+
// Filled diamond (composition's "whole"-end glyph).
|
|
72
|
+
const diamondMarker = this._createElement('marker', { id: this.markerId('diamond'), viewBox: '0 0 10 10', refX: 9, refY: 5, markerWidth: 8, markerHeight: 8, orient: 'auto-start-reverse' });
|
|
73
|
+
diamondMarker.appendChild(this._createElement('path', { d: 'M0,5 L5,0 L10,5 L5,10 Z', class: 'arrowhead-path' }));
|
|
74
|
+
defs.appendChild(diamondMarker);
|
|
75
|
+
|
|
76
|
+
// Hollow diamond (aggregation) — filled white, not transparent, so
|
|
77
|
+
// the line underneath doesn't show through the shape.
|
|
78
|
+
const diamondHollowMarker = this._createElement('marker', { id: this.markerId('diamond-hollow'), viewBox: '0 0 10 10', refX: 9, refY: 5, markerWidth: 8, markerHeight: 8, orient: 'auto-start-reverse' });
|
|
79
|
+
diamondHollowMarker.appendChild(this._createElement('path', { d: 'M0,5 L5,0 L10,5 L5,10 Z', class: 'arrowhead-path-hollow' }));
|
|
80
|
+
defs.appendChild(diamondHollowMarker);
|
|
81
|
+
|
|
82
|
+
// Hollow triangle (realization/specialization) — same white-filled
|
|
83
|
+
// convention as the hollow diamond above.
|
|
84
|
+
const triangleHollowMarker = this._createElement('marker', { id: this.markerId('triangle-hollow'), viewBox: '0 0 10 10', refX: 9, refY: 5, markerWidth: 8, markerHeight: 8, orient: 'auto-start-reverse' });
|
|
85
|
+
triangleHollowMarker.appendChild(this._createElement('path', { d: 'M0,0 L10,5 L0,10 Z', class: 'arrowhead-path-hollow' }));
|
|
86
|
+
defs.appendChild(triangleHollowMarker);
|
|
87
|
+
|
|
88
|
+
// Open/thin arrow (serving, access, influence) — an unfilled chevron,
|
|
89
|
+
// distinct from 'arrow's solid filled triangle.
|
|
90
|
+
const openArrowMarker = this._createElement('marker', { id: this.markerId('open-arrow'), viewBox: '0 0 10 10', refX: 9, refY: 5, markerWidth: 8, markerHeight: 8, orient: 'auto-start-reverse' });
|
|
91
|
+
openArrowMarker.appendChild(this._createElement('path', { d: 'M0,0 L9,5 L0,10', class: 'arrowhead-path-open' }));
|
|
92
|
+
defs.appendChild(openArrowMarker);
|
|
93
|
+
|
|
94
|
+
// Half/single-barb open arrow — ArchiMate's DIRECTED association (the
|
|
95
|
+
// spec's own relationships figure shows association twice: a plain
|
|
96
|
+
// line, and this one-barb variant for the directed form).
|
|
97
|
+
const halfArrowMarker = this._createElement('marker', { id: this.markerId('open-arrow-half'), viewBox: '0 0 10 10', refX: 9, refY: 5, markerWidth: 8, markerHeight: 8, orient: 'auto-start-reverse' });
|
|
98
|
+
halfArrowMarker.appendChild(this._createElement('path', { d: 'M0,0 L9,5', class: 'arrowhead-path-open' }));
|
|
99
|
+
defs.appendChild(halfArrowMarker);
|
|
100
|
+
|
|
101
|
+
// Crow's-foot ERD cardinality glyphs (dataModelDiagram's 'crowsfoot'
|
|
102
|
+
// notation — see src/datamodel.js) — a representative rendition, not
|
|
103
|
+
// every real-notation optionality combination. All four share a
|
|
104
|
+
// 16-wide viewBox with refX:15 (the point that sits ON the entity's
|
|
105
|
+
// edge) so the drawn glyph sits BETWEEN the entity and the line,
|
|
106
|
+
// extending away from it toward x:0.
|
|
107
|
+
// "Exactly one" (mandatory, single cardinality) — one perpendicular tick.
|
|
108
|
+
const crowsfootOneMarker = this._createElement('marker', { id: this.markerId('crowsfoot-one'), viewBox: '0 0 16 10', refX: 15, refY: 5, markerWidth: 12, markerHeight: 8, orient: 'auto-start-reverse' });
|
|
109
|
+
crowsfootOneMarker.appendChild(this._createElement('path', { d: 'M10,1 L10,9', class: 'arrowhead-path-open' }));
|
|
110
|
+
defs.appendChild(crowsfootOneMarker);
|
|
111
|
+
// "Zero or one" (optional, single cardinality) — hollow circle (the
|
|
112
|
+
// "zero"/optional part) plus one tick (the "one" part) further out.
|
|
113
|
+
const crowsfootOneOptionalMarker = this._createElement('marker', { id: this.markerId('crowsfoot-one-optional'), viewBox: '0 0 16 10', refX: 15, refY: 5, markerWidth: 12, markerHeight: 8, orient: 'auto-start-reverse' });
|
|
114
|
+
crowsfootOneOptionalMarker.appendChild(this._createElement('circle', { cx: 12, cy: 5, r: 3, class: 'arrowhead-path-hollow' }));
|
|
115
|
+
crowsfootOneOptionalMarker.appendChild(this._createElement('path', { d: 'M5,1 L5,9', class: 'arrowhead-path-open' }));
|
|
116
|
+
defs.appendChild(crowsfootOneOptionalMarker);
|
|
117
|
+
// "Zero or many" (optional, many cardinality) — the crow's-foot fork
|
|
118
|
+
// alone, its three prongs touching the entity edge.
|
|
119
|
+
const crowsfootManyMarker = this._createElement('marker', { id: this.markerId('crowsfoot-many'), viewBox: '0 0 16 10', refX: 15, refY: 5, markerWidth: 12, markerHeight: 8, orient: 'auto-start-reverse' });
|
|
120
|
+
crowsfootManyMarker.appendChild(this._createElement('path', { d: 'M5,5 L15,0 M5,5 L15,5 M5,5 L15,10', class: 'arrowhead-path-open' }));
|
|
121
|
+
defs.appendChild(crowsfootManyMarker);
|
|
122
|
+
// "One or many" (mandatory, many cardinality) — the fork plus one
|
|
123
|
+
// tick further out (the "at least one" part).
|
|
124
|
+
const crowsfootManyMandatoryMarker = this._createElement('marker', { id: this.markerId('crowsfoot-many-mandatory'), viewBox: '0 0 16 10', refX: 15, refY: 5, markerWidth: 12, markerHeight: 8, orient: 'auto-start-reverse' });
|
|
125
|
+
crowsfootManyMandatoryMarker.appendChild(this._createElement('path', { d: 'M5,5 L15,0 M5,5 L15,5 M5,5 L15,10 M2,2 L2,8', class: 'arrowhead-path-open' }));
|
|
126
|
+
defs.appendChild(crowsfootManyMandatoryMarker);
|
|
127
|
+
|
|
128
|
+
this.svg.appendChild(defs);
|
|
129
|
+
const style =this._createElement("style");
|
|
130
|
+
style.textContent = `
|
|
131
|
+
.hover {
|
|
132
|
+
fill: orange;
|
|
133
|
+
stroke: #333;
|
|
134
|
+
stroke-width: 2px;
|
|
135
|
+
}
|
|
136
|
+
.selected {
|
|
137
|
+
filter: drop-shadow(0 0 3px var(--ws-accent, #91cc75));
|
|
138
|
+
}
|
|
139
|
+
.selected > path,
|
|
140
|
+
.selected > rect,
|
|
141
|
+
.selected > circle,
|
|
142
|
+
.selected > ellipse,
|
|
143
|
+
.selected > polygon {
|
|
144
|
+
stroke: var(--ws-accent, #91cc75);
|
|
145
|
+
stroke-width: 3px;
|
|
146
|
+
}
|
|
147
|
+
.selected-edge {
|
|
148
|
+
stroke: var(--ws-accent, #91cc75);
|
|
149
|
+
stroke-width: 3px;
|
|
150
|
+
}
|
|
151
|
+
.arrowhead-path {
|
|
152
|
+
fill: inherit;
|
|
153
|
+
stroke: inherit;
|
|
154
|
+
}
|
|
155
|
+
.arrowhead-path-hollow {
|
|
156
|
+
fill: white;
|
|
157
|
+
stroke: black;
|
|
158
|
+
stroke-width: 1px;
|
|
159
|
+
}
|
|
160
|
+
.arrowhead-path-open {
|
|
161
|
+
fill: none;
|
|
162
|
+
stroke: black;
|
|
163
|
+
stroke-width: 1.5px;
|
|
164
|
+
}
|
|
165
|
+
`;
|
|
166
|
+
this.svg.appendChild(style);
|
|
167
|
+
// The main SVG is now the default parent for new elements.
|
|
168
|
+
this.container.appendChild(this.svg);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resize the <svg>. Its viewBox follows only while it is still the size it
|
|
173
|
+
* was created with: a diagram sets its own viewBox to its content, and that
|
|
174
|
+
* must survive, so the diagram then scales to the new box instead of
|
|
175
|
+
* being cropped to it.
|
|
176
|
+
*/
|
|
177
|
+
resize(width, height) {
|
|
178
|
+
const defaultViewBox = `0 0 ${this.width} ${this.height}`;
|
|
179
|
+
super.resize(width, height);
|
|
180
|
+
this.svg.setAttribute('width', width);
|
|
181
|
+
this.svg.setAttribute('height', height);
|
|
182
|
+
if (this.svg.getAttribute('viewBox') === defaultViewBox) {
|
|
183
|
+
this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
clear() {
|
|
188
|
+
this._elMap.clear();
|
|
189
|
+
// Keep <defs> and <style> elements; remove everything else.
|
|
190
|
+
for (const child of [...this.svg.children]) {
|
|
191
|
+
if (child.tagName !== 'style' && child.tagName !== 'defs') child.remove();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
_defineShape(shape,attributes, stroke = null) {
|
|
195
|
+
shape.id=`shape_${this._elMap.size}`;
|
|
196
|
+
const parent = shape.parent || this.svg;
|
|
197
|
+
const el = this._createElement(shape.type, attributes, stroke);
|
|
198
|
+
parent.appendChild(el);
|
|
199
|
+
this._elMap.set(shape.id, { el, shape });
|
|
200
|
+
return shape.id;
|
|
201
|
+
}
|
|
202
|
+
rect(x, y, width, height, fill, stroke, extra) {
|
|
203
|
+
const shape = { type: 'rect', x, y, width, height, fill, stroke, ...extra };
|
|
204
|
+
this._defineShape(shape, { x, y, width, height, fill: fill || 'none', ...extra }, stroke);
|
|
205
|
+
return this._elMap.get(shape.id).el;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
circle(cx, cy, r, fill, stroke, extra) {
|
|
209
|
+
const shape = { type: 'circle', cx, cy, r, fill, stroke, ...extra};
|
|
210
|
+
this._defineShape(shape, { cx, cy, r, fill: fill || 'none' }, stroke);
|
|
211
|
+
return this._elMap.get(shape.id).el;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
path(d, fill, stroke, extra = {}) {
|
|
215
|
+
// `extra` (e.g. {tooltip}) merges into the shape descriptor only, not
|
|
216
|
+
// into DOM attributes — hitTest()/tooltip resolution reads it straight
|
|
217
|
+
// off `shape` (see _addTooltipListeners), it was never rendered as a
|
|
218
|
+
// real SVG attribute for rect/circle either. Previously missing here
|
|
219
|
+
// (CanvasRenderer.path already accepted it) — pie/doughnut wedges,
|
|
220
|
+
// candlestick bodies, and box-plot boxes all need path-level tooltips.
|
|
221
|
+
const shape = { type: 'path', d, fill, stroke, ...extra, id: `shape_${this._elMap.size}` };
|
|
222
|
+
// _createElement already applies stroke.color/width as real SVG attributes
|
|
223
|
+
// (see the `stroke` param handling above) — do NOT also set them as inline
|
|
224
|
+
// styles here. Setting style.fill = stroke.color previously clobbered any
|
|
225
|
+
// closed shape's real fill (e.g. graphDiagram's diamond node, filled with
|
|
226
|
+
// the group color but stroked white) with the stroke color instead, since
|
|
227
|
+
// inline styles win over attributes. Open paths (edge lines, fill:'none')
|
|
228
|
+
// never showed the bug since there's no enclosed area to paint either way.
|
|
229
|
+
const pathEl = this._createElement("path", { d, fill: fill || 'none' }, stroke);
|
|
230
|
+
this.svg.appendChild(pathEl);
|
|
231
|
+
this._elMap.set(shape.id, { el: pathEl, shape });
|
|
232
|
+
return pathEl;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Text width measured with a (shared, off-page) 2D canvas context; the estimate without a document. */
|
|
236
|
+
measureText(text, font = {}) {
|
|
237
|
+
if (typeof document === 'undefined' || typeof document.createElement !== 'function') return super.measureText(text, font);
|
|
238
|
+
const ctx = (SVGRenderer._measureContext ??= document.createElement('canvas').getContext?.('2d'));
|
|
239
|
+
if (!ctx) return super.measureText(text, font);
|
|
240
|
+
ctx.font = BaseRenderer.cssFont(font);
|
|
241
|
+
return ctx.measureText(String(text)).width;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
text(text, x, y, options = {}) {
|
|
245
|
+
const shape = { type: 'text', text, x, y, ...options, id: `shape_${this._elMap.size}` };
|
|
246
|
+
const attrs = { x, y, fill: options.fill || 'black', 'font-family': options.fontFamily || 'sans-serif', 'font-size': options.fontSize || '12px', 'text-anchor': options.textAnchor || 'start' };
|
|
247
|
+
// Several call sites (graph-node initials/labels, table-node rows) pass
|
|
248
|
+
// verticalAlign:'middle' expecting the text to be vertically centred on
|
|
249
|
+
// its y-coordinate — this was silently ignored (SVG's default baseline
|
|
250
|
+
// is alphabetic, not middle), so multi-row content sat visibly low.
|
|
251
|
+
if (options.verticalAlign === 'middle') attrs['dominant-baseline'] = 'middle';
|
|
252
|
+
if (options.fontWeight) attrs['font-weight'] = options.fontWeight;
|
|
253
|
+
if (options.fontStyle) attrs['font-style'] = options.fontStyle;
|
|
254
|
+
// Every graphDiagram call site passes 'pointer-events':'none' on node
|
|
255
|
+
// initials/labels expecting clicks to fall through to the shape/badge
|
|
256
|
+
// underneath — silently dropped since it wasn't in this whitelist, so
|
|
257
|
+
// filled text always intercepted the click regardless of what callers
|
|
258
|
+
// asked for (surfaced by the new collapse-badge glyph sitting on top
|
|
259
|
+
// of its own circle and eating every click meant for it).
|
|
260
|
+
if (options['pointer-events']) attrs['pointer-events'] = options['pointer-events'];
|
|
261
|
+
// `rotation` is in radians (matches Canvas's ctx.rotate() convention) —
|
|
262
|
+
// convert to degrees for SVG's transform=rotate(deg, cx, cy). Every
|
|
263
|
+
// yAxisLabel call across the chart methods relies on this to render
|
|
264
|
+
// vertically instead of overlapping/clipping past the chart's left edge.
|
|
265
|
+
if (options.rotation) attrs.transform = `rotate(${options.rotation * 180 / Math.PI} ${x} ${y})`;
|
|
266
|
+
const el = this._createElement('text', attrs);
|
|
267
|
+
el.textContent = text;
|
|
268
|
+
this.svg.appendChild(el);
|
|
269
|
+
this._elMap.set(shape.id, { el, shape });
|
|
270
|
+
return el;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** An SVG `<image>` — used for graphDiagram's `node.iconUrl` (a custom
|
|
274
|
+
* icon/image file/data URI centred on a node, in place of the plain
|
|
275
|
+
* initials text). `href` is set as the plain SVG2 attribute; every
|
|
276
|
+
* renderer target this library ships against (real browsers, jsdom in
|
|
277
|
+
* tests) resolves that without also needing the legacy `xlink:href`. */
|
|
278
|
+
image(href, x, y, width, height, options = {}) {
|
|
279
|
+
const shape = { type: 'image', href, x, y, width, height, ...options, id: `shape_${this._elMap.size}` };
|
|
280
|
+
const attrs = { href, x, y, width, height, preserveAspectRatio: options.preserveAspectRatio || 'xMidYMid meet' };
|
|
281
|
+
if (options['pointer-events']) attrs['pointer-events'] = options['pointer-events'];
|
|
282
|
+
const el = this._createElement('image', attrs);
|
|
283
|
+
this.svg.appendChild(el);
|
|
284
|
+
this._elMap.set(shape.id, { el, shape });
|
|
285
|
+
return el;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
group(attributes = {}) {
|
|
289
|
+
const shape = { type: 'g', ...attributes, id: `shape_${this._elMap.size}` };
|
|
290
|
+
const el = this._createElement('g', attributes);
|
|
291
|
+
this.svg.appendChild(el);
|
|
292
|
+
this._elMap.set(shape.id, { el, shape });
|
|
293
|
+
return el;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
hitTest(x, y) {
|
|
297
|
+
// This is a bit of a hack. We find the element and then iterate our map to find the shape.
|
|
298
|
+
// A more robust solution might use a WeakMap from element to shape.
|
|
299
|
+
//
|
|
300
|
+
// `x`/`y` are viewport coordinates (every caller in index.js passes
|
|
301
|
+
// event.clientX/clientY straight through) — document.elementFromPoint
|
|
302
|
+
// already takes coordinates in that same space. Adding this.svg's own
|
|
303
|
+
// getBoundingClientRect().left/top on top double-offset the query
|
|
304
|
+
// point by the canvas's on-page position, so hitTest silently queried
|
|
305
|
+
// the wrong spot on the page for any layout where the SVG isn't flush
|
|
306
|
+
// against the browser's top-left corner (i.e. basically always, once
|
|
307
|
+
// there's a toolbar/sidebar around it) — drag-start, the context
|
|
308
|
+
// menu, and tooltips all read this result directly, so all three
|
|
309
|
+
// misfired by exactly that offset. Zoom (the SVG's internal viewBox
|
|
310
|
+
// scale) doesn't factor in here at all: elementFromPoint operates on
|
|
311
|
+
// real rendered screen pixels, which already reflect whatever zoom is
|
|
312
|
+
// applied, so no CTM/zoom math belongs in this function.
|
|
313
|
+
const hitEl = this.svg.ownerDocument.elementFromPoint(x, y);
|
|
314
|
+
if (!hitEl) return null;
|
|
315
|
+
return SVGRenderer.resolveHit(this._elMap, hitEl);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Given the element under the cursor and the el→{el,shape} registry, picks
|
|
320
|
+
* which registered shape a hit resolves to. Split out from hitTest() so
|
|
321
|
+
* this candidate-selection logic is unit-testable without a real DOM
|
|
322
|
+
* (`hitEl`/registered `el`s just need a `.contains()` method).
|
|
323
|
+
*
|
|
324
|
+
* A graph node is drawn as an outer <g draggable nodeId tooltip> wrapping
|
|
325
|
+
* an inner shape primitive (circle/rect/path) plus text labels — each
|
|
326
|
+
* registered in _elMap separately, with the inner primitive inserted
|
|
327
|
+
* *before* the wrapping group. Since _elMap is iterated in insertion order
|
|
328
|
+
* and this used to return the *first* match, a click on the node's own
|
|
329
|
+
* visible body resolved to the inner primitive's bare shape (no
|
|
330
|
+
* draggable/tooltip/nodeId) instead of the wrapping group — silently
|
|
331
|
+
* breaking drag-start, the context menu, and hover tooltips (all of which
|
|
332
|
+
* read hitTest()'s result directly, unlike click-to-select which bubbles
|
|
333
|
+
* via a real DOM listener). Prefer a draggable node wrapper among the
|
|
334
|
+
* candidates when one is present; otherwise keep the original first-match
|
|
335
|
+
* behavior (legend markers etc. resolve via their own direct listeners
|
|
336
|
+
* already, not this method).
|
|
337
|
+
*/
|
|
338
|
+
static resolveHit(elMap, hitEl) {
|
|
339
|
+
let first = null;
|
|
340
|
+
for (const { el, shape } of elMap.values()) {
|
|
341
|
+
if (el === hitEl || el.contains(hitEl)) {
|
|
342
|
+
if (!first) first = shape;
|
|
343
|
+
if (shape.draggable && shape.nodeId != null) return shape;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return first;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
enterState(shape, stateName) {
|
|
350
|
+
this._elMap.get(shape.id)?.el.classList.add(stateName);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
leaveState(shape, stateName) {
|
|
354
|
+
this._elMap.get(shape.id)?.el.classList.remove(stateName);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
renderDiagram(diagram) {
|
|
358
|
+
// 20,20 padding matches this file's own DiagramNode.prototype.drawOn
|
|
359
|
+
// (railroadDiagram.js, the Canvas rendering path) — without a real
|
|
360
|
+
// starting x/y, every node's format() computes its own position as
|
|
361
|
+
// NaN (this used to be called with zero args, so x/y were undefined
|
|
362
|
+
// throughout the whole tree).
|
|
363
|
+
diagram.format(20, 20);
|
|
364
|
+
|
|
365
|
+
const group = this._createElement('g', {
|
|
366
|
+
'font-family': 'monospace',
|
|
367
|
+
'font-size': '12px'
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Collect every leaf first, then draw in a fixed path -> rect -> text
|
|
371
|
+
// paint order, rather than the tree's own traversal order. Connector
|
|
372
|
+
// lines (Sequence's backbone, ZeroOrMore's top line, Choice's arcs)
|
|
373
|
+
// legitimately run behind boxes they connect through — but walk()
|
|
374
|
+
// visits nodes in whatever order each FORMATTERS.* function happened
|
|
375
|
+
// to build its .item/.items — e.g. a Sequence's own backbone Path is
|
|
376
|
+
// appended (via addTo()) AFTER its child items are formatted, so it
|
|
377
|
+
// painted on top of and struck through every terminal/nonterminal
|
|
378
|
+
// box's text. A fixed paint order guarantees lines stay underneath
|
|
379
|
+
// opaque boxes and text stays readable on top, regardless of which
|
|
380
|
+
// order the diagram tree happens to store its children in.
|
|
381
|
+
const leaves = [];
|
|
382
|
+
diagram.walk(item => {
|
|
383
|
+
// The `if (item.type === 'rect'/'path'/'text')` chain below
|
|
384
|
+
// already correctly picks out only real leaf shapes — every
|
|
385
|
+
// intermediate DiagramNode-tree node (Sequence/Choice/Terminal/
|
|
386
|
+
// NonTerminal/...) has a .type that never matches any of those,
|
|
387
|
+
// so it's harmless to reach the chain and fall through. A prior
|
|
388
|
+
// `if (!item.width) return;` guard here was presumably meant to
|
|
389
|
+
// skip those same intermediate nodes, but 'text' and 'path'
|
|
390
|
+
// FakeSVG leaves never carry a .width at all (only 'rect' does —
|
|
391
|
+
// same as real SVG <text>/<path> elements), so it silently
|
|
392
|
+
// discarded every label and every connector line too.
|
|
393
|
+
if (item.type === 'rect' || item.type === 'path' || item.type === 'text') leaves.push(item);
|
|
394
|
+
});
|
|
395
|
+
const paintOrder = { path: 0, rect: 1, text: 2 };
|
|
396
|
+
leaves.sort((a, b) => paintOrder[a.type] - paintOrder[b.type]);
|
|
397
|
+
leaves.forEach(item => {
|
|
398
|
+
if (item.type === 'rect') {
|
|
399
|
+
// NonTerminal rects have no rx/ry (sharp corners, by design —
|
|
400
|
+
// see railroadDiagram.js's FORMATTERS.NonTerminal) — only
|
|
401
|
+
// pass them through when real, rather than setting a literal
|
|
402
|
+
// "undefined" string attribute value.
|
|
403
|
+
const extra = { parent: group };
|
|
404
|
+
if (item.rx != null) extra.rx = item.rx;
|
|
405
|
+
if (item.ry != null) extra.ry = item.ry;
|
|
406
|
+
this.rect(item.x, item.y, item.width, item.height, 'hsl(30,20%,95%)', { color: 'black', width: 2 }, extra);
|
|
407
|
+
} else if (item.type === 'path') {
|
|
408
|
+
const pathEl = this._createElement('path', { d: item.attrs.d }, { color: 'black', width: 3 });
|
|
409
|
+
pathEl.setAttribute('fill', 'none');
|
|
410
|
+
group.appendChild(pathEl);
|
|
411
|
+
} else if (item.type === 'text') {
|
|
412
|
+
const textEl = this._createElement('text', {
|
|
413
|
+
x: item.x,
|
|
414
|
+
y: item.y,
|
|
415
|
+
'text-anchor': 'middle',
|
|
416
|
+
fill: 'black'
|
|
417
|
+
});
|
|
418
|
+
if (item.attrs.class === 'comment') {
|
|
419
|
+
textEl.setAttribute('font-style', 'italic');
|
|
420
|
+
} else if (item.type === 'NonTerminal') {
|
|
421
|
+
textEl.setAttribute('font-weight', 'bold');
|
|
422
|
+
}
|
|
423
|
+
textEl.textContent = item.text;
|
|
424
|
+
group.appendChild(textEl);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Every other SVGRenderer primitive (group()/rect()/circle()/...)
|
|
429
|
+
// appends itself to this.svg (or a specified parent) within its own
|
|
430
|
+
// method — this one built `group` and populated it via the walk
|
|
431
|
+
// callback's this.rect(...) calls (which do append into `group`,
|
|
432
|
+
// via the `parent: group` extra above), but never attached `group`
|
|
433
|
+
// to the actual <svg> itself, so nothing was ever visible.
|
|
434
|
+
this.svg.appendChild(group);
|
|
435
|
+
|
|
436
|
+
// A diagram's natural width is data-driven (however many rule
|
|
437
|
+
// elements it has) and routinely wider than a chart's fixed
|
|
438
|
+
// container box. Only the viewBox is updated to the content's real
|
|
439
|
+
// size — width/height stay at whatever they were set to at
|
|
440
|
+
// construction (the container's own clientWidth/clientHeight, same
|
|
441
|
+
// as every other chart type) — so the SVG's default
|
|
442
|
+
// preserveAspectRatio="xMidYMid meet" scales the whole diagram down
|
|
443
|
+
// to fit that fixed box, exactly like an <img>: no scrollbar, no
|
|
444
|
+
// clipping, just a smaller rendering for a wide diagram. (An
|
|
445
|
+
// earlier version of this also resized width/height to match the
|
|
446
|
+
// content, which fit the diagram at full native size but broke
|
|
447
|
+
// the page's CSS grid layout — a grid item's automatic minimum
|
|
448
|
+
// size is based on its content's intrinsic size unless something
|
|
449
|
+
// caps it — and even with that capped, still needed a scrollbar to
|
|
450
|
+
// reach the full content. Not resizing width/height avoids both.)
|
|
451
|
+
const w = diagram.width + 40, h = diagram.height + 40;
|
|
452
|
+
this.svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
|
|
453
|
+
|
|
454
|
+
return group;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Add a <linearGradient> to <defs> and return its url(#id) reference.
|
|
459
|
+
* @param {string} id
|
|
460
|
+
* @param {Array<{offset: string|number, color: string, opacity?: number}>} stops
|
|
461
|
+
* @param {{x1?: string, y1?: string, x2?: string, y2?: string}} [options]
|
|
462
|
+
* @returns {string} url(#id)
|
|
463
|
+
*/
|
|
464
|
+
linearGradient(id, stops = [], { x1 = '0%', y1 = '0%', x2 = '0%', y2 = '100%' } = {}) {
|
|
465
|
+
const existing = this.svg?.querySelector ? this.svg.querySelector(`#${id}`) : null;
|
|
466
|
+
if (existing) return `url(#${id})`;
|
|
467
|
+
const grad = this._createElement('linearGradient', { id, x1, y1, x2, y2 });
|
|
468
|
+
for (const stop of stops) {
|
|
469
|
+
const offset = typeof stop.offset === 'number' ? `${stop.offset * 100}%` : stop.offset;
|
|
470
|
+
const attrs = { offset, 'stop-color': stop.color };
|
|
471
|
+
if (stop.opacity !== undefined) attrs['stop-opacity'] = String(stop.opacity);
|
|
472
|
+
grad.appendChild(this._createElement('stop', attrs));
|
|
473
|
+
}
|
|
474
|
+
this.defs?.appendChild(grad);
|
|
475
|
+
return `url(#${id})`;
|
|
476
|
+
}
|
|
455
477
|
}
|