@peter.prib/renderengine 0.13.0 → 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/README.md +480 -302
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49217 -37484
- package/dist/renderengine.umd.js +24899 -13093
- package/documentation/REFERENCE.md +68 -1
- package/package.json +13 -3
- 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 +269 -3
- 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 +7 -2
- 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 +142 -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/ask/AskBar.js +388 -0
- package/src/charting/ask/AskView.js +400 -0
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +69 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +6 -4
- package/src/charting/charts/BarChart3DOrbit.js +3 -1
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +25 -3
- package/src/charting/charts/BubbleChart.js +4 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +107 -4
- 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/DensityChart.js +1 -1
- 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 +5 -2
- 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 +6 -3
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HistogramChart.js +2 -2
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/IcicleChart.js +47 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/LollipopChart.js +104 -0
- package/src/charting/charts/MarimekkoChart.js +75 -0
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/OutlineChart.js +10 -8
- package/src/charting/charts/ParallelCoordinatesChart.js +11 -3
- package/src/charting/charts/ParetoChart.js +92 -0
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PipeChart.js +5 -2
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/PopulationPyramidChart.js +4 -4
- package/src/charting/charts/RadarChart.js +12 -5
- package/src/charting/charts/RadialBarChart.js +6 -3
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +129 -6
- package/src/charting/charts/ScatterChart.js +19 -3
- 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 +125 -11
- package/src/charting/charts/StackedBarChart3DOrbit.js +14 -6
- package/src/charting/charts/StreamGraphChart.js +18 -3
- package/src/charting/charts/SunburstChart.js +28 -8
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +31 -11
- package/src/charting/charts/ViolinChart.js +20 -2
- package/src/charting/charts/WaterfallChart.js +2 -2
- package/src/charting/charts/WordCloudChart.js +2 -2
- 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 +310 -13
- package/src/charting/pivot/PivotGridChart.js +322 -34
- package/src/charting/pivot/PivotView.js +626 -21
- 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 -2899
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/styles.js +36 -1
|
@@ -74,7 +74,7 @@ catalog `key` for you.
|
|
|
74
74
|
| `sankey` | `sankeyDiagram(data, opts)` | `{nodes?: Array<{id,label?,color?}>, links: Array<{source,target,value}>}` | proportional-width flow links; `nodeWidth`, `linkOpacity` |
|
|
75
75
|
| `chord` | `chordDiagram(data, opts)` | same as `sankey` | nodes on a circle with curved chords of proportional thickness |
|
|
76
76
|
| `candlestick` | `candlestick(data, opts)` | `Array<{label, open, high, low, close}>` | `upColor`, `downColor`, `markers` (tooltip annotations) |
|
|
77
|
-
| `stackedBar` | `stackedBarChart(data, opts)` | `Array<{label, values:number[]}>` | `colors`
|
|
77
|
+
| `stackedBar` | `stackedBarChart(data, opts)` | multi-series `Array<{name, values:Array<{label,value}>}>` or `Array<{label, values:number[]}>` | `colors`, `legend`, `seriesLabels`; draws Y-axis ticks and legend |
|
|
78
78
|
| `pipe` | `pipeChart(data, opts)` | `Array<{label, value, metric?}>` or multi-series | `metric` tapers line width; `legend` |
|
|
79
79
|
| `radar` | `radarChart(data, opts)` | `Array<{axis, value}>` or multi-series | `colors`, `legend` |
|
|
80
80
|
| `funnel` | `funnelChart(data, opts)` | `Array<{label, value}>` (largest first) | `colors` |
|
|
@@ -3200,6 +3200,44 @@ to it) / `deleteEdgeCommand(id)`. A subclass whose shapes and lines stand for so
|
|
|
3200
3200
|
overrides those factories (ArchimateCanvasEditor does). A template class says whether the editor
|
|
3201
3201
|
offers it a rotate handle (`static rotatable`, true by default; ArchiMate's templates are not). `pickFile(accept)` resolves with the chosen `File` (or `null`).
|
|
3202
3202
|
|
|
3203
|
+
## Decision Workbench (`DecisionWorkbench`)
|
|
3204
|
+
|
|
3205
|
+
The views of a SADIS recommendation made by decisionengine (`gitlab.com/peter.prib/decisionengine`),
|
|
3206
|
+
drawn with this library's charts. It reads decisionengine's JSON, not decisionengine: a workbench
|
|
3207
|
+
document
|
|
3208
|
+
|
|
3209
|
+
```js
|
|
3210
|
+
{ kind: 'sadis-workbench', report, plan?, before? } // report.toJSON(), plan.toJSON(), an earlier report.toJSON()
|
|
3211
|
+
```
|
|
3212
|
+
|
|
3213
|
+
which decisionengine's SADIS Explorer downloads ("Download for the Decision Workbench").
|
|
3214
|
+
|
|
3215
|
+
```js
|
|
3216
|
+
import { DecisionWorkbench } from '@peter.prib/renderengine';
|
|
3217
|
+
const workbench = DecisionWorkbench.from(doc, { start: '2026-11-02', periodDays: 14 });
|
|
3218
|
+
for (const view of workbench.views()) {
|
|
3219
|
+
view.id, view.title; // 'executive', 'Executive', …
|
|
3220
|
+
view.charts(); // [{ title, chartType, data, options }] — chartType is a CHART_CATALOG key
|
|
3221
|
+
view.notes(); // plain lines for what the charts cannot show
|
|
3222
|
+
}
|
|
3223
|
+
```
|
|
3224
|
+
|
|
3225
|
+
| View | Needs | Charts |
|
|
3226
|
+
|---|---|---|
|
|
3227
|
+
| Executive | a report | `kpi-tile` (what to build next; half the value delivered by), `stackedBar` (score part by part), `line` (value delivered by period) |
|
|
3228
|
+
| Architecture | a report | `scatter` (visible rating against basin position: foundations sit high and left), `lollipop` (share each frees) |
|
|
3229
|
+
| Delivery | a plan | `gantt` (the plan on a calendar), `stackedBar` (capacity used per period, per team when planned by team) |
|
|
3230
|
+
| Portfolio | a report with perspectives | `heatmap` (rank by perspective), `dumbbell` (best and worst rank, most contested first) |
|
|
3231
|
+
| Replay | an earlier report | `slopegraph` (rank then → now); notes say what entered, left (and why) and moved |
|
|
3232
|
+
|
|
3233
|
+
A view the document cannot fill is not offered. `from()` refuses what is not a workbench document,
|
|
3234
|
+
saying why. The delivery plan's periods are put on a calendar: period 1 starts at `start` (default:
|
|
3235
|
+
the report's as-of business time, else today), each `periodDays` long (default 14). Each view is a
|
|
3236
|
+
`WorkbenchView` subclass (`WORKBENCH_VIEWS`); a new view is a new subclass.
|
|
3237
|
+
|
|
3238
|
+
`examples/decision-workbench.html` is the page: open a workbench file and each view is a workspace
|
|
3239
|
+
perspective, its charts in `ChartPart`s and its notes in a list.
|
|
3240
|
+
|
|
3203
3241
|
## Navigation menus
|
|
3204
3242
|
|
|
3205
3243
|
`NavMenu` is a vertical navigation menu (a sidebar's), written as data: no markup. Each entry is
|
|
@@ -3282,6 +3320,35 @@ const mine = ICONS.with({ // a host's own icons over
|
|
|
3282
3320
|
the workspace bundle), until a `foreignObject`, whose children are HTML again. A spec's `ns` sets the namespace
|
|
3283
3321
|
explicitly.
|
|
3284
3322
|
|
|
3323
|
+
## Ask Bar & Next-Word Typeahead (`AskBar` / `AskView`)
|
|
3324
|
+
|
|
3325
|
+
`AskBar` (`src/charting/ask/AskBar.js`) and its DOM-free state model `AskView` (`src/charting/ask/AskView.js`) provide the interactive question bar and scrollable next-word prediction dropdown (as used in `semanticengine/examples/browser/index.html` and the **Cubing** tab in `test/charts.html`).
|
|
3326
|
+
|
|
3327
|
+
```javascript
|
|
3328
|
+
import { RenderEngine, AskBar, AskView } from '@peter.prib/renderengine';
|
|
3329
|
+
|
|
3330
|
+
const bar = RenderEngine.askBar(document.getElementById('ask-bar'), {
|
|
3331
|
+
value: 'top 3 customers by revenue',
|
|
3332
|
+
// Either pass a custom suggestion callback (e.g. semanticengine's engine.suggest):
|
|
3333
|
+
suggest: (text) => semanticEngine.suggest(text, { sampler: (dim) => semanticEngine.members(dim) }),
|
|
3334
|
+
// Or pass declarative cube/table metadata for built-in next-word prediction:
|
|
3335
|
+
// measures: ['Revenue', 'Cost'],
|
|
3336
|
+
// dimensions: ['Region', 'Quarter'],
|
|
3337
|
+
// members: (dim) => [...],
|
|
3338
|
+
sampleQuestions: ['revenue by region', 'top 3 customers by revenue'],
|
|
3339
|
+
visibleCount: 5, // items visible at once in the scrollable list
|
|
3340
|
+
predictChoices: [0, 3, 5, 8, 10], // 0 = Off; user can switch in the bar's selector
|
|
3341
|
+
onAsk: (question) => runQuery(question),
|
|
3342
|
+
});
|
|
3343
|
+
```
|
|
3344
|
+
|
|
3345
|
+
- **Configurable scrollable next-word prediction:** `visibleCount` (default `5`) sets how many predicted items show at once before scrolling (`max-height` on `.re-ask-suggest-box` with `overflow-y: auto`), and `predictChoices` (default `[0, 3, 5, 8, 10]`) populates the **Predict next word** selector (`Off`, `3 visible (scrollable)`, `5 visible (scrollable)`, …). Set `showPredictControl: false` to hide the selector.
|
|
3346
|
+
- **Semantic badges:** each suggestion item (`{ display, completion, sense?, reference?, relation?, kind? }`) renders its `display` token, optional `sense → reference` subtitle, lexical `relation` badge (`synonym`, `hypernym`, `hyponym`, `antonym`), and `kind` badge (`metric`, `dimension`, `keyword`, `value`).
|
|
3347
|
+
- **Keyboard navigation:** nothing in the list is selected until you move into it, so `Enter` asks the question as typed. `↑` / `↓` move through the scrollable list, starting at the last or first item and scrolling the active item into view. `Enter` completes the selected item; `Tab` completes the selected item, or the first. Either one immediately predicts the following word. `Escape` closes the popup.
|
|
3348
|
+
- **Async sources and answers:** `suggest` can return a promise, for example from an engine that is still loading or a vocabulary on a server. The list keeps showing what it had until the promise settles. An answer for text the user has since changed is dropped, and a rejected promise shows no suggestions. `onAsk` can also return a promise of `{ rows, columns, complete }`, which `resultEngine` shows once it settles.
|
|
3349
|
+
- **For a host:** `bar.setSuggest(fn)` swaps where suggestions come from, for example when the user picks another model or ontology. `bar.setValue(text)` sets the question without suggesting. `bar.focus()` puts the cursor in the question, and `bar.destroy()` removes the bar and cancels any pending close.
|
|
3350
|
+
- **Accessible:** the input is a WAI-ARIA combobox over a `listbox` of `option`s. `aria-expanded` follows the popup and `aria-activedescendant` follows the active item. Each bar's list has its own id, so several bars can share a page.
|
|
3351
|
+
|
|
3285
3352
|
## Where to go next
|
|
3286
3353
|
|
|
3287
3354
|
- **`ARCHITECTURE.md`** — module layout, the chart-type catalog mechanics, how to add a new chart type.
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peter.prib/renderengine",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "ArchiMate modeller, diagram editor and chart library for the browser, with no runtime dependencies.",
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"description": "ArchiMate modeller, data modeller, diagram editor, OLAP cube & pivot chart studio, and 50+ type chart library for the browser, with no runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"module": "src/index.js",
|
|
8
8
|
"browser": "dist/renderengine.umd.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"renderengine": "./bin/renderengine.mjs"
|
|
11
|
+
},
|
|
9
12
|
"exports": {
|
|
10
13
|
".": "./src/index.js",
|
|
11
14
|
"./workspace": "./src/workspace/index.js",
|
|
@@ -14,6 +17,7 @@
|
|
|
14
17
|
"./package.json": "./package.json"
|
|
15
18
|
},
|
|
16
19
|
"files": [
|
|
20
|
+
"bin/*.mjs",
|
|
17
21
|
"src/",
|
|
18
22
|
"dist/*.umd.js",
|
|
19
23
|
"examples/archimate.html",
|
|
@@ -24,11 +28,12 @@
|
|
|
24
28
|
"LICENSE"
|
|
25
29
|
],
|
|
26
30
|
"scripts": {
|
|
31
|
+
"cli": "node bin/renderengine.mjs",
|
|
27
32
|
"start": "node scripts/open.mjs examples/archimate.html",
|
|
28
33
|
"archimate": "node scripts/open.mjs examples/archimate.html",
|
|
29
34
|
"diagram": "node scripts/open.mjs examples/diagram-editor.html",
|
|
30
35
|
"datamodel": "node scripts/open.mjs examples/data-model.html",
|
|
31
|
-
"test": "node --test --test-timeout=10000 ./test/*.js ./test/charting/*.js ./test/charting/charts/*.js ./test/charting/table/*.js ./test/charting/pivot/*.js ./test/charting/diagrams/*.js ./test/charting/shapes/*.js",
|
|
36
|
+
"test": "node --test --test-timeout=10000 ./test/*.js ./test/charting/*.js ./test/charting/charts/*.js ./test/charting/table/*.js ./test/charting/pivot/*.js ./test/charting/ask/*.js ./test/charting/diagrams/*.js ./test/charting/shapes/*.js",
|
|
32
37
|
"build": "rollup -c",
|
|
33
38
|
"prepublishOnly": "npm test && npm run build",
|
|
34
39
|
"screenshots": "node scripts/screenshots/screenshots.mjs"
|
|
@@ -38,6 +43,11 @@
|
|
|
38
43
|
"archi",
|
|
39
44
|
"diagram",
|
|
40
45
|
"diagram-editor",
|
|
46
|
+
"data-modelling",
|
|
47
|
+
"bian",
|
|
48
|
+
"olap",
|
|
49
|
+
"pivot",
|
|
50
|
+
"cube",
|
|
41
51
|
"modelling",
|
|
42
52
|
"charts",
|
|
43
53
|
"svg",
|
package/src/CanvasRenderer.js
CHANGED
|
@@ -1,201 +1,226 @@
|
|
|
1
|
-
import { BaseRenderer } from './BaseRenderer.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @class CanvasRenderer
|
|
5
|
-
* Renders shapes by drawing on a 2D canvas context.
|
|
6
|
-
*/
|
|
7
|
-
export class CanvasRenderer extends BaseRenderer {
|
|
8
|
-
get type() { return 'canvas'; }
|
|
9
|
-
get centerTextAnchor() { return 'center'; }
|
|
10
|
-
|
|
11
|
-
constructor(container, width, height) {
|
|
12
|
-
super(container, width, height);
|
|
13
|
-
|
|
14
|
-
this.canvas
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.canvas.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return p;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.ctx.stroke
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.ctx.
|
|
84
|
-
this.ctx.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this.ctx.
|
|
91
|
-
|
|
92
|
-
this.ctx.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
this.ctx.
|
|
114
|
-
this.ctx.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
this.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.
|
|
155
|
-
this.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (path2d ? this.ctx.
|
|
174
|
-
return shape;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const targetShape = this._shapes.find(s => s.id === shape.id);
|
|
191
|
-
if (targetShape) {
|
|
192
|
-
|
|
193
|
-
this._redraw();
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
1
|
+
import { BaseRenderer } from './BaseRenderer.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class CanvasRenderer
|
|
5
|
+
* Renders shapes by drawing on a 2D canvas context.
|
|
6
|
+
*/
|
|
7
|
+
export class CanvasRenderer extends BaseRenderer {
|
|
8
|
+
get type() { return 'canvas'; }
|
|
9
|
+
get centerTextAnchor() { return 'center'; }
|
|
10
|
+
|
|
11
|
+
constructor(container, width, height) {
|
|
12
|
+
super(container, width, height);
|
|
13
|
+
const doc = typeof document !== 'undefined' ? document : container?.ownerDocument;
|
|
14
|
+
this.canvas = doc?.createElement ? doc.createElement('canvas') : (container?.canvas || {
|
|
15
|
+
style: {}, width, height,
|
|
16
|
+
getContext: () => ({ fillRect: () => {}, measureText: () => ({ width: 10 }), clearRect: () => {}, beginPath: () => {}, arc: () => {}, fill: () => {}, stroke: () => {} }),
|
|
17
|
+
});
|
|
18
|
+
this.canvas.width = width;
|
|
19
|
+
this.canvas.height = height;
|
|
20
|
+
// Inline elements (the default for <canvas>) leave a few px of baseline
|
|
21
|
+
// whitespace below them inside a block container, making the
|
|
22
|
+
// container's scrollHeight exceed its clientHeight for every chart.
|
|
23
|
+
if (this.canvas?.style) this.canvas.style.display = 'block';
|
|
24
|
+
this._shapes = [];
|
|
25
|
+
this.container?.appendChild?.(this.canvas);
|
|
26
|
+
this.ctx = this.canvas?.getContext?.('2d') || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resize the canvas. Setting a canvas's size clears it, so redraw after. */
|
|
30
|
+
resize(width, height) {
|
|
31
|
+
super.resize(width, height);
|
|
32
|
+
this.canvas.width = width;
|
|
33
|
+
this.canvas.height = height;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
clear() {
|
|
37
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
38
|
+
this._shapes = [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Always returns an isolated Path2D — used by hitTest() to test one
|
|
42
|
+
// shape's geometry at a time. Previously rect/circle called this.ctx.rect()/
|
|
43
|
+
// .arc() directly onto the canvas's own current path without resetting it
|
|
44
|
+
// first, so each hitTest() loop iteration kept accumulating geometry from
|
|
45
|
+
// every shape checked before it, corrupting hit results for anything but
|
|
46
|
+
// the very first shape tested (tooltips/clicks/hover on Canvas charts
|
|
47
|
+
// could resolve to the wrong shape).
|
|
48
|
+
_drawPath(shape) {
|
|
49
|
+
const p = new Path2D();
|
|
50
|
+
switch (shape.type) {
|
|
51
|
+
case 'rect':
|
|
52
|
+
p.rect(shape.x, shape.y, shape.width, shape.height);
|
|
53
|
+
return p;
|
|
54
|
+
case 'circle':
|
|
55
|
+
p.arc(shape.cx, shape.cy, shape.r, 0, 2 * Math.PI);
|
|
56
|
+
return p;
|
|
57
|
+
case 'path':
|
|
58
|
+
return new Path2D(shape.d);
|
|
59
|
+
default:
|
|
60
|
+
return p; // empty — no geometry to hit-test
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
_setShapeType={
|
|
64
|
+
rect:(shape)=>this.ctx.rect(shape.x, shape.y, shape.width, shape.height),
|
|
65
|
+
circle:(shape)=>this.ctx.arc(shape.cx, shape.cy, shape.r, 0, 2 * Math.PI),
|
|
66
|
+
path:(shape,fill,stroke)=>{
|
|
67
|
+
const p = new Path2D(shape.d);
|
|
68
|
+
if (fill && fill !== 'none') { this.ctx.fillStyle = fill; this.ctx.fill(p); }
|
|
69
|
+
if (stroke && stroke.color !== 'none') {
|
|
70
|
+
this.ctx.strokeStyle = stroke.color;
|
|
71
|
+
this.ctx.lineWidth = stroke.width || 1;
|
|
72
|
+
// Used by the gauge arcs' rounded end-caps ({cap: 'round'}).
|
|
73
|
+
this.ctx.lineCap = stroke.cap || 'butt';
|
|
74
|
+
this.ctx.stroke(p);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Shared by text() (initial draw) and _draw()'s 'text' case (redraw on
|
|
79
|
+
// hover enter/leave) so rotation/alignment logic isn't duplicated.
|
|
80
|
+
_paintText(shape) {
|
|
81
|
+
const hoverStyle = shape._hoverStyle;
|
|
82
|
+
const fill = hoverStyle?.fill ?? shape.fill;
|
|
83
|
+
this.ctx.fillStyle = fill || 'black';
|
|
84
|
+
this.ctx.font = `${shape.fontStyle || ''} ${shape.fontWeight || ''} ${shape.fontSize || '12px'} ${shape.fontFamily || 'sans-serif'}`.trim().replace(/\s+/g, ' ');
|
|
85
|
+
let textAlign = shape.textAnchor || 'start';
|
|
86
|
+
if (textAlign === 'middle') textAlign = 'center';
|
|
87
|
+
this.ctx.textAlign = textAlign;
|
|
88
|
+
this.ctx.textBaseline = 'middle';
|
|
89
|
+
if (shape.rotation) {
|
|
90
|
+
this.ctx.save();
|
|
91
|
+
this.ctx.translate(shape.x, shape.y);
|
|
92
|
+
this.ctx.rotate(shape.rotation);
|
|
93
|
+
this.ctx.fillText(shape.text, 0, 0);
|
|
94
|
+
this.ctx.restore();
|
|
95
|
+
} else {
|
|
96
|
+
this.ctx.fillText(shape.text, shape.x, shape.y);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
_draw(shape) {
|
|
100
|
+
if (shape.type === 'text') { this._paintText(shape); return; }
|
|
101
|
+
|
|
102
|
+
const hoverStyle = shape._hoverStyle;
|
|
103
|
+
const fill = hoverStyle?.fill ?? shape.fill;
|
|
104
|
+
const stroke = hoverStyle?.stroke ?? shape.stroke;
|
|
105
|
+
|
|
106
|
+
// For path, we need to handle fill and stroke separately inside the case.
|
|
107
|
+
const isPath = shape.type === 'path';
|
|
108
|
+
|
|
109
|
+
this.ctx.beginPath();
|
|
110
|
+
this._setShapeType[shape.type](shape, fill, stroke);
|
|
111
|
+
if (isPath) return
|
|
112
|
+
if (fill && fill !== 'none') {
|
|
113
|
+
this.ctx.fillStyle = fill;
|
|
114
|
+
this.ctx.fill();
|
|
115
|
+
}
|
|
116
|
+
if (stroke) {
|
|
117
|
+
this.ctx.strokeStyle = stroke.color || 'none';
|
|
118
|
+
this.ctx.lineWidth = stroke.width || 1;
|
|
119
|
+
this.ctx.stroke();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_redraw() {
|
|
124
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
125
|
+
this._shapes.forEach(shape => this._draw(shape));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
rect(x, y, width, height, fill, stroke, extra = {}) {
|
|
129
|
+
const shape = { type: 'rect', x, y, width, height, fill, stroke, id: `shape_${this._shapes.length}`, ...extra };
|
|
130
|
+
this._shapes.push(shape);
|
|
131
|
+
this._draw(shape);
|
|
132
|
+
return shape;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
circle(cx, cy, r, fill, stroke, extra = {}) {
|
|
136
|
+
const shape = { type: 'circle', cx, cy, r, fill, stroke, id: `shape_${this._shapes.length}`, ...extra };
|
|
137
|
+
this._shapes.push(shape);
|
|
138
|
+
this._draw(shape);
|
|
139
|
+
return shape;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
path(d, fill, stroke, extra = {}) {
|
|
143
|
+
const shape = { type: 'path', d, fill, stroke, id: `shape_${this._shapes.length}`, ...extra };
|
|
144
|
+
this._shapes.push(shape);
|
|
145
|
+
this._draw(shape);
|
|
146
|
+
return shape;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Text width measured by this renderer's own 2D context. */
|
|
150
|
+
measureText(text, font = {}) {
|
|
151
|
+
if (!this.ctx?.measureText) return super.measureText(text, font);
|
|
152
|
+
this.ctx.save();
|
|
153
|
+
this.ctx.font = BaseRenderer.cssFont(font);
|
|
154
|
+
const width = this.ctx.measureText(String(text)).width;
|
|
155
|
+
this.ctx.restore();
|
|
156
|
+
return width;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
text(text, x, y, options = {}) {
|
|
160
|
+
const shape = { type: 'text', text, x, y, ...options, id: `shape_${this._shapes.length}` };
|
|
161
|
+
this._shapes.push(shape);
|
|
162
|
+
this._paintText(shape);
|
|
163
|
+
return shape;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
hitTest(x, y) {
|
|
167
|
+
for (let i = this._shapes.length - 1; i >= 0; i--) {
|
|
168
|
+
const shape = this._shapes[i];
|
|
169
|
+
const path2d = this._drawPath(shape);
|
|
170
|
+
|
|
171
|
+
// 1. Check for a fill hit first.
|
|
172
|
+
if (shape.fill && shape.fill !== 'none') {
|
|
173
|
+
if (path2d ? this.ctx.isPointInPath(path2d, x, y) : this.ctx.isPointInPath(x, y)) {
|
|
174
|
+
return shape;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 2. If no fill hit, check for a stroke hit.
|
|
179
|
+
if (shape.stroke && shape.stroke.color !== 'none') {
|
|
180
|
+
this.ctx.lineWidth = shape.stroke.width || 1;
|
|
181
|
+
if (path2d ? this.ctx.isPointInStroke(path2d, x, y) : this.ctx.isPointInStroke(x, y)) {
|
|
182
|
+
return shape;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
enterState(shape, stateName) {
|
|
190
|
+
const targetShape = this._shapes.find(s => s.id === shape.id);
|
|
191
|
+
if (targetShape) {
|
|
192
|
+
targetShape._hoverStyle = { fill: 'orange', stroke: { color: '#333', width: 2 } };
|
|
193
|
+
this._redraw();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
leaveState(shape, stateName) {
|
|
198
|
+
const targetShape = this._shapes.find(s => s.id === shape.id);
|
|
199
|
+
if (targetShape) {
|
|
200
|
+
delete targetShape._hoverStyle;
|
|
201
|
+
this._redraw();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
renderDiagram(diagram) {
|
|
206
|
+
// Draw the diagram directly onto the canvas context.
|
|
207
|
+
diagram.drawOn(this.ctx);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a linear gradient on this canvas context.
|
|
212
|
+
* @param {string} _id
|
|
213
|
+
* @param {Array<{offset: string|number, color: string}>} stops
|
|
214
|
+
* @param {{x0?: number, y0?: number, x1?: number, y1?: number}} [coords]
|
|
215
|
+
* @returns {CanvasGradient|null}
|
|
216
|
+
*/
|
|
217
|
+
linearGradient(_id, stops = [], { x0 = 0, y0 = 0, x1 = 0, y1 = 100 } = {}) {
|
|
218
|
+
if (!this.ctx?.createLinearGradient) return null;
|
|
219
|
+
const grad = this.ctx.createLinearGradient(x0, y0, x1, y1);
|
|
220
|
+
for (const stop of stops) {
|
|
221
|
+
const offset = typeof stop.offset === 'number' ? stop.offset : (parseFloat(stop.offset) || 0) / 100;
|
|
222
|
+
grad.addColorStop(offset, stop.color);
|
|
223
|
+
}
|
|
224
|
+
return grad;
|
|
225
|
+
}
|
|
201
226
|
}
|