@peter.prib/renderengine 0.12.0 → 0.14.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.
Files changed (67) hide show
  1. package/README.md +454 -296
  2. package/dist/renderengine-workspace.umd.js +4122 -1167
  3. package/dist/renderengine.umd.js +4103 -1164
  4. package/documentation/REFERENCE.md +96 -6
  5. package/package.json +8 -3
  6. package/src/chartCatalog.js +50 -3
  7. package/src/charting/Axis.js +47 -5
  8. package/src/charting/Chart3D.js +4 -2
  9. package/src/charting/DiagramEditor.js +239 -34
  10. package/src/charting/Hierarchy.js +28 -0
  11. package/src/charting/ModelFileFormat.js +47 -0
  12. package/src/charting/archimate/ArchimateCanvasEditor.js +1108 -1012
  13. package/src/charting/archimate/deleteFromModel.js +57 -0
  14. package/src/charting/archimate/elementMenuItems.js +3 -1
  15. package/src/charting/archimate/heatMaps.js +169 -0
  16. package/src/charting/archimate/index.js +1 -0
  17. package/src/charting/ask/AskBar.js +388 -0
  18. package/src/charting/ask/AskView.js +400 -0
  19. package/src/charting/charts/AreaChart.js +20 -3
  20. package/src/charting/charts/BarChart.js +5 -5
  21. package/src/charting/charts/BarChart3D.js +2 -2
  22. package/src/charting/charts/BarChart3DOrbit.js +3 -1
  23. package/src/charting/charts/BarChartBase.js +21 -3
  24. package/src/charting/charts/BoxPlotChart.js +2 -2
  25. package/src/charting/charts/BubbleChart.js +4 -1
  26. package/src/charting/charts/ChordDiagram.js +4 -4
  27. package/src/charting/charts/DensityChart.js +1 -1
  28. package/src/charting/charts/FunnelChart.js +1 -1
  29. package/src/charting/charts/GroupedBarChart.js +7 -9
  30. package/src/charting/charts/HeatmapChart.js +4 -1
  31. package/src/charting/charts/HistogramChart.js +2 -2
  32. package/src/charting/charts/IcicleChart.js +47 -0
  33. package/src/charting/charts/LollipopChart.js +104 -0
  34. package/src/charting/charts/MarimekkoChart.js +75 -0
  35. package/src/charting/charts/OutlineChart.js +10 -8
  36. package/src/charting/charts/ParallelCoordinatesChart.js +2 -2
  37. package/src/charting/charts/ParetoChart.js +92 -0
  38. package/src/charting/charts/PipeChart.js +5 -2
  39. package/src/charting/charts/PopulationPyramidChart.js +4 -4
  40. package/src/charting/charts/RadarChart.js +5 -4
  41. package/src/charting/charts/RadialBarChart.js +2 -2
  42. package/src/charting/charts/SankeyDiagram.js +4 -4
  43. package/src/charting/charts/ScatterChart.js +6 -3
  44. package/src/charting/charts/StackedBarChart.js +82 -11
  45. package/src/charting/charts/StackedBarChart3DOrbit.js +5 -2
  46. package/src/charting/charts/StreamGraphChart.js +8 -0
  47. package/src/charting/charts/SunburstChart.js +2 -2
  48. package/src/charting/charts/TableChart.js +18 -2
  49. package/src/charting/charts/TreemapChart.js +4 -4
  50. package/src/charting/charts/ViolinChart.js +2 -2
  51. package/src/charting/charts/WaterfallChart.js +2 -2
  52. package/src/charting/charts/WordCloudChart.js +2 -2
  53. package/src/charting/diagrams/ArchimateModel.js +33 -0
  54. package/src/charting/diagrams/DeleteFromModelCommand.js +81 -0
  55. package/src/charting/diagrams/EdgeGeometry.js +16 -0
  56. package/src/charting/edgeStyle/EdgeStyleSchema.js +6 -2
  57. package/src/charting/pivot/PivotChart.js +205 -13
  58. package/src/charting/pivot/PivotGridChart.js +1 -1
  59. package/src/charting/pivot/PivotView.js +555 -23
  60. package/src/charting/table/TableView.js +10 -1
  61. package/src/charting/table/TableViewChart.js +1 -1
  62. package/src/charting/templates/Template.js +10 -0
  63. package/src/exportImage.js +45 -18
  64. package/src/index.js +115 -6
  65. package/src/workspace/parts/ArchimateCanvasPart.js +3 -0
  66. package/src/workspace/parts/DiagramEditorPart.js +2 -0
  67. package/src/workspace/parts/ModelHosted.js +15 -4
@@ -1,4 +1,5 @@
1
1
  import { ChartChooser } from '../ChartChooser.js';
