@peter.prib/renderengine 0.15.2 → 0.15.3

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 (42) hide show
  1. package/dist/renderengine-workspace.umd.js +5963 -3766
  2. package/dist/renderengine.umd.js +5863 -3772
  3. package/documentation/REFERENCE.md +37 -3
  4. package/package.json +1 -1
  5. package/src/archimateArchiFormat.js +114 -10
  6. package/src/archimateArchive.js +27 -4
  7. package/src/archimateExchangeFormat.js +12 -4
  8. package/src/archimateSpecializations.js +11 -1
  9. package/src/chartCatalog.js +6 -4
  10. package/src/charting/ArchimateDiagramEditor.js +5 -108
  11. package/src/charting/DiagramTypeRegistry.js +20 -1
  12. package/src/charting/DiagramViewPropertiesDialog.js +54 -10
  13. package/src/charting/Legend.js +1 -2
  14. package/src/charting/Statistics.js +14 -0
  15. package/src/charting/charts/BarChart.js +59 -8
  16. package/src/charting/charts/HexbinChart.js +15 -0
  17. package/src/charting/charts/LollipopChart.js +42 -7
  18. package/src/charting/decision/DecisionWorkbench.js +71 -4
  19. package/src/charting/diagramModel/DiagramView.js +7 -2
  20. package/src/charting/diagrams/GraphDiagram3D.js +15 -0
  21. package/src/charting/graph/GraphSource.js +72 -0
  22. package/src/charting/modelTree/NavigatorModel.js +22 -8
  23. package/src/charting/report/TemplatedReport.js +27 -377
  24. package/src/charting/report/ViewsReport.js +118 -0
  25. package/src/charting/report/reportWriters.js +496 -0
  26. package/src/charting/templates/C4BoundaryTemplate.js +34 -0
  27. package/src/charting/templates/C4ComponentTemplate.js +30 -0
  28. package/src/charting/templates/C4ContainerTemplate.js +49 -0
  29. package/src/charting/templates/C4PersonTemplate.js +35 -0
  30. package/src/charting/templates/C4SystemTemplate.js +32 -0
  31. package/src/charting/templates/C4Template.js +95 -0
  32. package/src/charting/templates/index.js +6 -0
  33. package/src/charting/templates/templateRegistry.js +7 -0
  34. package/src/compute/ComputeBackend.js +132 -0
  35. package/src/compute/CpuBackend.js +209 -0
  36. package/src/compute/WebGpuBackend.js +630 -0
  37. package/src/compute/index.js +5 -0
  38. package/src/compute/selectBackend.js +85 -0
  39. package/src/index.js +3403 -3371
  40. package/src/kernelExpr.js +80 -5
  41. package/src/workspace/parts/DiagramEditorPart.js +3 -1
  42. package/src/workspace/parts/ViewsPart.js +106 -0
@@ -1,7 +1,11 @@
1
1
  import { treeLayerOf } from './ModelTreePanel.js';
2
+ import { DocumentGraph } from '../graph/GraphSource.js';
2
3
 
3
4
  /**
4
- * NavigatorModel — pure, DOM-free graph traversal and navigation engine for an ArchiMate model.
5
+ * NavigatorModel — pure, DOM-free graph traversal and navigation engine for any graph: an ArchiMate
6
+ * model, or any GraphSource (`DocumentGraph` for a `{ nodes, edges }` graph document — see
7
+ * `NavigatorModel.forGraph`). It reads the graph only through `getElement`, `relationshipsFor`,
8
+ * `viewsContaining` and `layerOf` (see `GraphSource` for why).
5
9
  *
6
10
  * Allows exploring relationships step-by-step from any focused root element to arbitrary depth,
7
11
  * with cycle detection, navigation history (back/forward), direction filters, path-finding
@@ -9,12 +13,17 @@ import { treeLayerOf } from './ModelTreePanel.js';
9
13
  */
