@peter.prib/renderengine 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -19
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49641 -39747
- package/dist/renderengine.umd.js +25366 -15650
- package/documentation/REFERENCE.md +206 -0
- package/examples/decision-workbench.html +26 -0
- package/package.json +7 -1
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +219 -0
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +3 -0
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +114 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +52 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +5 -3
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +23 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +105 -2
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +4 -1
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +2 -2
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/RadarChart.js +7 -1
- package/src/charting/charts/RadialBarChart.js +5 -2
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +127 -4
- package/src/charting/charts/ScatterChart.js +13 -0
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +48 -5
- package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
- package/src/charting/charts/StreamGraphChart.js +10 -3
- package/src/charting/charts/SunburstChart.js +27 -7
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +28 -8
- package/src/charting/charts/ViolinChart.js +18 -0
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +109 -4
- package/src/charting/pivot/PivotGridChart.js +321 -33
- package/src/charting/pivot/PivotView.js +82 -0
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2995
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/index.js +3 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/parts/DecisionWorkbenchPart.js +58 -0
- package/src/workspace/parts/ViewsPart.js +196 -0
- package/src/workspace/styles.js +36 -1
|
@@ -169,15 +169,134 @@ export class Workspace extends HTMLElement {
|
|
|
169
169
|
}));
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/** Return every partId referenced by a registered perspective's layout. */
|
|
173
|
+
partIdsForPerspective(name) {
|
|
174
|
+
const layout = this.perspectives.get(name);
|
|
175
|
+
if (!layout) return [];
|
|
176
|
+
try {
|
|
177
|
+
return createLayout(layout).partIds();
|
|
178
|
+
} catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Activate a perspective containing `partId` (if not already visible), scroll
|
|
185
|
+
* the part's pane into the center of the viewport, and pulse a highlight ring.
|
|
186
|
+
*/
|
|
187
|
+
focusPart(partId, perspectiveName = null) {
|
|
188
|
+
if (!partId) return false;
|
|
189
|
+
const targetPerspective = perspectiveName
|
|
190
|
+
?? (this.activePerspective && this.partIdsForPerspective(this.activePerspective).includes(partId)
|
|
191
|
+
? this.activePerspective
|
|
192
|
+
: [...this.perspectives.keys()].find(p => this.partIdsForPerspective(p).includes(partId)));
|
|
193
|
+
if (targetPerspective && targetPerspective !== this.activePerspective) {
|
|
194
|
+
this.activatePerspective(targetPerspective);
|
|
195
|
+
}
|
|
196
|
+
const host = this._rootEl?.querySelector(`[data-part-id="${partId}"]`);
|
|
197
|
+
const pane = host?.closest('.ws-pane') ?? host;
|
|
198
|
+
if (!pane) return false;
|
|
199
|
+
pane.scrollIntoView?.({ behavior: 'smooth', block: 'center' });
|
|
200
|
+
pane.classList.remove('ws-pane-flash');
|
|
201
|
+
void pane.offsetWidth;
|
|
202
|
+
pane.classList.add('ws-pane-flash');
|
|
203
|
+
setTimeout(() => pane.classList.remove('ws-pane-flash'), 1500);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
172
207
|
_renderPerspectiveButtons() {
|
|
173
208
|
if (!this._perspectivesEl) return;
|
|
174
|
-
render(this._perspectivesEl, [...this.perspectives.keys()].map(name =>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
209
|
+
render(this._perspectivesEl, [...this.perspectives.keys()].map(name => {
|
|
210
|
+
const parts = this.partIdsForPerspective(name)
|
|
211
|
+
.map(id => this.getPart(id))
|
|
212
|
+
.filter(Boolean);
|
|
213
|
+
const hasSubmenu = parts.length > 1;
|
|
214
|
+
let listEl = null;
|
|
215
|
+
let submenuEl = null;
|
|
216
|
+
|
|
217
|
+
const buildSubmenuRows = (filterText = '') => {
|
|
218
|
+
const q = filterText.trim().toLowerCase();
|
|
219
|
+
const filtered = q
|
|
220
|
+
? parts.filter(p => (p.title ?? p.id).toLowerCase().includes(q))
|
|
221
|
+
: parts;
|
|
222
|
+
return filtered.map(part => ({
|
|
223
|
+
cls: 'ws-perspective-submenu-row',
|
|
224
|
+
children: [
|
|
225
|
+
{
|
|
226
|
+
tag: 'button',
|
|
227
|
+
cls: 'ws-perspective-submenu-item',
|
|
228
|
+
text: part.title ?? part.id,
|
|
229
|
+
attrs: { type: 'button', title: `Jump to ${part.title ?? part.id}` },
|
|
230
|
+
on: {
|
|
231
|
+
click: (e) => {
|
|
232
|
+
e.stopPropagation();
|
|
233
|
+
submenuEl?.classList.remove('ws-submenu-open');
|
|
234
|
+
this.focusPart(part.id, name);
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
...(part.actions ?? []).filter(a => !a.separator).map(action => ({
|
|
239
|
+
tag: 'button',
|
|
240
|
+
cls: 'ws-perspective-submenu-action',
|
|
241
|
+
text: action.label,
|
|
242
|
+
attrs: { type: 'button', title: action.title ?? action.label },
|
|
243
|
+
on: {
|
|
244
|
+
click: (e) => {
|
|
245
|
+
e.stopPropagation();
|
|
246
|
+
submenuEl?.classList.remove('ws-submenu-open');
|
|
247
|
+
action.onClick?.(part, e);
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
})),
|
|
251
|
+
],
|
|
252
|
+
}));
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
cls: 'ws-perspective-item',
|
|
257
|
+
on: {
|
|
258
|
+
mouseenter: () => submenuEl?.classList.add('ws-submenu-open'),
|
|
259
|
+
mouseleave: () => submenuEl?.classList.remove('ws-submenu-open'),
|
|
260
|
+
},
|
|
261
|
+
children: [
|
|
262
|
+
{
|
|
263
|
+
tag: 'button',
|
|
264
|
+
cls: ['ws-perspective-btn', name === this.activePerspective && 'ws-active'],
|
|
265
|
+
attrs: { type: 'button' },
|
|
266
|
+
on: { click: () => this.activatePerspective(name) },
|
|
267
|
+
children: [
|
|
268
|
+
{ tag: 'span', text: name },
|
|
269
|
+
hasSubmenu && { tag: 'span', cls: 'ws-perspective-caret', text: ' ▾' },
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
hasSubmenu && {
|
|
273
|
+
cls: 'ws-perspective-submenu',
|
|
274
|
+
ref: n => { submenuEl = n; },
|
|
275
|
+
children: [
|
|
276
|
+
parts.length > 8 && {
|
|
277
|
+
cls: 'ws-perspective-submenu-search-wrap',
|
|
278
|
+
children: [{
|
|
279
|
+
tag: 'input',
|
|
280
|
+
cls: 'ws-perspective-submenu-search',
|
|
281
|
+
attrs: { type: 'search', placeholder: `Filter ${name} (${parts.length})…` },
|
|
282
|
+
on: {
|
|
283
|
+
click: e => e.stopPropagation(),
|
|
284
|
+
input: e => {
|
|
285
|
+
if (listEl) render(listEl, buildSubmenuRows(e.target.value));
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
}],
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
cls: 'ws-perspective-submenu-list',
|
|
292
|
+
ref: n => { listEl = n; },
|
|
293
|
+
children: buildSubmenuRows(),
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
};
|
|
299
|
+
}));
|
|
181
300
|
}
|
|
182
301
|
|
|
183
302
|
/* ── Theming ────────────────────────────────────────────────────────── */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RenderEngine, ArchimateModel, ArchimateSpecializationSelector, DerivedRelationships, DiagramTabs, ModelTree,
|
|
3
3
|
SettingsManager, buildAnalysisPanel, buildModelTreePanel, fromArchimateView, parseArchiModel, toArchimateModel,
|
|
4
|
+
NavigatorModel, NavigatorPanel, buildNavigatorPanel,
|
|
4
5
|
parseArchiFile, toArchiFile, toArchiExport, showGlobalSettingsDialog, showPropertiesDialog, showDiagramPropertiesDialog,
|
|
5
6
|
} from '../index.js';
|
|
6
7
|
|
|
@@ -21,6 +22,9 @@ export const ARCHIMATE_SERVICES = Object.freeze({
|
|
|
21
22
|
SettingsManager,
|
|
22
23
|
buildAnalysisPanel,
|
|
23
24
|
buildModelTreePanel,
|
|
25
|
+
NavigatorModel,
|
|
26
|
+
NavigatorPanel,
|
|
27
|
+
buildNavigatorPanel,
|
|
24
28
|
fromArchimateView,
|
|
25
29
|
parseArchiModel,
|
|
26
30
|
parseArchiFile,
|
package/src/workspace/index.js
CHANGED
|
@@ -56,6 +56,9 @@ export {
|
|
|
56
56
|
LIGHT_THEME, DARK_THEME, MIDNIGHT_THEME, SEPIA_THEME,
|
|
57
57
|
} from './themes.js';
|
|
58
58
|
export { ChartPart } from './parts/ChartPart.js';
|
|
59
|
+
// Chart views in one part (they use ChartPart, so they sit with it, outside BUILTIN_PARTS).
|
|
60
|
+
export { ViewsPart } from './parts/ViewsPart.js';
|
|
61
|
+
export { DecisionWorkbenchPart } from './parts/DecisionWorkbenchPart.js';
|
|
59
62
|
export { DataModelPart } from './parts/DataModelPart.js';
|
|
60
63
|
export { RequirementsPart } from './parts/RequirementsPart.js';
|
|
61
64
|
export { ModelHosted } from './parts/ModelHosted.js';
|
|
@@ -281,7 +281,7 @@ export class ArchimateCanvasPart extends EditorPart {
|
|
|
281
281
|
if (!analysisHost) return;
|
|
282
282
|
analysisHost.replaceChildren(buildAnalysisPanel(doc, {
|
|
283
283
|
model, elementId: elementId && model.getElement(elementId) ? elementId : null,
|
|
284
|
-
onSelectItem: (info) => this.modelTree.select(info.id, info.kind),
|
|
284
|
+
onSelectItem: (info) => this.modelTree.select(info.id, info.kind), onSelectView: (viewId) => { if (viewId !== this.model.currentViewId) this.model.switchTo(viewId); },
|
|
285
285
|
}));
|
|
286
286
|
};
|
|
287
287
|
this.editor.onSelectionChange(({ templateId }) => {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DecisionWorkbenchPart — the Decision Workbench as one part: a SADIS workbench document
|
|
3
|
+
* (decisionengine's `{ kind: 'sadis-workbench', report, plan?, before? }`) shown as its views.
|
|
4
|
+
*
|
|
5
|
+
* Everything about showing views — tabs, the chart grid, notes, opening a file, being embeddable in
|
|
6
|
+
* any layout — is `ViewsPart`'s; this adds only what differs for this kind of document: reading it
|
|
7
|
+
* through `DecisionWorkbench` (which turns decisionengine's JSON into views), and the calendar its
|
|
8
|
+
* delivery plan is drawn on (when period 1 starts, how many days a period is). That split is why it
|
|
9
|
+
* is a subclass: a second kind of document is a second subclass, not a flag on one class.
|
|
10
|
+
*
|
|
11
|
+
* Spec: `ViewsPart`'s, plus `document` (a workbench document to show at once), `start` and
|
|
12
|
+
* `periodDays` (the calendar; default the report's as-of time and 14 days).
|
|
13
|
+
*/
|
|
14
|
+
import { ViewsPart } from './ViewsPart.js';
|
|
15
|
+
import { DecisionWorkbench } from '../../charting/decision/DecisionWorkbench.js';
|
|
16
|
+
|
|
17
|
+
export class DecisionWorkbenchPart extends ViewsPart {
|
|
18
|
+
static type = 'decision-workbench';
|
|
19
|
+
|
|
20
|
+
constructor(spec = {}) {
|
|
21
|
+
super({ empty: 'Open a workbench file: decisionengine\'s SADIS Explorer downloads one ("Download for the Decision Workbench").', ...spec });
|
|
22
|
+
this.calendar = { start: spec.start, periodDays: spec.periodDays ?? 14 };
|
|
23
|
+
if (spec.document !== undefined) this.read(spec.document);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
viewsFrom(doc) {
|
|
27
|
+
return DecisionWorkbench.from(doc, this.controlValues()).views().map((view) => view.toJSON());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
controls() {
|
|
31
|
+
const field = { padding: '2px 4px', font: 'inherit' };
|
|
32
|
+
return [
|
|
33
|
+
{ tag: 'label', children: [
|
|
34
|
+
{ tag: 'span', text: 'Period 1 starts ' },
|
|
35
|
+
{ tag: 'input', attrs: { type: 'date', value: this.calendar.start ?? '' }, style: field,
|
|
36
|
+
on: { change: (e) => { this.calendar.start = e.target.value || undefined; this.reread(); } } },
|
|
37
|
+
]},
|
|
38
|
+
{ tag: 'label', children: [
|
|
39
|
+
{ tag: 'span', text: 'Days per period ' },
|
|
40
|
+
{ tag: 'input', attrs: { type: 'number', min: '1', value: String(this.calendar.periodDays) }, style: { ...field, width: '4em' },
|
|
41
|
+
on: { change: (e) => { this.calendar.periodDays = Number(e.target.value) || 14; this.reread(); } } },
|
|
42
|
+
]},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
controlValues() {
|
|
47
|
+
return { start: this.calendar.start, periodDays: this.calendar.periodDays };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
read(doc) {
|
|
51
|
+
const ok = super.read(doc);
|
|
52
|
+
if (ok) {
|
|
53
|
+
const asOf = doc.report.asOf?.businessTime?.slice(0, 10);
|
|
54
|
+
this.say(`${doc.report.config}: ${doc.report.entries.length} candidates${asOf ? `, as of ${asOf}` : ''}`);
|
|
55
|
+
}
|
|
56
|
+
return ok;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViewsPart — a set of chart views in one part: a tab per view, each a grid of charts and its notes.
|
|
3
|
+
*
|
|
4
|
+
* A view is plain data — `{ id, title, charts: [{ title, chartType, data, options? }], notes? }`,
|
|
5
|
+
* `chartType` a CHART_CATALOG key — so anything that can say what it wants drawn can be shown here
|
|
6
|
+
* without a page of its own: a report, an analysis, a monitoring summary. The Decision Workbench
|
|
7
|
+
* (`DecisionWorkbenchPart`) is one; any other producer of views is another, by spec or by subclass.
|
|
8
|
+
*
|
|
9
|
+
* Why it is one part, not a perspective per view. A part drops into any layout — a tab, a split, a
|
|
10
|
+
* dashboard cell, a WorkArea document — beside whatever else the host shows; perspectives belong to
|
|
11
|
+
* the whole workspace, so a producer that registered its own would take over the host's. Inside, it
|
|
12
|
+
* composes the parts that already exist (`ChartPart` per chart, `ListPart` for notes) rather than
|
|
13
|
+
* drawing anything itself, so a chart here behaves as a chart anywhere: same sizing, same redraw.
|
|
14
|
+
*
|
|
15
|
+
* Why subclasses read files, not a reader in the spec. A spec stays JSON (the workspace serializes
|
|
16
|
+
* it), so a function cannot ride in it. The base reads a document that already is `{ views: [...] }`;
|
|
17
|
+
* a subclass that reads something else overrides `viewsFrom(doc)` (and `accept`, `controls()`,
|
|
18
|
+
* `controlValues()` for options its reader takes) — one subclass per kind of document.
|
|
19
|
+
*
|
|
20
|
+
* Spec: `{ views?, open?: boolean, empty?: string }`. With `open` (default true) a toolbar opens a
|
|
21
|
+
* file (`accept`); `read(doc)` does the same from code. The views shown are kept in `spec.views`,
|
|
22
|
+
* so a serialized workspace comes back showing them.
|
|
23
|
+
*/
|
|
24
|
+
import { Part } from '../Part.js';
|
|
25
|
+
import { el } from '../dom.js';
|
|
26
|
+
import { ChartPart } from './ChartPart.js';
|
|
27
|
+
import { ListPart } from './ListPart.js';
|
|
28
|
+
import { CHART_CATALOG } from '../../chartCatalog.js';
|
|
29
|
+
|
|
30
|
+
/** Chart categories drawn as small tiles across the top of a view rather than as full charts. */
|
|
31
|
+
const TILE_CATEGORIES = new Set(['gauge']);
|
|
32
|
+
|
|
33
|
+
export class ViewsPart extends Part {
|
|
34
|
+
static type = 'views';
|
|
35
|
+
|
|
36
|
+
/** File types the toolbar offers to open. */
|
|
37
|
+
static accept = '.json';
|
|
38
|
+
|
|
39
|
+
constructor(spec = {}) {
|
|
40
|
+
super(spec);
|
|
41
|
+
this.views = spec.views ?? [];
|
|
42
|
+
this.allowOpen = spec.open !== false;
|
|
43
|
+
this.empty = spec.empty ?? 'Open a file to show its views.';
|
|
44
|
+
this.active = this.views[0]?.id ?? null;
|
|
45
|
+
/** The chart and notes parts made for the views shown, torn down when they change. @private */
|
|
46
|
+
this._children = [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Where each view's charts go: tiles (gauge-like charts) share the top row, the rest take half a
|
|
51
|
+
* row each, or the whole row when a view has one; notes go underneath. Pure: the layout rule,
|
|
52
|
+
* kept apart from the DOM so it can be read and tested on its own.
|
|
53
|
+
* @returns {Array<{id, title, items: Array<{kind: 'chart'|'notes', index?: number, span: number, tile?: boolean}>}>}
|
|
54
|
+
*/
|
|
55
|
+
static layout(views) {
|
|
56
|
+
return views.map((view) => {
|
|
57
|
+
const charts = view.charts ?? [];
|
|
58
|
+
const isTile = (chart) => TILE_CATEGORIES.has(CHART_CATALOG.find((c) => c.key === chart.chartType)?.category);
|
|
59
|
+
const tiles = charts.filter(isTile).length;
|
|
60
|
+
const others = charts.length - tiles;
|
|
61
|
+
const items = charts.map((chart, index) => (isTile(chart)
|
|
62
|
+
? { kind: 'chart', index, tile: true, span: Math.max(1, Math.floor(12 / tiles)) }
|
|
63
|
+
: { kind: 'chart', index, tile: false, span: others === 1 ? 12 : 6 }));
|
|
64
|
+
if (view.notes?.length) items.push({ kind: 'notes', span: 12 });
|
|
65
|
+
return { id: view.id, title: view.title ?? view.id, items };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The views a document holds. The base takes `{ views: [...] }`; a subclass reads its own kind of document. */
|
|
70
|
+
viewsFrom(doc) {
|
|
71
|
+
if (!doc || !Array.isArray(doc.views)) throw new Error('Not a views document: it has no views list');
|
|
72
|
+
return doc.views;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Extra toolbar fields a subclass's reader takes (element specs). */
|
|
76
|
+
controls() { return []; }
|
|
77
|
+
|
|
78
|
+
/** The values of those fields, for `viewsFrom`. */
|
|
79
|
+
controlValues() { return {}; }
|
|
80
|
+
|
|
81
|
+
/** Show the views a document holds; a document that cannot be read is said so, and the views shown stay. */
|
|
82
|
+
read(doc) {
|
|
83
|
+
let views;
|
|
84
|
+
try {
|
|
85
|
+
views = this.viewsFrom(doc);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
this.say(err.message);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
this.document = doc;
|
|
91
|
+
this.show(views);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Show `views`, replacing those shown. */
|
|
96
|
+
show(views) {
|
|
97
|
+
this.views = views;
|
|
98
|
+
this.spec.views = views;
|
|
99
|
+
if (!views.some((v) => v.id === this.active)) this.active = views[0]?.id ?? null;
|
|
100
|
+
if (this.mounted) this.renderViews();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
say(text) {
|
|
104
|
+
if (this._message) this._message.textContent = text;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
render() {
|
|
108
|
+
Object.assign(this.container.style, { display: 'flex', flexDirection: 'column', height: '100%', minHeight: '0' });
|
|
109
|
+
if (this.allowOpen) {
|
|
110
|
+
this.container.appendChild(el({
|
|
111
|
+
cls: 'ws-views-toolbar',
|
|
112
|
+
style: { display: 'flex', gap: '12px', alignItems: 'center', flexWrap: 'wrap', padding: '6px 8px' },
|
|
113
|
+
children: [
|
|
114
|
+
{ tag: 'label', children: [
|
|
115
|
+
{ tag: 'span', text: 'Open ' },
|
|
116
|
+
{ tag: 'input', attrs: { type: 'file', accept: this.constructor.accept }, on: { change: (e) => this.openFile(e.target) } },
|
|
117
|
+
]},
|
|
118
|
+
...this.controls(),
|
|
119
|
+
{ tag: 'span', cls: 'ws-views-message', style: { opacity: '0.75' }, ref: (n) => { this._message = n; } },
|
|
120
|
+
],
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
this._tabs = this.container.appendChild(el({ cls: 'ws-views-tabs', style: { display: 'flex', gap: '4px', padding: '0 8px' } }));
|
|
124
|
+
this._body = this.container.appendChild(el({ cls: 'ws-views-body', style: { flex: '1 1 auto', minHeight: '0', overflow: 'auto' } }));
|
|
125
|
+
this.onDispose(() => this.clearChildren());
|
|
126
|
+
this.renderViews();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async openFile(input) {
|
|
130
|
+
const file = input.files?.[0];
|
|
131
|
+
if (!file) return;
|
|
132
|
+
let doc;
|
|
133
|
+
try {
|
|
134
|
+
doc = JSON.parse(await file.text());
|
|
135
|
+
} catch (err) {
|
|
136
|
+
this.say(`Not JSON: ${err.message}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.read(doc);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Re-read the document shown, as when a control's value changes. */
|
|
143
|
+
reread() {
|
|
144
|
+
if (this.document !== undefined) this.read(this.document);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
clearChildren() {
|
|
148
|
+
for (const part of this._children) part.dispose?.();
|
|
149
|
+
this._children = [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
renderViews() {
|
|
153
|
+
this.clearChildren();
|
|
154
|
+
this._tabs.textContent = '';
|
|
155
|
+
this._body.textContent = '';
|
|
156
|
+
if (!this.views.length) {
|
|
157
|
+
this._body.appendChild(el({ tag: 'p', cls: 'ws-views-empty', style: { padding: '12px', opacity: '0.75' }, text: this.empty }));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (const view of ViewsPart.layout(this.views)) {
|
|
161
|
+
this._tabs.appendChild(el({
|
|
162
|
+
tag: 'button', attrs: { type: 'button' }, cls: view.id === this.active ? 'ws-views-tab active' : 'ws-views-tab',
|
|
163
|
+
style: { fontWeight: view.id === this.active ? '600' : '400' },
|
|
164
|
+
text: view.title, on: { click: () => { this.active = view.id; this.renderViews(); } },
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
const view = this.views.find((v) => v.id === this.active);
|
|
168
|
+
const placed = ViewsPart.layout([view])[0];
|
|
169
|
+
const grid = this._body.appendChild(el({
|
|
170
|
+
cls: 'ws-views-grid',
|
|
171
|
+
style: { display: 'grid', gridTemplateColumns: 'repeat(12, 1fr)', gap: '8px', padding: '8px' },
|
|
172
|
+
}));
|
|
173
|
+
for (const item of placed.items) {
|
|
174
|
+
// A cell is a part host, as a layout's pane is (`.ws-part-host`: flex, the part fills it), so a
|
|
175
|
+
// chart here sizes to its cell exactly as it would to a pane; only the cell's height is its own.
|
|
176
|
+
const cell = grid.appendChild(el({
|
|
177
|
+
cls: 'ws-views-cell ws-part-host',
|
|
178
|
+
style: { gridColumn: `span ${item.span}`, height: item.kind === 'chart' ? (item.tile ? '130px' : '380px') : 'auto', minWidth: '0' },
|
|
179
|
+
}));
|
|
180
|
+
const part = item.kind === 'chart'
|
|
181
|
+
? new ChartPart(this.chartSpec(view, item.index))
|
|
182
|
+
: new ListPart({ id: `${this.id}-${view.id}-notes`, title: 'Notes', channel: `${this.id}:notes:${view.id}`, items: view.notes });
|
|
183
|
+
part.mount(cell, this.ctx);
|
|
184
|
+
this._children.push(part);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The ChartPart spec for a view's chart. */
|
|
189
|
+
chartSpec(view, index) {
|
|
190
|
+
const chart = view.charts[index];
|
|
191
|
+
return {
|
|
192
|
+
id: `${this.id}-${view.id}-${index}`, title: chart.title, chartType: chart.chartType, rendererType: 'svg',
|
|
193
|
+
options: { ...chart.options, title: chart.title, data: chart.data },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
package/src/workspace/styles.js
CHANGED
|
@@ -57,7 +57,42 @@ export const WORKSPACE_CSS = `
|
|
|
57
57
|
}
|
|
58
58
|
.ws-toolbar-title { font-weight: 600; }
|
|
59
59
|
.ws-toolbar-spacer { flex: 1; }
|
|
60
|
-
.ws-perspectives { display: flex; gap: 2px; }
|
|
60
|
+
.ws-perspectives { display: flex; gap: 2px; }
|
|
61
|
+
.ws-perspective-item { position: relative; display: inline-flex; }
|
|
62
|
+
.ws-perspective-caret { font-size: 10px; opacity: .75; }
|
|
63
|
+
.ws-perspective-submenu {
|
|
64
|
+
display: none; position: absolute; top: 100%; right: 0; z-index: 1600;
|
|
65
|
+
min-width: 260px; max-width: 380px; background: var(--ws-panel-bg); color: var(--ws-fg);
|
|
66
|
+
border: 1px solid var(--ws-border); border-radius: 0 0 6px 6px;
|
|
67
|
+
box-shadow: 0 8px 24px rgba(0,0,0,.28); padding: 4px 0;
|
|
68
|
+
}
|
|
69
|
+
.ws-perspective-submenu.ws-submenu-open { display: flex; flex-direction: column; }
|
|
70
|
+
.ws-perspective-submenu-search-wrap { padding: 4px 8px 6px; border-bottom: 1px solid var(--ws-border); }
|
|
71
|
+
.ws-perspective-submenu-search {
|
|
72
|
+
width: 100%; padding: 4px 8px; font: inherit; font-size: 12px;
|
|
73
|
+
background: var(--ws-bg); color: var(--ws-fg); border: 1px solid var(--ws-border); border-radius: 4px;
|
|
74
|
+
}
|
|
75
|
+
.ws-perspective-submenu-list { max-height: 68vh; overflow-y: auto; }
|
|
76
|
+
.ws-perspective-submenu-row {
|
|
77
|
+
display: flex; align-items: center; gap: 4px; padding: 1px 6px;
|
|
78
|
+
}
|
|
79
|
+
.ws-perspective-submenu-row:hover { background: rgba(127,127,127,.15); }
|
|
80
|
+
.ws-perspective-submenu-item {
|
|
81
|
+
flex: 1 1 auto; text-align: left; padding: 5px 6px; font: inherit; font-size: 12px;
|
|
82
|
+
background: transparent; color: var(--ws-fg); border: none; cursor: pointer;
|
|
83
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
84
|
+
}
|
|
85
|
+
.ws-perspective-submenu-action {
|
|
86
|
+
flex: 0 0 auto; padding: 2px 6px; font: inherit; font-size: 10px; font-family: monospace;
|
|
87
|
+
background: rgba(127,127,127,.15); color: var(--ws-fg);
|
|
88
|
+
border: 1px solid var(--ws-border); border-radius: 3px; cursor: pointer; opacity: .8;
|
|
89
|
+
}
|
|
90
|
+
.ws-perspective-submenu-action:hover { opacity: 1; background: var(--ws-accent); color: #000; border-color: var(--ws-accent); }
|
|
91
|
+
@keyframes ws-pane-flash-anim {
|
|
92
|
+
0%, 30% { box-shadow: 0 0 0 3px var(--ws-accent), 0 6px 20px rgba(0,0,0,.22); }
|
|
93
|
+
100% { box-shadow: none; }
|
|
94
|
+
}
|
|
95
|
+
.ws-pane-flash { animation: ws-pane-flash-anim 1.4s ease-out; }
|
|
61
96
|
.ws-perspective-btn {
|
|
62
97
|
padding: 4px 12px; font: inherit; font-size: 12px; cursor: pointer;
|
|
63
98
|
color: var(--ws-toolbar-fg); background: rgba(127,127,127,.12);
|