2
+ import { TableChart } from '../charts/TableChart.js';
2
3
 
3
4
  /**
4
5
  * TableView — the state of an interactive table over result rows, kept apart from drawing so all of
@@ -52,8 +53,10 @@ export class TableView {
52
53
  * @param {function} [options.copy] writes text to the clipboard (navigator.clipboard by default)
53
54
  * @param {ChartChooser} [options.chooser]
54
55
  * @param {function} [options.highlight] row → true for a row drawn highlighted (a host's selection)
56
+ * @param {function|Intl.NumberFormat|object} [options.format] how numbers show, for every column or per
57
+ * column (`TableChart.formatFor`). A copy keeps the raw numbers.
55
58
  */
56
- constructor(rows, { columns = null, complete = true, pageSize = 50, onChange = null, menus = {}, copy = null, chooser = null, highlight = null } = {}) {
59
+ constructor(rows, { columns = null, complete = true, pageSize = 50, onChange = null, menus = {}, copy = null, chooser = null, highlight = null, format = null } = {}) {
57
60
  this.rows = rows;
58
61
  this.columns = ChartChooser.describe(rows, columns);
59
62
  this.complete = complete;
@@ -63,6 +66,7 @@ export class TableView {
63
66
  this.copy = copy ?? TableView.clipboard;
64
67
  this.chooser = chooser ?? new ChartChooser();
65
68
  this.highlight = highlight;
69
+ this.format = format;
66
70
  /** The sort keys, most significant first: `{ column, direction: 'asc' | 'desc' }`. */
67
71
  this.sorts = [];
68
72
  this.filters = []; // { column, op, value }
@@ -70,6 +74,11 @@ export class TableView {
70
74
  this.page = 0;
71
75
  }
72
76
 
77
+ /** A column's value as the table shows it, in the column's `format` when there is one. */
78
+ valueText(column, value, nullText = '—') {
79
+ return TableChart.cellText(value, nullText, TableChart.formatFor(this.format, column));
80
+ }
81
+
73
82
  static clipboard(text) {
74
83
  return globalThis.navigator?.clipboard?.writeText(text);
75
84
  }
@@ -82,7 +82,7 @@ export class TableViewChart extends TableChart {
82
82
  columns.forEach((c, i) => {
83
83
  const left = x0 + i * colW;
84
84
  const hit = this.renderer.rect(left, top, colW, ROW_H, 'transparent', null);
85
- const text = this._cell(TableChart.cellText(row[c.key], nullText), c, left, top, colW, { fill: row[c.key] == null ? '#999' : '#222' });
85
+ const text = this._cell(view.valueText(c.key, row[c.key], nullText), c, left, top, colW, { fill: row[c.key] == null ? '#999' : '#222' });
86
86
  this._on(interactive, [hit, text], (e) => this.showMenu(e.clientX, e.clientY, view.cellMenu(row, c.key)), null);
87
87
  });
88
88
  top += ROW_H;
@@ -102,6 +102,16 @@ export class Template {
102
102
  return { x: this.x + ux * t, y: this.y + uy * t };
103
103
  }
104
104
 
105
+ /**
106
+ * Where a line from (px, py), a point inside the shape, heading in
107
+ * direction (dx, dy) leaves its outline (the shape unrotated).
108
+ * @returns {{x: number, y: number}}
109
+ */
110
+ exitPoint(px, py, dx, dy) {
111
+ const t = rayExitDistance(this.outline(), px, py, dx, dy);
112
+ return { x: px + dx * t, y: py + dy * t };
113
+ }
114
+
105
115
  /** Centre of the name text; the z-index line sits just below it. */
106
116
  get labelAnchor() { return { x: this.x, y: this.y - 8 }; }
107
117
 
@@ -31,10 +31,10 @@ export const MAX_IMAGE_PIXELS = 32_000_000;
31
31
  /**
32
32
  * Work out the bitmap size to rasterize at.
33
33
  *
34
- * `scale` is what the caller asked for (2 = retina-sharp). It is reduced, never
35
- * the diagram's own dimensions, when the result would be too large: halving the
36
- * scale of a huge diagram still shows all of it, whereas cropping or shrinking
37
- * the diagram loses content or legibility.
34
+ * `scale` is what the caller asked for (1 = 100%, 2 = retina-sharp, 0.5 = half
35
+ * size). It is reduced, never the diagram's own dimensions, when the result
36
+ * would be too large: halving the scale of a huge diagram still shows all of
37
+ * it, whereas cropping or shrinking the diagram loses content or legibility.
38
38
  *
39
39
  * @returns {{width:number, height:number, scale:number, clamped:boolean}}
40
40
  */
@@ -49,10 +49,6 @@ export function imageSize({ width, height, scale = 2, maxPixels = MAX_IMAGE_PIXE
49
49
  s = Math.sqrt(maxPixels / (w * h));
50
50
  clamped = true;
51
51
  }
52
- // A scale below 1:1 would be a downscale the caller never asked for, but a
53
- // diagram that is itself over the budget leaves no choice.
54
- if (s < 1 && w * h <= maxPixels) s = 1;
55
-
56
52
  return { width: Math.max(1, Math.round(w * s)), height: Math.max(1, Math.round(h * s)), scale: s, clamped };
57
53
  }
58
54
 
@@ -67,8 +63,10 @@ export function imageSize({ width, height, scale = 2, maxPixels = MAX_IMAGE_PIXE
67
63
  * @param {number} [options.width] pixels; omitted leaves what is there
68
64
  * @param {number} [options.height]
69
65
  * @param {string|null} [options.background='#ffffff'] null for transparent
66
+ * @param {string} [options.viewBox] the drawing's own units, when it has none
67
+ * (so a picture sized other than as drawn scales rather than crops)
70
68
  */
71
- export function prepareSvgMarkup(markup, { width, height, background = '#ffffff' } = {}) {
69
+ export function prepareSvgMarkup(markup, { width, height, background = '#ffffff', viewBox } = {}) {
72
70
  let out = String(markup ?? '').trim();
73
71
  const open = out.match(/^<svg\b[^>]*>/i);
74
72
  if (!open) return out; // not an <svg> — hand it back untouched
@@ -87,13 +85,23 @@ export function prepareSvgMarkup(markup, { width, height, background = '#ffffff'
87
85
  };
88
86
  if (Number.isFinite(width) && width > 0) setAttr('width', Math.round(width));
89
87
  if (Number.isFinite(height) && height > 0) setAttr('height', Math.round(height));
88
+ if (viewBox && !/\sviewBox\s*=/i.test(tag)) setAttr('viewBox', viewBox);
90
89
 
91
90
  out = tag + out.slice(open[0].length);
92
91
 
93
92
  if (background) {
94
- // First child, so it sits behind everything. Percentages rather than
95
- // pixels so it still covers the drawing if a viewBox rescales it.
96
- out = out.replace(tag, `${tag}<rect x="0" y="0" width="100%" height="100%" fill="${background}"/>`);
93
+ // First child, so it sits behind everything. With a viewBox it reaches
94
+ // a whole viewBox past each side (the picture clips it): a percentage
95
+ // counts from (0,0), so a viewBox starting elsewhere (a cropped
96
+ // drawing) would leave a strip bare, and the viewBox alone would leave
97
+ // the bands a picture of another shape letterboxes it with.
98
+ // Without one, percentages, so it covers whatever size it is shown at.
99
+ const box = tag.match(/\sviewBox\s*=\s*["']\s*([-\d.e+]+)[\s,]+([-\d.e+]+)[\s,]+([-\d.e+]+)[\s,]+([-\d.e+]+)\s*["']/i);
100
+ const [x, y, w, h] = box ? box.slice(1).map(Number) : [];
101
+ const rect = box
102
+ ? `<rect x="${x - w}" y="${y - h}" width="${3 * w}" height="${3 * h}" fill="${background}"/>`
103
+ : `<rect x="0" y="0" width="100%" height="100%" fill="${background}"/>`;
104
+ out = out.replace(tag, `${tag}${rect}`);
97
105
  }
98
106
  return out;
99
107
  }
@@ -126,12 +134,12 @@ export function findDrawable(host) {
126
134
  * Size is taken from the element's laid-out box rather than its attributes,
127
135
  * because that is what the user is actually looking at.
128
136
  */
129
- export function serializeSvg(svgEl, { background = '#ffffff', width, height } = {}) {
137
+ export function serializeSvg(svgEl, { background = '#ffffff', width, height, scale = 1 } = {}) {
130
138
  const box = svgEl.getBoundingClientRect?.() ?? {};
131
139
  const w = width ?? svgEl.width?.baseVal?.value ?? box.width;
132
140
  const h = height ?? svgEl.height?.baseVal?.value ?? box.height;
133
141
  const markup = new XMLSerializer().serializeToString(svgEl);
134
- return prepareSvgMarkup(markup, { width: w, height: h, background });
142
+ return prepareSvgMarkup(markup, { width: w * scale, height: h * scale, background, viewBox: `0 0 ${w} ${h}` });
135
143
  }
136
144
 
137
145
  /**
@@ -150,9 +158,16 @@ export async function toPngBlob(host, { scale = 2, background = '#ffffff', maxPi
150
158
  }
151
159
 
152
160
  const box = drawable.getBoundingClientRect();
153
- const size = imageSize({ width: box.width, height: box.height, scale, maxPixels });
154
161
  const markup = serializeSvg(drawable, { background, width: box.width, height: box.height });
162
+ return svgMarkupToPngBlob(markup, { width: box.width, height: box.height, scale, background, maxPixels });
163
+ }
155
164
 
165
+ /**
166
+ * Rasterize standalone SVG markup (prepareSvgMarkup's) drawn `width` × `height`
167
+ * to a PNG Blob at `scale`. DOM only.
168
+ */
169
+ export async function svgMarkupToPngBlob(markup, { width, height, scale = 1, background = '#ffffff', maxPixels = MAX_IMAGE_PIXELS } = {}) {
170
+ const size = imageSize({ width, height, scale, maxPixels });
156
171
  // A data: URL rather than a blob: URL — a blob: URL taints nothing either,
157
172
  // but Safari has historically refused to draw SVG from one.
158
173
  const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`;
@@ -200,7 +215,15 @@ export function downloadBlob(blob, filename) {
200
215
  * @returns {Promise<{via:'clipboard'|'download', filename?:string}>}
201
216
  */
202
217
  export async function copyImage(host, { scale = 2, background = '#ffffff', name = 'diagram' } = {}) {
203
- const blob = await toPngBlob(host, { scale, background });
218
+ return copyPngBlob(await toPngBlob(host, { scale, background }), { name });
219
+ }
220
+
221
+ /**
222
+ * Put a PNG Blob on the clipboard, or download it where the browser cannot
223
+ * (as copyImage). DOM only.
224
+ * @returns {Promise<{via:'clipboard'|'download', filename?:string}>}
225
+ */
226
+ export async function copyPngBlob(blob, { name = 'diagram' } = {}) {
204
227
  const canWrite = typeof ClipboardItem !== 'undefined'
205
228
  && typeof navigator !== 'undefined'
206
229
  && navigator.clipboard?.write;
@@ -220,10 +243,14 @@ export async function copyImage(host, { scale = 2, background = '#ffffff', name
220
243
  }
221
244
 
222
245
  /** Save a diagram as an SVG file. DOM only. */
223
- export function downloadSvg(host, { background = '#ffffff', name = 'diagram' } = {}) {
246
+ export function downloadSvg(host, { background = '#ffffff', name = 'diagram', scale = 1 } = {}) {
224
247
  const drawable = findDrawable(host);
225
248
  if (!drawable || drawable.tagName.toLowerCase() !== 'svg') throw new Error('No <svg> to export');
226
- const markup = serializeSvg(drawable, { background });
249
+ downloadSvgMarkup(serializeSvg(drawable, { background, scale }), { name });
250
+ }
251
+
252
+ /** Save standalone SVG markup as a file. DOM only. */
253
+ export function downloadSvgMarkup(markup, { name = 'diagram' } = {}) {
227
254
  downloadBlob(new Blob([markup], { type: 'image/svg+xml;charset=utf-8' }), imageFileName(name, 'svg'));
228
255
  }
229
256
 
package/src/index.js CHANGED
@@ -33,6 +33,10 @@ import { LagPlotChart } from './charting/charts/LagPlotChart.js';
33
33
  import { TreemapChart } from './charting/charts/TreemapChart.js';
34
34
  import { SunburstChart } from './charting/charts/SunburstChart.js';
35
35
  import { OutlineChart } from './charting/charts/OutlineChart.js';
36
+ import { IcicleChart } from './charting/charts/IcicleChart.js';
37
+ import { ParetoChart } from './charting/charts/ParetoChart.js';
38
+ import { LollipopChart } from './charting/charts/LollipopChart.js';
39
+ import { MarimekkoChart } from './charting/charts/MarimekkoChart.js';
36
40
  import { TableChart } from './charting/charts/TableChart.js';
37
41
  import { ChartChooser } from './charting/ChartChooser.js';
38
42
  import { TableView } from './charting/table/TableView.js';
@@ -40,6 +44,8 @@ import { TableViewChart } from './charting/table/TableViewChart.js';
40
44
  import { PivotView } from './charting/pivot/PivotView.js';
41
45
  import { PivotGridChart } from './charting/pivot/PivotGridChart.js';
42
46
  import { PivotChart } from './charting/pivot/PivotChart.js';
47
+ import { AskView } from './charting/ask/AskView.js';
48
+ import { AskBar } from './charting/ask/AskBar.js';
43
49
  import { RadarChart } from './charting/charts/RadarChart.js';
44
50
  import { FunnelChart } from './charting/charts/FunnelChart.js';
45
51
  import { WaterfallChart } from './charting/charts/WaterfallChart.js';
@@ -188,13 +194,18 @@ import {
188
194
  parseXML, parseArchiModel, toRenderEngineModel, toArchimateModel, toArchiXML, fromArchimateView, toArchiExport,
189
195
  } from './archimateArchiFormat.js';
190
196
  import { parseExchangeModel, toExchangeXML, EXCHANGE_NAMESPACE, EXCHANGE_TYPE_PROPERTY, EXCHANGE_VIEWPOINTS } from './archimateExchangeFormat.js';
191
- import { ModelFileFormat, modelFileMenuItems, importFile } from './charting/ModelFileFormat.js';
197
+ import { ModelFileFormat, modelFileMenuItems, importFile, findPayload, askPasteImport } from './charting/ModelFileFormat.js';
192
198
  import { ArchiFileFormat, ExchangeFileFormat, ArchimateCsvFileFormat, ARCHIMATE_FILE_FORMATS } from './charting/archimate/index.js';
193
199
  import { ShowElementCommand } from './charting/diagrams/ShowElementCommand.js';
194
200
  import { EmphasisRule, EMPHASIS_OPACITY, EMPHASIS_LEVELS, strongerEmphasis } from './charting/emphasis/EmphasisRule.js';
195
201
  import { HighlightRule } from './charting/emphasis/HighlightRule.js';
196
202
  import { CatalogDialog } from './charting/CatalogDialog.js';
197
- import { CustomViewpoints, CUSTOM_VIEWPOINTS_STORE, specializationCatalogDialog, elementTypeChoices } from './charting/archimate/index.js';
203
+ import { CustomViewpoints, CUSTOM_VIEWPOINTS_STORE, specializationCatalogDialog, elementTypeChoices, askDeleteFromModel, deleteFromModelMessage } from './charting/archimate/index.js';
204
+ import { DeleteFromModelCommand } from './charting/diagrams/DeleteFromModelCommand.js';
205
+ import {
206
+ CHANGE_COLOURS, CHANGE_MAKERS, CHANGE_WORDS, changeAction, relationshipChange, elementChange,
207
+ propertyParams, expressionValues, resultBands, sequentialColours,
208
+ } from './charting/archimate/heatMaps.js';
198
209
  import { ViewpointRule, ElementFilterRule } from './charting/archimate/index.js';
199
210
  import { MODEL_ELEMENT_MIME, TREE_LAYERS, treeLayerOf } from './charting/modelTree/ModelTreePanel.js';
200
211
  import { parseArchiFile, toArchiFile, ARCHI_MODEL_ENTRY } from './archimateArchive.js';
@@ -207,6 +218,7 @@ import {
207
218
  // reference sits inside a function body, so loading it under node does nothing.
208
219
  import {
209
220
  copyImage, toPngBlob, downloadPng, downloadSvg, downloadBlob,
221
+ svgMarkupToPngBlob, copyPngBlob, downloadSvgMarkup,
210
222
  serializeSvg, prepareSvgMarkup, imageSize, imageFileName, findDrawable, MAX_IMAGE_PIXELS,
211
223
  } from './exportImage.js';
212
224
  // Same reasoning as exportImage: every DOM reference sits inside a function
@@ -222,7 +234,8 @@ import {
222
234
  // separate browser-only entry: import 'renderengine/src/workspace.js' directly.
223
235
 
224
236
  export {
225
- Mat4, OrbitCamera, CHART_CATALOG, getChartCatalogEntry, chartRegistry, ChartChooser, TableChart, TableView, TableViewChart, PivotView, PivotGridChart, PivotChart,
237
+ Mat4, OrbitCamera, CHART_CATALOG, getChartCatalogEntry, chartRegistry, ChartChooser, TableChart, TableView, TableViewChart, PivotView, PivotGridChart, PivotChart, AskView, AskBar,
238
+ ParetoChart, LollipopChart, MarimekkoChart, IcicleChart,
226
239
  DIAGRAM_CATALOG, getDiagramCatalogEntry, diagramRegistry,
227
240
  TREE_MODE_CATALOG, treeModeRegistry, getTreeModeCatalogEntry,
228
241
  AXIS_KIND, ENCODING_CHANNEL, COORD_SYSTEM, COLOR_MODE, CAMERA_MODE,
@@ -249,12 +262,16 @@ export {
249
262
  importElementsCSV, importRelationshipsCSV, importModelCSV,
250
263
  parseArchiModel, toArchimateModel, fromArchimateView, toArchiExport,
251
264
  parseExchangeModel, toExchangeXML, EXCHANGE_NAMESPACE, EXCHANGE_TYPE_PROPERTY, EXCHANGE_VIEWPOINTS,
252
- ModelFileFormat, modelFileMenuItems, importFile, ArchiFileFormat, ExchangeFileFormat, ArchimateCsvFileFormat, ARCHIMATE_FILE_FORMATS,
265
+ ModelFileFormat, modelFileMenuItems, importFile, findPayload, askPasteImport, ArchiFileFormat, ExchangeFileFormat, ArchimateCsvFileFormat, ARCHIMATE_FILE_FORMATS,
253
266
  ShowElementCommand, MODEL_ELEMENT_MIME, TREE_LAYERS, treeLayerOf,
254
267
  EmphasisRule, EMPHASIS_OPACITY, EMPHASIS_LEVELS, strongerEmphasis, HighlightRule, ViewpointRule, ElementFilterRule,
255
268
  CatalogDialog, CustomViewpoints, CUSTOM_VIEWPOINTS_STORE, specializationCatalogDialog, elementTypeChoices,
269
+ askDeleteFromModel, deleteFromModelMessage, DeleteFromModelCommand,
270
+ CHANGE_COLOURS, CHANGE_MAKERS, CHANGE_WORDS, changeAction, relationshipChange, elementChange,
271
+ propertyParams, expressionValues, resultBands, sequentialColours,
256
272
  parseArchiFile, toArchiFile, ARCHI_MODEL_ENTRY, ZipArchive,
257
273
  copyImage, toPngBlob, downloadPng, downloadSvg, downloadBlob,
274
+ svgMarkupToPngBlob, copyPngBlob, downloadSvgMarkup,
258
275
  serializeSvg, prepareSvgMarkup, imageSize, imageFileName, findDrawable, MAX_IMAGE_PIXELS,
259
276
  showPropertiesDialog, showDiagramPropertiesDialog, showGlobalSettingsDialog,
260
277
  propertyRowsFor, coerceFieldValue, derivedRefreshPlan, LAYER_CHOICES, NODE_SECTIONS,
@@ -1460,6 +1477,8 @@ export class RenderEngine {
1460
1477
  * @param {string} [options.title]
1461
1478
  * @param {number} [options.chartHeight] a chart's height (360)
1462
1479
  * @param {boolean} [options.fitHeight] size the container to what is drawn (true)
1480
+ * @param {function|Intl.NumberFormat|object} [options.format] how numbers show: for every column, or
1481
+ * per column ({ Amount: Intl.NumberFormat }); copies keep the raw numbers
1463
1482
  * @returns {TableView|null} the table's state, for the host to read or change
1464
1483
  */
1465
1484
  resultView(rows, options = {}) {
@@ -1488,6 +1507,39 @@ export class RenderEngine {
1488
1507
  const back = this.renderer.text('◂ Table', 8, 14, { fill: '#2b5fd9', fontSize: '12px', verticalAlign: 'middle' });
1489
1508
  back?.addEventListener?.('click', () => this._drawResultTable(view, options));
1490
1509
  if (back?.style) back.style.cursor = 'pointer';
1510
+ const choices = view.chartChoices();
1511
+ if (choices.length > 1) {
1512
+ const pick = this.renderer.text(`${choice.label} ▾`, 68, 14, { fill: '#2b5fd9', fontSize: '12px', verticalAlign: 'middle', tooltip: choice.reason });
1513
+ const show = options.showMenu ?? ((x, y, items) => PopupMenu.show(x, y, items));
1514
+ pick?.addEventListener?.('click', (e) => show(e.clientX, e.clientY, choices.map(c => ({
1515
+ label: `${c.label}${c.key === choice.key ? ' ✓' : ''}`,
1516
+ action: () => this._drawResultChart(view, c, options),
1517
+ }))));
1518
+ if (pick?.style) pick.style.cursor = 'pointer';
1519
+ }
1520
+ }
1521
+
1522
+ /**
1523
+ * Mounts an interactive `AskBar` (question input, configurable next-word prediction in a
1524
+ * scrollable list, semantic sense/reference/relation badges, and sample query pills) into
1525
+ * `container` (or links `resultEngine: this` when called as `engine.askBar(container, options)`).
1526
+ * @param {HTMLElement|string|object} containerOrOptions host container element/id, or options when mounting into `this.container`
1527
+ * @param {object} [options] `AskBar` / `AskView` options (`suggest`, `measures`, `dimensions`, `members`, `sampleQuestions`, `visibleCount`, `predictChoices`, `onAsk`, etc.)
1528
+ * @returns {AskBar}
1529
+ */
1530
+ askBar(containerOrOptions, options = {}) {
1531
+ const isPlainOptions = containerOrOptions
1532
+ && typeof containerOrOptions === 'object'
1533
+ && !containerOrOptions.nodeType
1534
+ && typeof containerOrOptions.appendChild !== 'function';
1535
+ const container = isPlainOptions ? this.container : containerOrOptions;
1536
+ const opts = isPlainOptions ? containerOrOptions : options;
1537
+ return new AskBar(container, { resultEngine: this, ...opts });
1538
+ }
1539
+
1540
+ /** Static helper: `RenderEngine.askBar(container, options)` constructs and mounts an `AskBar`. */
1541
+ static askBar(container, options = {}) {
1542
+ return new AskBar(container, options);
1491
1543
  }
1492
1544
 
1493
1545
  /**
@@ -1499,7 +1551,8 @@ export class RenderEngine {
1499
1551
  * the axes. The container's height follows the grid, as resultView's does.
1500
1552
  * @param {Array<object>} rows
1501
1553
  * @param {object} [options] rowAxis, columnAxis, measures, columns (hints, with the rollup column's),
1502
- * dimensions, onDrill, menus, title, nullText, fitHeight
1554
+ * dimensions, onDrill, menus, title, nullText, fitHeight, format (how numbers show, per measure or for
1555
+ * all: cells, totals and the traffic lights' cut-offs; the copy keeps the raw numbers)
1503
1556
  * @returns {PivotView|null} the grid's state, for the host to read or change
1504
1557
  */
1505
1558
  pivotView(rows, options = {}) {
@@ -1537,7 +1590,8 @@ export class RenderEngine {
1537
1590
  pivotChart(view, options = {}) {
1538
1591
  if (!this.renderer) return null;
1539
1592
  const chart = new PivotChart(view, options);
1540
- chart.detach = view.listen(() => this._drawPivotChart(chart, options));
1593
+ chart.redraw = () => this._drawPivotChart(chart, options);
1594
+ chart.detach = view.listen(chart.redraw);
1541
1595
  this._drawPivotChart(chart, options);
1542
1596
  return chart;
1543
1597
  }
@@ -1548,15 +1602,19 @@ export class RenderEngine {
1548
1602
  if (choice === null) {
1549
1603
  this.renderer.clear();
1550
1604
  this.renderer.text('No chart fits what the grid shows', 8, 14, { fill: '#666', fontSize: '12px', verticalAlign: 'middle' });
1605
+ options.onDraw?.(null, chart);
1551
1606
  return;
1552
1607
  }
1553
1608
  // No title unless one is given: the grid beside it says what is charted, and the link below
1554
1609
  // names the chart (with the chooser's reason as its tooltip).
1555
1610
  this.renderChartType(choice.key, choice.data, {
1611
+ ...choice.options,
1556
1612
  title: options.title,
1557
1613
  legend: true, // a series per column member: which colour is which must be said
1558
1614
  onDatumClick: (datum) => chart.click(datum),
1615
+ onNodeClick: (node) => chart.click({ label: node.label }),
1559
1616
  });
1617
+ options.onDraw?.(choice, chart);
1560
1618
  // Which chart, and the menu to change it, drawn over the chart as resultView's "◂ Table" is.
1561
1619
  if (!this.renderer.supportsSceneGraph) return;
1562
1620
  const pick = this.renderer.text(`${choice.label} ▾`, 8, 14, { fill: '#2b5fd9', fontSize: '12px', verticalAlign: 'middle', tooltip: choice.reason });
@@ -1574,6 +1632,57 @@ export class RenderEngine {
1574
1632
  this.fitToContainer();
1575
1633
  }
1576
1634
 
1635
+ /**
1636
+ * Renders an icicle chart — rectangular hierarchical partition chart (top-down bands by depth,
1637
+ * width proportional to value within each parent's span).
1638
+ * @param {Array<{label: string, value?: number, children?: Array}>} data
1639
+ * @param {object} [options]
1640
+ */
1641
+ icicleChart(data, options = {}) {
1642
+ if (!this.renderer) return;
1643
+ this._chart3D = false;
1644
+ this._chartState = { type: 'icicle', data, options };
1645
+ this._renderChart(new IcicleChart(this.renderer, data, options));
1646
+ }
1647
+
1648
+ /**
1649
+ * Renders a Pareto chart — categories sorted descending with a cumulative percentage line (0–100%)
1650
+ * on a right-hand axis and an 80% cutoff reference line.
1651
+ * @param {Array<{label: string, value: number}>} data
1652
+ * @param {object} [options]
1653
+ */
1654
+ paretoChart(data, options = {}) {
1655
+ if (!this.renderer) return;
1656
+ this._chart3D = false;
1657
+ this._chartState = { type: 'pareto', data, options };
1658
+ this._renderChart(new ParetoChart(this.renderer, data, options));
1659
+ }
1660
+
1661
+ /**
1662
+ * Renders a lollipop chart — stem-and-dot alternative to bars for 1D or multi-series comparisons.
1663
+ * @param {Array<{label: string, value: number}>|Array<{name: string, values: Array<{label: string, value: number}>}>} data
1664
+ * @param {object} [options]
1665
+ */
1666
+ lollipopChart(data, options = {}) {
1667
+ if (!this.renderer) return;
1668
+ this._chart3D = false;
1669
+ this._chartState = { type: 'lollipop', data, options };
1670
+ this._renderChart(new LollipopChart(this.renderer, data, options));
1671
+ }
1672
+
1673
+ /**
1674
+ * Renders a Marimekko (mosaic) chart — variable-width 100%-stacked columns where column width is
1675
+ * proportional to each category's total and segment height is each series' share within that category.
1676
+ * @param {Array<{name: string, values: Array<{label: string, value: number}>}>|Array<{label: string, values: number[]}>} data
1677
+ * @param {object} [options]
1678
+ */
1679
+ marimekkoChart(data, options = {}) {
1680
+ if (!this.renderer) return;
1681
+ this._chart3D = false;
1682
+ this._chartState = { type: 'marimekko', data, options };
1683
+ this._renderChart(new MarimekkoChart(this.renderer, data, options));
1684
+ }
1685
+
1577
1686
  /**
1578
1687
  * Renders a waterfall chart — running-total bars, each floating from the
1579
1688
  * previous cumulative value to the new one (a "delta" bar), except
@@ -253,6 +253,8 @@ export class ArchimateCanvasPart extends EditorPart {
253
253
  if (previous) {
254
254
  this.editor.showGrid = previous.showGrid;
255
255
  this.editor.gridSize = previous.gridSize;
256
+ this.editor.showExportArea = previous.showExportArea;
257
+ this.editor.exportOptions = previous.exportOptions;
256
258
  }
257
259
 
258
260
  // A canvas's palette is Archi's canvas palette; another view's, ArchiMate's element types.
@@ -351,6 +353,7 @@ export class ArchimateCanvasPart extends EditorPart {
351
353
  this.editor?.selectTemplate(instanceId);
352
354
  },
353
355
  addToView: (el) => this.editor?.showElement(el.id, this.editor.visibleCentre()),
356
+ deleteFromModel: (el) => this.editor?.askDeleteFromModel([el.id]),
354
357
  });
355
358
  }
356
359
 
@@ -89,6 +89,8 @@ export class DiagramEditorPart extends EditorPart {
89
89
  if (previous) {
90
90
  this.editor.showGrid = previous.showGrid;
91
91
  this.editor.gridSize = previous.gridSize;
92
+ this.editor.showExportArea = previous.showExportArea;
93
+ this.editor.exportOptions = previous.exportOptions;
92
94
  }
93
95
  this.editor.initPaletteForDiagramType(palette, model.templateType);
94
96
  this.menuBar?.dispose();
@@ -1,6 +1,6 @@
1
1
  import { ModelFile } from './ModelFile.js';
2
2
  import { EditorPart } from './EditorPart.js';
3
- import { modelFileMenuItems } from '../../charting/ModelFileFormat.js';
3
+ import { modelFileMenuItems, askPasteImport } from '../../charting/ModelFileFormat.js';
4
4
  import { pickFile } from '../../charting/toolbar/pickFile.js';
5
5
 
6
6
  /**
@@ -121,9 +121,20 @@ export const ModelHosted = (Base) => class extends Base {
121
121
 
122
122
  /** Import ▸: each format that only reads (another tool's export), opened as a new model. */
123
123
  importItems() {
124
- return modelFileMenuItems(this.fileFormats().filter(f => f.exports === false), {
125
- model: () => this.model, only: 'import', onImport: (model) => this.onImported(model), document: () => this.document,
126
- });
124
+ return [
125
+ ...modelFileMenuItems(this.fileFormats().filter(f => f.exports === false), {
126
+ model: () => this.model, only: 'import', onImport: (model) => this.onImported(model), document: () => this.document,
127
+ }),
128
+ ...this.pasteItems(),
129
+ ];
130
+ }
131
+
132
+ /** Paste …: a model in each of its text formats from pasted text (an assistant's answer, say), as Open… would bring it. */
133
+ pasteItems() {
134
+ return this.fileFormats().filter(f => f.imports && !f.readsBytes).map(format => ({
135
+ label: `From pasted text (${format.label})…`,
136
+ action: () => askPasteImport(format, { onImport: (model) => this.onImported(model), document: this.document ?? globalThis.document }),
137
+ }));
127
138
  }
128
139
 
129
140
  /**