10
14
  export class NavigatorModel {
11
15
  /**
12
- * @param {ArchimateModel} model
16
+ * @param {ArchimateModel|import('../graph/GraphSource.js').GraphSource} model
13
17
  * @param {Object} [options]
14
18
  * @param {string} [options.rootId] - Initial focused element id
15
19
  * @param {'both'|'outgoing'|'incoming'} [options.direction='both']
16
20
  * @param {Iterable<string>} [options.allowedKinds] - Optional relationship kinds to include
17
21
  */
22
+ /** A navigator over a graph document (`{ nodes, edges }`): the same walker, through a DocumentGraph. */
23
+ static forGraph(doc, options) {
24
+ return new NavigatorModel(new DocumentGraph(doc), options);
25
+ }
26
+
18
27
  constructor(model, { rootId = null, direction = 'both', allowedKinds = null } = {}) {
19
28
  this.model = model;
20
29
  this.direction = direction;
@@ -39,6 +48,11 @@ export class NavigatorModel {
39
48
  return () => this._listeners.delete(listener);
40
49
  }
41
50
 
51
+ /** An element's layer: the graph's own answer, or ArchiMate's (an ArchimateModel has none of its own). */
52
+ layerOf(element) {
53
+ return typeof this.model.layerOf === 'function' ? this.model.layerOf(element) : treeLayerOf(element);
54
+ }
55
+
42
56
  _notify() {
43
57
  for (const listener of this._listeners) {
44
58
  try {
@@ -209,7 +223,7 @@ export class NavigatorModel {
209
223
  direction: dir,
210
224
  relationship: rel,
211
225
  element: otherElement,
212
- layer: treeLayerOf(otherElement),
226
+ layer: this.layerOf(otherElement),
213
227
  cycle,
214
228
  expanded,
215
229
  children,
@@ -227,8 +241,8 @@ export class NavigatorModel {
227
241
  return {
228
242
  pathKey,
229
243
  element,
230
- layer: treeLayerOf(element),
231
- views: this.model.viewsContaining(element.id),
244
+ layer: this.layerOf(element),
245
+ views: typeof this.model.viewsContaining === 'function' ? this.model.viewsContaining(element.id) : [],
232
246
  incoming: incomingBranches,
233
247
  outgoing: outgoingBranches,
234
248
  };
@@ -355,7 +369,7 @@ export class NavigatorModel {
355
369
  const depths = new Map([[elementId, 0]]);
356
370
  const elementsMap = new Map();
357
371
  const relsMap = new Map();
358
- const layers = new Set([treeLayerOf(root)]);
372
+ const layers = new Set([this.layerOf(root)]);
359
373
 
360
374
  const queue = [{ id: elementId, depth: 0 }];
361
375
 
@@ -376,7 +390,7 @@ export class NavigatorModel {
376
390
  if (!depths.has(target.id)) {
377
391
  depths.set(target.id, nextDepth);
378
392
  elementsMap.set(target.id, target);
379
- layers.add(treeLayerOf(target));
393
+ layers.add(this.layerOf(target));
380
394
  queue.push({ id: target.id, depth: nextDepth });
381
395
  }
382
396
  }
@@ -392,7 +406,7 @@ export class NavigatorModel {
392
406
  if (!depths.has(source.id)) {
393
407
  depths.set(source.id, nextDepth);
394
408
  elementsMap.set(source.id, source);
395
- layers.add(treeLayerOf(source));
409
+ layers.add(this.layerOf(source));
396
410
  queue.push({ id: source.id, depth: nextDepth });
397
411
  }
398
412
  }
@@ -1,11 +1,24 @@
1
1
  /**
2
- * TemplatedReport — template-driven report generator (PDF, Word, Excel, Markdown, HTML).
2
+ * TemplatedReport — template-driven report generator (HTML, Word, Excel, CSV, Markdown).
3
3
  *
4
4
  * Implements Archi's Jasper-style reporting with customizable templates,
5
5
  * placeholders for model metadata, views, view diagrams (SVG/PNG), elements,
6
6
  * relationships, and properties.
7
7
  */
8
8
 
9
+ import {
10
+ escapeHtml,
11
+ escapeXml,
12
+ escapeCsv,
13
+ formatHtml,
14
+ formatWord,
15
+ formatExcel,
16
+ formatCsv,
17
+ formatMarkdown,
18
+ } from './reportWriters.js';
19
+
20
+ export { escapeHtml, escapeXml, escapeCsv };
21
+
9
22
  /** Built-in template templates */
10
23
  export const REPORT_TEMPLATES = Object.freeze({
11
24
  EXECUTIVE: 'executive',
@@ -13,27 +26,6 @@ export const REPORT_TEMPLATES = Object.freeze({
13
26
  TABULAR: 'tabular',
14
27
  });
15
28
 
16
- function escapeHtml(str) {
17
- return String(str ?? '')
18
- .replace(/&/g, '&amp;')
19
- .replace(/</g, '&lt;')
20
- .replace(/>/g, '&gt;')
21
- .replace(/"/g, '&quot;')
22
- .replace(/'/g, '&#39;');
23
- }
24
-
25
- function escapeXml(str) {
26
- return escapeHtml(str);
27
- }
28
-
29
- function escapeCsv(val) {
30
- const s = String(val ?? '');
31
- if (/[",\n\r]/.test(s)) {
32
- return `"${s.replace(/"/g, '""')}"`;
33
- }
34
- return s;
35
- }
36
-
37
29
  export class TemplatedReport {
38
30
  /**
39
31
  * @param {Object|ViewSetModel|ArchimateModel} model
@@ -50,14 +42,14 @@ export class TemplatedReport {
50
42
  }
51
43
 
52
44
  get title() {
53
- return this._title || this.model.name || this.model.title || 'Model Report';
45
+ return this._title || this.model?.name || this.model?.title || 'Model Report';
54
46
  }
55
47
 
56
48
  /**
57
49
  * Normalized model data extracted for reporting.
58
50
  */
59
51
  extractData() {
60
- const m = this.model;
52
+ const m = this.model || {};
61
53
  const title = this.title;
62
54
  const doc = m.documentation || '';
63
55
 
@@ -149,6 +141,9 @@ export class TemplatedReport {
149
141
  version: m.version || '1.0',
150
142
  properties,
151
143
  },
144
+ title,
145
+ documentation: doc,
146
+ version: m.version || '1.0',
152
147
  views,
153
148
  elements: normElements,
154
149
  relationships: normRelationships,
@@ -163,279 +158,24 @@ export class TemplatedReport {
163
158
  * @param {boolean} [options.printable=true]
164
159
  * @returns {string}
165
160
  */
166
- toHtml({ printable = true } = {}) {
167
- const data = this.extractData();
168
- const { model, views, elements, relationships, properties, date } = data;
169
-
170
- const printCss = printable ? `
171
- @media print {
172
- body { background: #fff; margin: 0; padding: 0; font-size: 11pt; color: #000; }
173
- header, footer { position: fixed; width: 100%; left: 0; font-size: 9pt; color: #777; }
174
- header { top: 0; border-bottom: 1px solid #ccc; padding-bottom: 4px; }
175
- footer { bottom: 0; border-top: 1px solid #ccc; padding-top: 4px; display: flex; justify-content: space-between; }
176
- .page-break { page-break-after: always; }
177
- .no-break { page-break-inside: avoid; }
178
- .picture { max-width: 100% !important; page-break-inside: avoid; }
179
- table { page-break-inside: auto; }
180
- tr { page-break-inside: avoid; page-break-after: auto; }
181
- }
182
- @page {
183
- size: A4;
184
- margin: 20mm;
185
- }` : '';
186
-
187
- return `<!DOCTYPE html>
188
- <html lang="en">
189
- <head>
190
- <meta charset="utf-8">
191
- <meta name="viewport" content="width=device-width, initial-scale=1">
192
- <title>${escapeHtml(model.name)}</title>
193
- <style>
194
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.5; color: #222; margin: 24px auto; max-width: 960px; padding: 0 16px; }
195
- h1 { font-size: 26px; border-bottom: 2px solid #2563eb; padding-bottom: 6px; margin-top: 24px; color: #1e3a8a; }
196
- h2 { font-size: 20px; border-bottom: 1px solid #cbd5e1; padding-bottom: 4px; margin-top: 24px; color: #1e40af; }
197
- h3 { font-size: 16px; margin-top: 18px; color: #334155; }
198
- .meta { font-size: 13px; color: #64748b; margin-bottom: 20px; }
199
- .doc { white-space: pre-wrap; background: #f8fafc; border-left: 4px solid #3b82f6; padding: 10px 14px; margin: 12px 0; font-size: 14px; }
200
- table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 13px; }
201
- th, td { border: 1px solid #e2e8f0; padding: 6px 10px; text-align: left; vertical-align: top; }
202
- th { background: #f1f5f9; font-weight: 600; color: #334155; }
203
- tr:nth-child(even) { background: #f8fafc; }
204
- .picture { margin: 16px 0; border: 1px solid #e2e8f0; border-radius: 4px; padding: 8px; background: #fff; overflow-x: auto; text-align: center; }
205
- .picture svg { max-width: 100%; height: auto; }
206
- .badge { display: inline-block; padding: 2px 6px; font-size: 11px; font-weight: 500; border-radius: 3px; background: #e0f2fe; color: #0369a1; }
207
- ${printCss}
208
- </style>
209
- </head>
210
- <body>
211
- <h1>${escapeHtml(model.name)}</h1>
212
- <div class="meta">Generated: ${escapeHtml(date)} | Version: ${escapeHtml(model.version)}</div>
213
- ${model.documentation ? `<div class="doc">${escapeHtml(model.documentation)}</div>` : ''}
214
-
215
- ${properties.length ? `
216
- <h2>Model Properties</h2>
217
- <table>
218
- <thead><tr><th>Property</th><th>Value</th></tr></thead>
219
- <tbody>
220
- ${properties.map(p => `<tr><td>${escapeHtml(p.name)}</td><td>${escapeHtml(p.value)}</td></tr>`).join('')}
221
- </tbody>
222
- </table>` : ''}
223
-
224
- <h2>Views (${views.length})</h2>
225
- ${views.map(v => `
226
- <div class="no-break">
227
- <h3>${escapeHtml(v.name)}</h3>
228
- ${v.documentation ? `<div class="doc">${escapeHtml(v.documentation)}</div>` : ''}
229
- ${v.picture ? `<div class="picture">${v.picture}</div>` : ''}
230
- ${v.elements.length ? `
231
- <table>
232
- <thead><tr><th>Element</th><th>Type</th></tr></thead>
233
- <tbody>
234
- ${v.elements.map(e => `<tr><td>${escapeHtml(e.name)}</td><td><span class="badge">${escapeHtml(e.type)}</span></td></tr>`).join('')}
235
- </tbody>
236
- </table>` : ''}
237
- </div>
238
- <div class="page-break"></div>
239
- `).join('')}
240
-
241
- ${elements.length ? `
242
- <h2>Elements (${elements.length})</h2>
243
- <table>
244
- <thead><tr><th>Name</th><th>Type</th><th>Layer</th><th>Documentation</th></tr></thead>
245
- <tbody>
246
- ${elements.map(e => `<tr><td><strong>${escapeHtml(e.name)}</strong></td><td><span class="badge">${escapeHtml(e.type)}</span></td><td>${escapeHtml(e.layer)}</td><td>${escapeHtml(e.documentation)}</td></tr>`).join('')}
247
- </tbody>
248
- </table>` : ''}
249
-
250
- ${relationships.length ? `
251
- <h2>Relationships (${relationships.length})</h2>
252
- <table>
253
- <thead><tr><th>Type</th><th>Source</th><th>Target</th><th>Name</th><th>Documentation</th></tr></thead>
254
- <tbody>
255
- ${relationships.map(r => `<tr><td><span class="badge">${escapeHtml(r.type)}</span></td><td>${escapeHtml(r.source)}</td><td>${escapeHtml(r.target)}</td><td>${escapeHtml(r.name)}</td><td>${escapeHtml(r.documentation)}</td></tr>`).join('')}
256
- </tbody>
257
- </table>` : ''}
258
-
259
- </body>
260
- </html>`;
161
+ toHtml(options = {}) {
162
+ return formatHtml(this.extractData(), options);
261
163
  }
262
164
 
263
165
  /**
264
- * Render the report as Microsoft Word-compatible HTML document (.doc / .docx compatible).
265
- * Word opens this with native tables, headings, and formatting intact.
166
+ * Render the report as Microsoft Word-compatible document (.doc).
266
167
  * @returns {string}
267
168
  */
268
169
  toWord() {
269
- const data = this.extractData();
270
- const { model, views, elements, relationships, properties, date } = data;
271
-
272
- return `<html xmlns:o="urn:schemas-microsoft-com:office:office"
273
- xmlns:w="urn:schemas-microsoft-com:office:word"
274
- xmlns="http://www.w3.org/TR/REC-html40">
275
- <head>
276
- <meta charset="utf-8">
277
- <title>${escapeXml(model.name)}</title>
278
- <!--[if gte mso 9]>
279
- <xml>
280
- <w:WordDocument>
281
- <w:View>Print</w:View>
282
- <w:Zoom>100</w:Zoom>
283
- <w:DoNotOptimizeForBrowser/>
284
- </w:WordDocument>
285
- </xml>
286
- <![endif]-->
287
- <style>
288
- body { font-family: Calibri, Arial, sans-serif; font-size: 11pt; line-height: 1.4; color: #333333; }
289
- h1 { font-size: 22pt; color: #1f4e79; border-bottom: 2pt solid #1f4e79; margin-bottom: 12pt; }
290
- h2 { font-size: 16pt; color: #2e75b6; margin-top: 18pt; margin-bottom: 8pt; }
291
- h3 { font-size: 13pt; color: #595959; margin-top: 12pt; }
292
- p.doc { font-style: italic; background-color: #f2f2f2; padding: 6pt; border-left: 3pt solid #2e75b6; }
293
- table { width: 100%; border-collapse: collapse; margin-top: 8pt; margin-bottom: 12pt; }
294
- th, td { border: 0.5pt solid #d9d9d9; padding: 4pt 6pt; font-size: 10pt; }
295
- th { background-color: #f2f2f2; font-weight: bold; color: #1f4e79; }
296
- .page-break { page-break-before: always; }
297
- </style>
298
- </head>
299
- <body>
300
- <h1>${escapeXml(model.name)}</h1>
301
- <p><strong>Generated:</strong> ${escapeXml(date)} | <strong>Version:</strong> ${escapeXml(model.version)}</p>
302
- ${model.documentation ? `<p class="doc">${escapeXml(model.documentation)}</p>` : ''}
303
-
304
- ${properties.length ? `
305
- <h2>Properties</h2>
306
- <table>
307
- <tr><th>Property</th><th>Value</th></tr>
308
- ${properties.map(p => `<tr><td>${escapeXml(p.name)}</td><td>${escapeXml(p.value)}</td></tr>`).join('')}
309
- </table>` : ''}
310
-
311
- <h2>Views</h2>
312
- ${views.map(v => `
313
- <div class="page-break"></div>
314
- <h3>${escapeXml(v.name)}</h3>
315
- ${v.documentation ? `<p class="doc">${escapeXml(v.documentation)}</p>` : ''}
316
- ${v.picture ? `<div>${v.picture}</div>` : ''}
317
- ${v.elements.length ? `
318
- <table>
319
- <tr><th>Element</th><th>Type</th></tr>
320
- ${v.elements.map(e => `<tr><td>${escapeXml(e.name)}</td><td>${escapeXml(e.type)}</td></tr>`).join('')}
321
- </table>` : ''}
322
- `).join('')}
323
-
324
- ${elements.length ? `
325
- <div class="page-break"></div>
326
- <h2>Elements (${elements.length})</h2>
327
- <table>
328
- <tr><th>Name</th><th>Type</th><th>Layer</th><th>Documentation</th></tr>
329
- ${elements.map(e => `<tr><td><strong>${escapeXml(e.name)}</strong></td><td>${escapeXml(e.type)}</td><td>${escapeXml(e.layer)}</td><td>${escapeXml(e.documentation)}</td></tr>`).join('')}
330
- </table>` : ''}
331
-
332
- ${relationships.length ? `
333
- <div class="page-break"></div>
334
- <h2>Relationships (${relationships.length})</h2>
335
- <table>
336
- <tr><th>Type</th><th>Source</th><th>Target</th><th>Name</th></tr>
337
- ${relationships.map(r => `<tr><td>${escapeXml(r.type)}</td><td>${escapeXml(r.source)}</td><td>${escapeXml(r.target)}</td><td>${escapeXml(r.name)}</td></tr>`).join('')}
338
- </table>` : ''}
339
-
340
- </body>
341
- </html>`;
170
+ return formatWord(this.extractData());
342
171
  }
343
172
 
344
173
  /**
345
- * Render the report as Microsoft Excel XML Spreadsheet 2003 format (.xml / .xls).
346
- * Opens in Excel with native worksheets: Model, Views, Elements, Relationships, Properties.
174
+ * Render the report as Microsoft Excel XML Spreadsheet 2003 format (.xml).
347
175
  * @returns {string}
348
176
  */
349
177
  toExcel() {
350
- const data = this.extractData();
351
- const { model, views, elements, relationships, properties, date } = data;
352
-
353
- const makeRow = (cells, isHeader = false) => `
354
- <Row>
355
- ${cells.map(c => ` <Cell${isHeader ? ' ss:StyleID="Header"' : ''}><Data ss:Type="String">${escapeXml(c)}</Data></Cell>`).join('\n')}
356
- </Row>`;
357
-
358
- return `<?xml version="1.0"?>
359
- <?mso-application progid="Excel.Sheet"?>
360
- <Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
361
- xmlns:o="urn:schemas-microsoft-com:office:office"
362
- xmlns:x="urn:schemas-microsoft-com:office:excel"
363
- xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
364
- <Styles>
365
- <Style ss:ID="Default" ss:Name="Normal">
366
- <Alignment ss:Vertical="Center"/>
367
- <Font ss:FontName="Calibri" ss:Size="11" ss:Color="#000000"/>
368
- </Style>
369
- <Style ss:ID="Header">
370
- <Font ss:FontName="Calibri" ss:Size="11" ss:Color="#FFFFFF" ss:Bold="1"/>
371
- <Interior ss:Color="#2563EB" ss:Pattern="Solid"/>
372
- </Style>
373
- <Style ss:ID="Title">
374
- <Font ss:FontName="Calibri" ss:Size="16" ss:Color="#1E3A8A" ss:Bold="1"/>
375
- </Style>
376
- </Styles>
377
-
378
- <Worksheet ss:Name="Model Summary">
379
- <Table>
380
- <Column ss:Width="160"/>
381
- <Column ss:Width="400"/>
382
- <Row>
383
- <Cell ss:StyleID="Title"><Data ss:Type="String">${escapeXml(model.name)}</Data></Cell>
384
- </Row>
385
- ${makeRow(['Item', 'Details'], true)}
386
- ${makeRow(['Title', model.name])}
387
- ${makeRow(['Version', model.version])}
388
- ${makeRow(['Generated', date])}
389
- ${makeRow(['Documentation', model.documentation])}
390
- ${makeRow(['Total Views', String(views.length)])}
391
- ${makeRow(['Total Elements', String(elements.length)])}
392
- ${makeRow(['Total Relationships', String(relationships.length)])}
393
- </Table>
394
- </Worksheet>
395
-
396
- <Worksheet ss:Name="Views">
397
- <Table>
398
- <Column ss:Width="180"/>
399
- <Column ss:Width="80"/>
400
- <Column ss:Width="300"/>
401
- ${makeRow(['View Name', 'Elements Count', 'Documentation'], true)}
402
- ${views.map(v => makeRow([v.name, String(v.elements.length), v.documentation])).join('')}
403
- </Table>
404
- </Worksheet>
405
-
406
- <Worksheet ss:Name="Elements">
407
- <Table>
408
- <Column ss:Width="140"/>
409
- <Column ss:Width="160"/>
410
- <Column ss:Width="100"/>
411
- <Column ss:Width="280"/>
412
- ${makeRow(['Name', 'Type', 'Layer', 'Documentation'], true)}
413
- ${elements.map(e => makeRow([e.name, e.type, e.layer, e.documentation])).join('')}
414
- </Table>
415
- </Worksheet>
416
-
417
- <Worksheet ss:Name="Relationships">
418
- <Table>
419
- <Column ss:Width="140"/>
420
- <Column ss:Width="160"/>
421
- <Column ss:Width="160"/>
422
- <Column ss:Width="100"/>
423
- <Column ss:Width="200"/>
424
- ${makeRow(['Type', 'Source', 'Target', 'Name', 'Documentation'], true)}
425
- ${relationships.map(r => makeRow([r.type, r.source, r.target, r.name, r.documentation])).join('')}
426
- </Table>
427
- </Worksheet>
428
-
429
- ${properties.length ? `
430
- <Worksheet ss:Name="Properties">
431
- <Table>
432
- <Column ss:Width="160"/>
433
- <Column ss:Width="300"/>
434
- ${makeRow(['Name', 'Value'], true)}
435
- ${properties.map(p => makeRow([p.name, p.value])).join('')}
436
- </Table>
437
- </Worksheet>` : ''}
438
- </Workbook>`;
178
+ return formatExcel(this.extractData());
439
179
  }
440
180
 
441
181
  /**
@@ -444,30 +184,7 @@ ${cells.map(c => ` <Cell${isHeader ? ' ss:StyleID="Header"' : ''}><Data ss:T
444
184
  * @returns {string}
445
185
  */
446
186
  toCsv(table = 'elements') {
447
- const data = this.extractData();
448
- switch (table) {
449
- case 'views': {
450
- const header = ['ID', 'Name', 'Elements', 'Documentation'];
451
- const rows = data.views.map(v => [v.id, v.name, v.elements.length, v.documentation]);
452
- return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
453
- }
454
- case 'relationships': {
455
- const header = ['ID', 'Type', 'Source', 'Target', 'Name', 'Documentation'];
456
- const rows = data.relationships.map(r => [r.id, r.type, r.source, r.target, r.name, r.documentation]);
457
- return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
458
- }
459
- case 'properties': {
460
- const header = ['Name', 'Value'];
461
- const rows = data.properties.map(p => [p.name, p.value]);
462
- return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
463
- }
464
- case 'elements':
465
- default: {
466
- const header = ['ID', 'Name', 'Type', 'Layer', 'Documentation'];
467
- const rows = data.elements.map(e => [e.id, e.name, e.type, e.layer, e.documentation]);
468
- return [header, ...rows].map(r => r.map(escapeCsv).join(',')).join('\r\n');
469
- }
470
- }
187
+ return formatCsv(this.extractData(), table);
471
188
  }
472
189
 
473
190
  /**
@@ -475,73 +192,6 @@ ${cells.map(c => ` <Cell${isHeader ? ' ss:StyleID="Header"' : ''}><Data ss:T
475
192
  * @returns {string}
476
193
  */
477
194
  toMarkdown() {
478
- const data = this.extractData();
479
- const { model, views, elements, relationships, properties, date } = data;
480
- const lines = [];
481
-
482
- lines.push(`# ${model.name}`);
483
- lines.push(`*Generated: ${date} | Version: ${model.version}*`);
484
- lines.push('');
485
-
486
- if (model.documentation) {
487
- lines.push(model.documentation);
488
- lines.push('');
489
- }
490
-
491
- if (properties.length) {
492
- lines.push('## Properties');
493
- lines.push('');
494
- lines.push('| Property | Value |');
495
- lines.push('| --- | --- |');
496
- for (const p of properties) {
497
- lines.push(`| ${p.name} | ${p.value} |`);
498
- }
499
- lines.push('');
500
- }
501
-
502
- if (views.length) {
503
- lines.push(`## Views (${views.length})`);
504
- lines.push('');
505
- for (const v of views) {
506
- lines.push(`### ${v.name}`);
507
- if (v.documentation) {
508
- lines.push('');
509
- lines.push(v.documentation);
510
- }
511
- if (v.elements.length) {
512
- lines.push('');
513
- lines.push('| Element | Type |');
514
- lines.push('| --- | --- |');
515
- for (const e of v.elements) {
516
- lines.push(`| ${e.name} | ${e.type} |`);
517
- }
518
- }
519
- lines.push('');
520
- }
521
- }
522
-
523
- if (elements.length) {
524
- lines.push(`## Elements (${elements.length})`);
525
- lines.push('');
526
- lines.push('| Name | Type | Layer | Documentation |');
527
- lines.push('| --- | --- | --- | --- |');
528
- for (const e of elements) {
529
- lines.push(`| **${e.name}** | ${e.type} | ${e.layer} | ${e.documentation.replace(/\n/g, ' ')} |`);
530
- }
531
- lines.push('');
532
- }
533
-
534
- if (relationships.length) {
535
- lines.push(`## Relationships (${relationships.length})`);
536
- lines.push('');
537
- lines.push('| Type | Source | Target | Name |');
538
- lines.push('| --- | --- | --- | --- |');
539
- for (const r of relationships) {
540
- lines.push(`| ${r.type} | ${r.source} | ${r.target} | ${r.name} |`);
541
- }
542
- lines.push('');
543
- }
544
-
545
- return lines.join('\n');
195
+ return formatMarkdown(this.extractData());
546
196
  }
547
197
  }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * ViewsReport — a templated report for any set of chart views (ViewsPart, DecisionWorkbench).
3
+ *
4
+ * Normalizes views containing charts (SVGs, titles, data) and notes/documentation into
5
+ * the report structure used by formatHtml, formatWord, formatExcel, formatCsv, and formatMarkdown.
6
+ */
7
+
8
+ import { TemplatedReport, REPORT_TEMPLATES } from './TemplatedReport.js';
9
+
10
+ export class ViewsReport extends TemplatedReport {
11
+ /**
12
+ * @param {Array<Object>|Object} views - array of view objects or document holding a views list
13
+ * @param {Object} [options]
14
+ * @param {string} [options.title] - Report title (default 'Views Report')
15
+ * @param {string} [options.documentation] - Overall documentation/summary
16
+ * @param {string} [options.version='1.0'] - Version
17
+ * @param {Array<Object>} [options.properties=[]] - Overall properties
18
+ * @param {Object<string, string>} [options.pictures={}] - SVG markup mapped by viewId, viewId:index, or chart title
19
+ * @param {string} [options.template] - executive | detailed | tabular
20
+ */
21
+ constructor(views = [], options = {}) {
22
+ let viewList = [];
23
+ let docTitle = null;
24
+ let docDocumentation = null;
25
+ let docProperties = [];
26
+
27
+ if (Array.isArray(views)) {
28
+ viewList = views;
29
+ } else if (views && typeof views === 'object') {
30
+ viewList = Array.isArray(views.views) ? views.views : (typeof views.views === 'function' ? views.views() : []);
31
+ docTitle = views.title || views.name || null;
32
+ docDocumentation = views.documentation || views.notes || null;
33
+ docProperties = Array.isArray(views.properties) ? views.properties : [];
34
+ }
35
+
36
+ super(null, {
37
+ title: options.title || docTitle || 'Views Report',
38
+ pictures: options.pictures || {},
39
+ template: options.template || REPORT_TEMPLATES.DETAILED,
40
+ });
41
+
42
+ this.rawViews = viewList;
43
+ this.options = options;
44
+ this.documentation = options.documentation ?? (typeof docDocumentation === 'string' ? docDocumentation : '') ?? '';
45
+ this.version = options.version ?? '1.0';
46
+ this.properties = options.properties ?? docProperties ?? [];
47
+ }
48
+
49
+ get title() {
50
+ return this._title || 'Views Report';
51
+ }
52
+
53
+ /**
54
+ * Extract normalized report data from views.
55
+ */
56
+ extractData() {
57
+ const title = this.title;
58
+ const views = this.rawViews.map((v, vIndex) => {
59
+ const vId = v.id || `view-${vIndex + 1}`;
60
+ const vName = v.title || v.name || vId;
61
+ const notes = Array.isArray(v.notes) ? v.notes : (v.notes ? [String(v.notes)] : []);
62
+ const doc = v.documentation || (notes.length ? notes.join('\n\n') : '');
63
+
64
+ const charts = (v.charts || []).map((c, cIndex) => {
65
+ const picKey = `${vId}:${cIndex}`;
66
+ const pic = this.pictures[picKey]
67
+ || this.pictures[c.title]
68
+ || this.pictures[c.id]
69
+ || (cIndex === 0 ? this.pictures[vId] : null)
70
+ || c.picture
71
+ || c.svg
72
+ || '';
73
+ return {
74
+ id: c.id || `${vId}-chart-${cIndex + 1}`,
75
+ title: c.title || `Chart ${cIndex + 1}`,
76
+ chartType: c.chartType || '',
77
+ picture: pic,
78
+ data: c.data,
79
+ };
80
+ });
81
+
82
+ // Single picture if charts pictures exist or view picture
83
+ const chartPictures = charts.map(c => c.picture).filter(Boolean);
84
+ const primaryPic = chartPictures.length
85
+ ? chartPictures.join('\n')
86
+ : (this.pictures[vId] || v.picture || (typeof v.toSvg === 'function' ? v.toSvg() : ''));
87
+
88
+ return {
89
+ id: vId,
90
+ name: vName,
91
+ documentation: doc,
92
+ notes,
93
+ picture: primaryPic,
94
+ charts,
95
+ elements: Array.isArray(v.elements) ? v.elements : [],
96
+ properties: Array.isArray(v.properties) ? v.properties : [],
97
+ };
98
+ });
99
+
100
+ return {
101
+ model: {
102
+ id: 'views-report',
103
+ name: title,
104
+ documentation: this.documentation,
105
+ version: this.version,
106
+ properties: this.properties,
107
+ },
108
+ title,
109
+ documentation: this.documentation,
110
+ version: this.version,
111
+ views,
112
+ elements: [],
113
+ relationships: [],
114
+ properties: this.properties,
115
+ date: new Date().toISOString().split('T')[0],
116
+ };
117
+ }
118
+ }