@peter.prib/renderengine 0.4.0 → 0.7.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 +4 -0
- package/REFERENCE.md +267 -17
- package/dist/renderengine-workspace.umd.js +5289 -2895
- package/dist/renderengine.umd.js +4243 -2120
- package/examples/archimate.html +8 -28
- package/examples/data-model.html +45 -556
- package/package.json +70 -70
- package/src/archimateArchiFormat.js +11 -3
- package/src/archimateExchangeFormat.js +17 -9
- package/src/charting/ArchimateDiagramEditor.js +1 -1
- package/src/charting/Axis.js +20 -0
- package/src/charting/DiagramEditor.js +35 -3
- package/src/charting/Dialog.js +195 -0
- package/src/charting/DuplicatesDialog.js +108 -0
- package/src/charting/FormDialog.js +28 -97
- package/src/charting/InteractionManager.js +3 -0
- package/src/charting/Legend.js +45 -2
- package/src/charting/MergeCompareDialog.js +81 -0
- package/src/charting/PopupMenu.js +12 -3
- package/src/charting/SettingsDialog.js +6 -2
- package/src/charting/SettingsManager.js +14 -1
- package/src/charting/archimate/ArchimateCanvasEditor.js +190 -15
- package/src/charting/archimate/ArchimatePalette.js +1 -1
- package/src/charting/charts/AreaChart.js +5 -2
- package/src/charting/charts/BarChart.js +5 -3
- package/src/charting/charts/BarChart3DOrbit.js +5 -7
- package/src/charting/charts/BubbleChart.js +5 -7
- package/src/charting/charts/BubbleChart3D.js +5 -7
- package/src/charting/charts/CandlestickChart.js +3 -1
- package/src/charting/charts/DensityChart.js +4 -6
- package/src/charting/charts/GroupedBarChart.js +2 -2
- package/src/charting/charts/LineChart.js +5 -2
- package/src/charting/charts/ParallelCoordinatesChart.js +2 -2
- package/src/charting/charts/PipeChart.js +4 -6
- package/src/charting/charts/RadarChart.js +5 -6
- package/src/charting/charts/ScatterChart.js +5 -7
- package/src/charting/charts/StackedBarChart.js +4 -2
- package/src/charting/charts/StackedBarChart3DOrbit.js +5 -7
- package/src/charting/charts/StreamGraphChart.js +5 -2
- package/src/charting/charts/WaterfallChart.js +2 -1
- package/src/charting/content/ContentSource.js +129 -0
- package/src/charting/content/ContentSources.js +77 -0
- package/src/charting/datamodel/DataModel.js +116 -0
- package/src/charting/diagrams/ArchimateModel.js +38 -3
- package/src/charting/diagrams/ArchimateModelChecker.js +13 -4
- package/src/charting/diagrams/MergeElementsCommand.js +124 -101
- package/src/charting/diagrams/SvgZoom.js +20 -1
- package/src/charting/diagrams/duplicates.js +95 -13
- package/src/charting/duplicateSearchForm.js +58 -0
- package/src/charting/modelTree/ModelTree.js +239 -202
- package/src/charting/modelTree/ModelTreePanel.js +955 -919
- package/src/charting/modelTree/TreeBuilder.js +93 -45
- package/src/charting/modelTree/TreeViewMode.js +9 -6
- package/src/charting/modelTree/viewFolders.js +15 -6
- package/src/charting/requirements/AzureDevOpsApi.js +55 -0
- package/src/charting/requirements/GitLabApi.js +43 -0
- package/src/charting/requirements/JiraApi.js +69 -0
- package/src/charting/requirements/TrackerApi.js +142 -0
- package/src/charting/requirements/TrackerExport.js +3 -2
- package/src/charting/requirements/index.js +5 -0
- package/src/charting/requirements/trackerApis.js +21 -0
- package/src/icons/IconSet.js +86 -0
- package/src/icons/index.js +59 -0
- package/src/index.js +67 -10
- package/src/workspace/dom.js +56 -42
- package/src/workspace/index.js +1 -1
- package/src/workspace/nav/NavEntry.js +42 -0
- package/src/workspace/nav/NavGroup.js +43 -0
- package/src/workspace/nav/NavItem.js +53 -0
- package/src/workspace/nav/NavMenu.js +143 -0
- package/src/workspace/nav/NavSection.js +17 -0
- package/src/workspace/nav/index.js +5 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +8 -3
- package/src/workspace/parts/DataModelPart.js +91 -0
- package/src/workspace/parts/EditorPart.js +8 -0
- package/src/workspace/parts/ModelHosted.js +1 -0
- package/src/workspace/parts/ModelsPart.js +765 -622
- package/src/workspace/styles.js +28 -1
|
@@ -33,6 +33,9 @@ import { ElementFilterRule } from './ElementFilterRule.js';
|
|
|
33
33
|
import { ArchimateViewpoints, ArchimateNotation } from '../../archimate.js';
|
|
34
34
|
import { PropertyListEditor } from '../PropertyListEditor.js';
|
|
35
35
|
import { FormDialog } from '../FormDialog.js';
|
|
36
|
+
import { duplicateSearchFields, duplicateSearchOptions, validateDuplicateSearch } from '../duplicateSearchForm.js';
|
|
37
|
+
import { DuplicatesDialog } from '../DuplicatesDialog.js';
|
|
38
|
+
import { MergeCompareDialog } from '../MergeCompareDialog.js';
|
|
36
39
|
import { elementPropertyFields, elementEditCommand, conceptEditCommand, manyElementsFields, manyElementsEditCommands } from './elementProperties.js';
|
|
37
40
|
|
|
38
41
|
/**
|
|
@@ -130,7 +133,10 @@ export class ArchimateCanvasEditor extends DiagramEditor {
|
|
|
130
133
|
const items = this.templateMenu?.saveAsTemplateItems(this.view.id, { itemIds: [...this.arrangement.movers(ids)] }) ?? [];
|
|
131
134
|
const element = this._elementOf(elementId);
|
|
132
135
|
const missing = element ? this.missingRelationshipsOf(elementId) : [];
|
|
136
|
+
// Two or more elements chosen: merge them into one (choose the one kept).
|
|
137
|
+
const merging = this.elementIdsOf(ids);
|
|
133
138
|
return [
|
|
139
|
+
...(merging.length > 1 ? [{ label: `Merge ${merging.length} elements…`, action: () => this.askMerge(merging) }] : []),
|
|
134
140
|
...(element ? [{ label: 'Generate View For This', action: () => this.model.generateView([element.id]) }] : []),
|
|
135
141
|
...(missing.length ? [{ label: `Add Missing Relationships (${missing.length})`, action: () => this.addMissingRelationships(elementId) }] : []),
|
|
136
142
|
...hintMenuItems(this.view, elementId, { onChange: () => this.render() }),
|
|
@@ -168,7 +174,94 @@ export class ArchimateCanvasEditor extends DiagramEditor {
|
|
|
168
174
|
|
|
169
175
|
/** The View menu, and Generate roadmap… (elements with dates on a time line). */
|
|
170
176
|
viewMenuItems() {
|
|
171
|
-
|
|
177
|
+
const repeated = this.view?.repeatedBoxes?.() ?? [];
|
|
178
|
+
return [
|
|
179
|
+
...super.viewMenuItems(),
|
|
180
|
+
{ label: `Remove repeated boxes${repeated.length ? ` (${repeated.length})` : ''}`, disabled: !repeated.length || undefined, action: () => this.removeRepeatedBoxes() },
|
|
181
|
+
{ label: 'Generate roadmap…', action: () => this.askRoadmap() },
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The model's own: Find likely duplicates…. */
|
|
186
|
+
modelMenuItems() {
|
|
187
|
+
return [...super.modelMenuItems(), { label: 'Find likely duplicates…', action: () => this.askDuplicates() }];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Take off this view the boxes that only repeat another box of their
|
|
192
|
+
* element (ArchimateView#repeatedBoxes: same label, same lines), with
|
|
193
|
+
* their lines, as one undo step. Returns the command, or null with none.
|
|
194
|
+
*/
|
|
195
|
+
removeRepeatedBoxes() {
|
|
196
|
+
const ids = this.view.repeatedBoxes();
|
|
197
|
+
if (!ids.length) return null;
|
|
198
|
+
const cmd = this.deleteTemplatesCommand(ids);
|
|
199
|
+
cmd.label = `Remove ${ids.length} repeated box${ids.length === 1 ? '' : 'es'}`;
|
|
200
|
+
this._executeCommand(cmd);
|
|
201
|
+
return cmd;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Where Find likely duplicates… can look: the whole model, the elements on
|
|
206
|
+
* the view in front, the elements selected (two or more). [key, label, element ids or null].
|
|
207
|
+
*/
|
|
208
|
+
duplicateScopes() {
|
|
209
|
+
const onView = this.elementIdsOf([...(this.view?.elementInstances?.keys() ?? [])]);
|
|
210
|
+
const selected = this.elementIdsOf(this.getSelectedIds());
|
|
211
|
+
return [
|
|
212
|
+
['model', 'The whole model', null],
|
|
213
|
+
...(onView.length > 1 ? [['view', `This view (${this.view.name || this.view.id})`, onView]] : []),
|
|
214
|
+
...(selected.length > 1 ? [['selection', `The ${selected.length} elements selected`, selected]] : []),
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Find likely duplicates…: where to look (duplicateScopes), how to compare
|
|
220
|
+
* (names, documentation or a property's value; exactly or by their words)
|
|
221
|
+
* and what noise to leave out (duplicateSearchFields), then the groups
|
|
222
|
+
* found, each with Merge… (showDuplicates).
|
|
223
|
+
*/
|
|
224
|
+
askDuplicates() {
|
|
225
|
+
const names = [...new Set(this.model.elements().flatMap(e => (e.properties ?? []).map(p => p.name ?? p.key)).filter(Boolean))].sort();
|
|
226
|
+
const specializations = [...new Set(this.model.elements().map(e => e.specialization).filter(Boolean))].sort();
|
|
227
|
+
const scopes = this.duplicateScopes();
|
|
228
|
+
return new FormDialog({
|
|
229
|
+
title: 'Find likely duplicates',
|
|
230
|
+
description: 'Elements are compared by their words: case, punctuation, word order and the noise words do not count.',
|
|
231
|
+
fields: duplicateSearchFields({ scope: { label: 'Look in', choices: scopes.map(([key, label]) => [key, label]) }, properties: names, specializations }),
|
|
232
|
+
submitLabel: 'Find',
|
|
233
|
+
idPrefix: 'duplicates-',
|
|
234
|
+
document: this.document ?? globalThis.document,
|
|
235
|
+
validate: validateDuplicateSearch,
|
|
236
|
+
onApply: (values) => {
|
|
237
|
+
const { options, scope } = duplicateSearchOptions(values);
|
|
238
|
+
this.showDuplicates(options, { within: scopes.find(([key]) => key === scope)?.[2] ?? null });
|
|
239
|
+
},
|
|
240
|
+
}).open();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The elements that look like one thing twice (the model check's likely
|
|
245
|
+
* duplicates, compared as `options` say: DUPLICATE_OPTIONS; `within`: only
|
|
246
|
+
* among these element ids), each group with Merge….
|
|
247
|
+
*/
|
|
248
|
+
showDuplicates(options = {}, { within = null } = {}) {
|
|
249
|
+
const find = () => this.model.checker(this.checkerOptions()).possibleDuplicates(options, { within });
|
|
250
|
+
const groups = find();
|
|
251
|
+
new DuplicatesDialog({
|
|
252
|
+
groups,
|
|
253
|
+
refind: find,
|
|
254
|
+
merge: async (group) => !!(await this.askMerge(group.ids)),
|
|
255
|
+
onReveal: (group) => this.revealFinding(group),
|
|
256
|
+
empty: 'No elements look like the same thing twice.',
|
|
257
|
+
document: this.document ?? globalThis.document,
|
|
258
|
+
}).open();
|
|
259
|
+
return groups;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The model elements of some boxes, each once (a box that is not an element is left out). */
|
|
263
|
+
elementIdsOf(instanceIds) {
|
|
264
|
+
return [...new Set(instanceIds.map(id => this._elementOf(id)?.id).filter(Boolean))];
|
|
172
265
|
}
|
|
173
266
|
|
|
174
267
|
/**
|
|
@@ -302,11 +395,73 @@ export class ArchimateCanvasEditor extends DiagramEditor {
|
|
|
302
395
|
}
|
|
303
396
|
|
|
304
397
|
/**
|
|
305
|
-
*
|
|
306
|
-
* (
|
|
307
|
-
*
|
|
398
|
+
* What differs among some elements, for the merge to choose: their
|
|
399
|
+
* documentation (when two or more have some, and it differs), each property
|
|
400
|
+
* with different values, and the specialization.
|
|
401
|
+
* @returns {{documentation: Array<[string, string]>, properties: Map<string, Array<[string, string]>>, specializations: string[]}}
|
|
402
|
+
* each choice [value, label]
|
|
403
|
+
*/
|
|
404
|
+
mergeChoices(elements) {
|
|
405
|
+
const name = (e) => e.name || e.id;
|
|
406
|
+
const documented = elements.filter(e => (e.documentation ?? '').trim());
|
|
407
|
+
const texts = [...new Set(documented.map(e => e.documentation.trim()))];
|
|
408
|
+
const properties = new Map();
|
|
409
|
+
for (const e of elements) {
|
|
410
|
+
for (const p of e.properties ?? []) {
|
|
411
|
+
const key = p.name ?? p.key;
|
|
412
|
+
if (!properties.has(key)) properties.set(key, new Map());
|
|
413
|
+
const values = properties.get(key);
|
|
414
|
+
values.set(String(p.value ?? ''), [...(values.get(String(p.value ?? '')) ?? []), name(e)]);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const specializations = [...new Set(elements.map(e => e.specialization).filter(Boolean))];
|
|
418
|
+
return {
|
|
419
|
+
documentation: texts.length > 1 ? texts.map(t => [t, `${documented.filter(e => e.documentation.trim() === t).map(name).join(', ')}: ${t.length > 50 ? `${t.slice(0, 50)}…` : t}`]) : [],
|
|
420
|
+
properties: new Map([...properties].filter(([, values]) => values.size > 1)
|
|
421
|
+
.map(([key, values]) => [key, [...values].map(([value, from]) => [value, `${value || '(empty)'} (${from.join(', ')})`])])),
|
|
422
|
+
specializations: specializations.length > 1 ? specializations : [],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Some elements side by side, for a merge (MergeCompareDialog's rows): their
|
|
428
|
+
* names, types, specializations, documentation, each property, their
|
|
429
|
+
* relationships (kind, direction, the element at the other end) and the
|
|
430
|
+
* views they are on.
|
|
308
431
|
*/
|
|
309
|
-
|
|
432
|
+
mergeComparison(elements) {
|
|
433
|
+
const nameOf = (id) => this.model.getElement(id)?.name || id;
|
|
434
|
+
const valueOf = (e, key) => (e.properties ?? []).filter(p => (p.name ?? p.key) === key).map(p => p.value ?? '').join(', ');
|
|
435
|
+
const keys = [...new Set(elements.flatMap(e => (e.properties ?? []).map(p => p.name ?? p.key)).filter(Boolean))];
|
|
436
|
+
const lines = (e) => {
|
|
437
|
+
const { incoming, outgoing } = this.model.relationshipsFor(e.id);
|
|
438
|
+
return [
|
|
439
|
+
...outgoing.map(r => `${r.kind} → ${nameOf(r.target)}`),
|
|
440
|
+
...incoming.map(r => `${r.kind} ← ${nameOf(r.source)}`),
|
|
441
|
+
].sort().join('\n');
|
|
442
|
+
};
|
|
443
|
+
const views = (e) => this.model.views().filter(v => v.elementInstancesFor?.(e.id).length).map(v => v.name || v.id).join('\n');
|
|
444
|
+
return [
|
|
445
|
+
{ label: 'Name', values: elements.map(e => e.name ?? '') },
|
|
446
|
+
{ label: 'Type', values: elements.map(e => e.type ?? '') },
|
|
447
|
+
...(elements.some(e => e.specialization) ? [{ label: 'Specialization', values: elements.map(e => e.specialization ?? '') }] : []),
|
|
448
|
+
{ label: 'Documentation', values: elements.map(e => (e.documentation ?? '').trim()) },
|
|
449
|
+
...keys.map(key => ({ label: `${key} (property)`, values: elements.map(e => valueOf(e, key)) })),
|
|
450
|
+
{ label: 'Relationships', values: elements.map(lines) },
|
|
451
|
+
{ label: 'Views', values: elements.map(views) },
|
|
452
|
+
];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Merge some elements into one, one undo step (ArchimateModel#mergeCommand).
|
|
457
|
+
* Their relationships always come together. When they differ in anything
|
|
458
|
+
* but the views they are on, they are shown side by side
|
|
459
|
+
* (MergeCompareDialog) to choose which is kept and, where they differ, the
|
|
460
|
+
* documentation (one, or all joined), each property's value, the
|
|
461
|
+
* specialization; OK merges, Cancel leaves them. Alike, the first is kept
|
|
462
|
+
* without asking. Resolves to the merge's command, or null.
|
|
463
|
+
*/
|
|
464
|
+
async askMerge(elementIds) {
|
|
310
465
|
const elements = elementIds.map(id => this.model.getElement(id)).filter(Boolean);
|
|
311
466
|
if (elements.length < 2) return null;
|
|
312
467
|
const used = (e) => {
|
|
@@ -314,20 +469,40 @@ export class ArchimateCanvasEditor extends DiagramEditor {
|
|
|
314
469
|
const { incoming, outgoing } = this.model.relationshipsFor(e.id);
|
|
315
470
|
return `${views} view${views === 1 ? '' : 's'}, ${incoming.length + outgoing.length} relationship${incoming.length + outgoing.length === 1 ? '' : 's'}`;
|
|
316
471
|
};
|
|
317
|
-
|
|
472
|
+
const ids = elements.map(e => e.id);
|
|
473
|
+
const rows = this.mergeComparison(elements);
|
|
474
|
+
const compare = new MergeCompareDialog({
|
|
318
475
|
title: `Merge ${elements.length} elements`,
|
|
319
|
-
description: 'The one kept takes the others\' relationships, their boxes on every view, and the
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
idPrefix: 'merge-',
|
|
476
|
+
description: 'The one kept takes the others\' relationships, their boxes on every view, and the properties it lacks; the others go. Where they differ (marked ≠), choose.',
|
|
477
|
+
columns: elements.map(e => `${e.name || e.id} (${used(e)})`),
|
|
478
|
+
rows,
|
|
323
479
|
document: this.document ?? globalThis.document,
|
|
324
|
-
|
|
325
|
-
|
|
480
|
+
});
|
|
481
|
+
// Views apart (their boxes all come to the kept one), nothing to choose: the first is kept.
|
|
482
|
+
if (!rows.some(row => row.label !== 'Views' && MergeCompareDialog.differs(row))) return this.mergeElements(ids[0], ids.slice(1));
|
|
483
|
+
const { documentation, properties, specializations } = this.mergeChoices(elements);
|
|
484
|
+
const JOINED = '\u0000joined';
|
|
485
|
+
const propertyKeys = [...properties.keys()];
|
|
486
|
+
compare.fields = [
|
|
487
|
+
{ key: 'keep', label: 'Keep', type: 'select', value: ids[0], choices: elements.map(e => [e.id, `${e.name || e.id} (${used(e)})`]) },
|
|
488
|
+
...(documentation.length ? [{ key: 'documentation', label: 'Documentation', type: 'select', value: JOINED, choices: [[JOINED, 'All of them, joined'], ...documentation] }] : []),
|
|
489
|
+
...(specializations.length ? [{ key: 'specialization', label: 'Specialization', type: 'select', value: specializations[0], choices: specializations.map(s => [s, s]) }] : []),
|
|
490
|
+
...propertyKeys.map((key, i) => ({ key: `property-${i}`, label: key, type: 'select', value: properties.get(key)[0][0], choices: properties.get(key) })),
|
|
491
|
+
];
|
|
492
|
+
const values = await compare.ask();
|
|
493
|
+
if (!values) return null;
|
|
494
|
+
const { keep } = values;
|
|
495
|
+
const joined = () => [...new Set(elements.map(e => (e.documentation ?? '').trim()).filter(Boolean))].join('\n\n');
|
|
496
|
+
return this.mergeElements(keep, ids.filter(id => id !== keep), {
|
|
497
|
+
...(documentation.length && { documentation: values.documentation === JOINED ? joined() : values.documentation }),
|
|
498
|
+
...(specializations.length && { specialization: values.specialization }),
|
|
499
|
+
properties: Object.fromEntries(propertyKeys.map((key, i) => [key, values[`property-${i}`]])),
|
|
500
|
+
});
|
|
326
501
|
}
|
|
327
502
|
|
|
328
|
-
/** Merge elements into one (`keepId`), one undo step; the canvas and tree redraw. */
|
|
329
|
-
mergeElements(keepId, otherIds) {
|
|
330
|
-
const cmd = this.model.mergeCommand(keepId, otherIds);
|
|
503
|
+
/** Merge elements into one (`keepId`), one undo step; the canvas and tree redraw. `choices`: MergeElementsCommand's. */
|
|
504
|
+
mergeElements(keepId, otherIds, choices = {}) {
|
|
505
|
+
const cmd = this.model.mergeCommand(keepId, otherIds, choices);
|
|
331
506
|
this._executeCommand(cmd);
|
|
332
507
|
return cmd;
|
|
333
508
|
}
|
|
@@ -17,7 +17,7 @@ export class ArchimatePalette {
|
|
|
17
17
|
* Tiles (one place, for every palette): a picture only, `columns` to a row,
|
|
18
18
|
* its name on hover. A tile is there to say which type to drag, so it is small.
|
|
19
19
|
*/
|
|
20
|
-
static TILE = Object.freeze({ icon:
|
|
20
|
+
static TILE = Object.freeze({ icon: 12, columns: 6, padding: '2px 1px', gap: '2px' });
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* The groups for a version: each layer that has types, then ArchiMate 4's
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
1
2
|
import { Chart } from '../Chart.js';
|
|
2
3
|
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
3
4
|
import { Legend } from '../Legend.js';
|
|
@@ -54,7 +55,9 @@ export class AreaChart extends Chart {
|
|
|
54
55
|
this.renderer.path('M' + topPts.map(p => `${p.px},${p.py}`).join(' L'), 'none', { color, width: 2 });
|
|
55
56
|
|
|
56
57
|
if (seriesIndex === 0) {
|
|
58
|
+
const step = Axis.categoryLabelStep(s.values.map(d => d.label), width, { fontSize: 10 });
|
|
57
59
|
s.values.forEach((d, i) => {
|
|
60
|
+
if (i % step) return;
|
|
58
61
|
this.renderer.text(d.label, x + i * pointMargin, y + height + 20, { textAnchor, fontSize: '10px' });
|
|
59
62
|
});
|
|
60
63
|
}
|
|
@@ -62,8 +65,8 @@ export class AreaChart extends Chart {
|
|
|
62
65
|
|
|
63
66
|
if (isMultiline && showLegend) {
|
|
64
67
|
const legendItems = series.map((s, i) => ({ name: s.name, color: palette.colorAt(i) }));
|
|
65
|
-
|
|
66
|
-
items: legendItems,
|
|
68
|
+
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
69
|
+
items: legendItems,
|
|
67
70
|
itemHeight: 20, markerSize: 12,
|
|
68
71
|
}).draw(this.renderer);
|
|
69
72
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
1
2
|
import { Legend } from '../Legend.js';
|
|
2
3
|
import { DataFilter } from '../DataFilter.js';
|
|
3
4
|
import { BarChartBase } from './BarChartBase.js';
|
|
@@ -27,6 +28,7 @@ export class BarChart extends BarChartBase {
|
|
|
27
28
|
const data = this.data;
|
|
28
29
|
const options = this.options;
|
|
29
30
|
const { x, y, width, height } = this._plotBox;
|
|
31
|
+
const labelStep = Axis.categoryLabelStep(data.map(d => d.label), width);
|
|
30
32
|
const { barColor = 'steelblue', colors } = options;
|
|
31
33
|
const { bandWidth, margin } = this._bandScale(data.length);
|
|
32
34
|
const textAnchor = this.renderer.centerTextAnchor;
|
|
@@ -43,7 +45,7 @@ export class BarChart extends BarChartBase {
|
|
|
43
45
|
|
|
44
46
|
this.renderer.rect(barX, barY, bandWidth, barHeight, color, null, { bar: true, dataIndex: i, tooltip: `${d.label}: ${d.value}` });
|
|
45
47
|
this.renderer.text(String(d.value), barX + bandWidth / 2, barY - 5, { textAnchor, fontSize: '10px', fill: '#333' });
|
|
46
|
-
this.renderer.text(d.label, barX + bandWidth / 2, y + height + 20, { textAnchor });
|
|
48
|
+
if (i % labelStep === 0) this.renderer.text(d.label, barX + bandWidth / 2, y + height + 20, { textAnchor });
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
51
|
|
|
@@ -60,8 +62,8 @@ export class BarChart extends BarChartBase {
|
|
|
60
62
|
hidden: this._dataFilter.has(i),
|
|
61
63
|
dataIndex: i,
|
|
62
64
|
}));
|
|
63
|
-
|
|
64
|
-
items: legendItems,
|
|
65
|
+
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
66
|
+
items: legendItems,
|
|
65
67
|
itemHeight: 20, markerSize: 10,
|
|
66
68
|
}).draw(this.renderer);
|
|
67
69
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Legend } from '../Legend.js';
|
|
1
2
|
import { ColorPalette } from '../ColorPalette.js';
|
|
2
3
|
import { Chart3D } from '../Chart3D.js';
|
|
3
4
|
|
|
@@ -42,12 +43,9 @@ export class BarChart3DOrbit extends Chart3D {
|
|
|
42
43
|
this.drawBox(this.renderer, bar.x0, bar.x1, 0, bar.y1, bar.z0, bar.z1, bar.color, toScreen);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.renderer.text(String(label), legX + 16, legY + 4, { textAnchor: 'start', fontSize: '11px', fill: '#374151' });
|
|
50
|
-
legY += 18;
|
|
51
|
-
});
|
|
46
|
+
new Legend({
|
|
47
|
+
items: zLabels.map((label, i) => ({ label: String(label), color: palette.colorAt(i) })),
|
|
48
|
+
x: bx + width - 100, y: by + 5, itemHeight: 18, fontSize: '11px', marker: 'circle',
|
|
49
|
+
}).draw(this.renderer);
|
|
52
50
|
}
|
|
53
51
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Legend } from '../Legend.js';
|
|
1
2
|
import { Chart } from '../Chart.js';
|
|
2
3
|
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
3
4
|
import { ColorPalette } from '../ColorPalette.js';
|
|
@@ -87,13 +88,10 @@ export class BubbleChart extends Chart {
|
|
|
87
88
|
|
|
88
89
|
// Category color legend
|
|
89
90
|
if (catField && catColors.size > 0) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
this.renderer.text(String(cat), legX + 16, legY + 4, { textAnchor: 'start', fontSize: '11px', fill: '#374151' });
|
|
95
|
-
legY += 18;
|
|
96
|
-
}
|
|
91
|
+
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
92
|
+
items: [...catColors].map(([cat, color]) => ({ label: String(cat), color })),
|
|
93
|
+
itemHeight: 18, fontSize: '11px', marker: 'circle',
|
|
94
|
+
}).draw(this.renderer);
|
|
97
95
|
}
|
|
98
96
|
}
|
|
99
97
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Legend } from '../Legend.js';
|
|
1
2
|
import { ColorPalette } from '../ColorPalette.js';
|
|
2
3
|
import { Chart3D } from '../Chart3D.js';
|
|
3
4
|
import { ChartData } from '../ChartData.js';
|
|
@@ -66,13 +67,10 @@ export class BubbleChart3D extends Chart3D {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
if (catField && catColors.size > 0) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.renderer.text(String(cat), legX + 16, legY + 4, { textAnchor: 'start', fontSize: '11px', fill: '#374151' });
|
|
74
|
-
legY += 18;
|
|
75
|
-
}
|
|
70
|
+
new Legend({
|
|
71
|
+
items: [...catColors].map(([cat, color]) => ({ label: String(cat), color })),
|
|
72
|
+
x: bx + width - 90, y: by + 5, itemHeight: 18, fontSize: '11px', marker: 'circle',
|
|
73
|
+
}).draw(this.renderer);
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
76
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
1
2
|
import { Chart } from '../Chart.js';
|
|
2
3
|
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
3
4
|
|
|
@@ -22,6 +23,7 @@ export class CandlestickChart extends Chart {
|
|
|
22
23
|
const vMin = Math.min(...data.map(d => d.low));
|
|
23
24
|
const range = (vMax - vMin) || 1;
|
|
24
25
|
const slotWidth = width / data.length;
|
|
26
|
+
const labelStep = Axis.categoryLabelStep(data.map(d => d.label), width, { fontSize: 10 });
|
|
25
27
|
const bodyWidth = slotWidth * 0.6;
|
|
26
28
|
const toY = (v) => y + height - ((v - vMin) / range) * height;
|
|
27
29
|
const textAnchor = this.renderer.centerTextAnchor;
|
|
@@ -39,7 +41,7 @@ export class CandlestickChart extends Chart {
|
|
|
39
41
|
this.renderer.rect(cx - bodyWidth / 2, bodyTop, bodyWidth, Math.max(1, bodyBottom - bodyTop), color, null, {
|
|
40
42
|
tooltip: `${d.label}: O ${d.open} H ${d.high} L ${d.low} C ${d.close}`,
|
|
41
43
|
});
|
|
42
|
-
if (i %
|
|
44
|
+
if (i % labelStep === 0) {
|
|
43
45
|
this.renderer.text(d.label, cx, y + height + 20, { textAnchor, fontSize: '10px' });
|
|
44
46
|
}
|
|
45
47
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Legend } from '../Legend.js';
|
|
1
2
|
import { Chart } from '../Chart.js';
|
|
2
3
|
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
3
4
|
import { ColorPalette } from '../ColorPalette.js';
|
|
@@ -62,12 +63,9 @@ export class DensityChart extends Chart {
|
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
if (showLegend) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
this.renderer.rect(legendX, legendY, 12, 12, palette.colorAt(i), null);
|
|
69
|
-
this.renderer.text(d.label, legendX + 20, legendY + 6, { fontSize: '12px', fill: '#333' });
|
|
70
|
-
});
|
|
66
|
+
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
67
|
+
items: data.map((d, i) => ({ label: d.label, color: palette.colorAt(i) })), markerSize: 12,
|
|
68
|
+
}).draw(this.renderer);
|
|
71
69
|
}
|
|
72
70
|
}
|
|
73
71
|
}
|
|
@@ -79,8 +79,8 @@ export class GroupedBarChart extends BarChartBase {
|
|
|
79
79
|
color: this._palette.colorAt(i),
|
|
80
80
|
dataIndex: i,
|
|
81
81
|
}));
|
|
82
|
-
|
|
83
|
-
items: legendItems,
|
|
82
|
+
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
83
|
+
items: legendItems,
|
|
84
84
|
itemHeight: 20, markerSize: 10,
|
|
85
85
|
}).draw(this.renderer);
|
|
86
86
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
1
2
|
import { Chart } from '../Chart.js';
|
|
2
3
|
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
3
4
|
import { Legend } from '../Legend.js';
|
|
@@ -104,7 +105,9 @@ export class LineChart extends Chart {
|
|
|
104
105
|
// series' own seriesDepth (not the raw max `depth`) for the x-shift
|
|
105
106
|
// so labels line up under series 0's actual (depth-shifted) points.
|
|
106
107
|
if (seriesIndex === 0) {
|
|
108
|
+
const step = Axis.categoryLabelStep(seriesValues.map(d => d.label), width);
|
|
107
109
|
seriesValues.forEach((d, i) => {
|
|
110
|
+
if (i % step) return;
|
|
108
111
|
const pointX = x + i * pointMargin + seriesDepth;
|
|
109
112
|
const textAnchor = this.renderer.centerTextAnchor;
|
|
110
113
|
this.renderer.text(d.label, pointX, y + height + 20, { textAnchor: textAnchor });
|
|
@@ -115,8 +118,8 @@ export class LineChart extends Chart {
|
|
|
115
118
|
// Draw legend for multiline
|
|
116
119
|
if (isMultiline && showLegend) {
|
|
117
120
|
const legendItems = series.map((s, i) => ({ name: s.name, color: palette.colorAt(i) }));
|
|
118
|
-
|
|
119
|
-
items: legendItems,
|
|
121
|
+
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
122
|
+
items: legendItems,
|
|
120
123
|
itemHeight: 20, markerSize: 12,
|
|
121
124
|
}).draw(this.renderer);
|
|
122
125
|
}
|
|
@@ -94,8 +94,8 @@ export class ParallelCoordinatesChart extends Chart {
|
|
|
94
94
|
|
|
95
95
|
if (legendVisible && hasCategories) {
|
|
96
96
|
const legendItems = categories.map((c, i) => ({ name: c, color: palette.colorAt(i) }));
|
|
97
|
-
|
|
98
|
-
items: legendItems,
|
|
97
|
+
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
98
|
+
items: legendItems,
|
|
99
99
|
itemHeight: 20, markerSize: 12,
|
|
100
100
|
}).draw(this.renderer);
|
|
101
101
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Legend } from '../Legend.js';
|
|
1
2
|
import { Chart } from '../Chart.js';
|
|
2
3
|
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
3
4
|
import { ColorPalette } from '../ColorPalette.js';
|
|
@@ -77,12 +78,9 @@ export class PipeChart extends Chart {
|
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
if (isMultiline && showLegend) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.renderer.rect(legendX, legendY, 12, 12, palette.colorAt(i), null);
|
|
84
|
-
this.renderer.text(s.name, legendX + 20, legendY + 6, { fontSize: '12px', fill: '#333' });
|
|
85
|
-
});
|
|
81
|
+
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
82
|
+
items: series.map((s, i) => ({ name: s.name, color: palette.colorAt(i) })), markerSize: 12,
|
|
83
|
+
}).draw(this.renderer);
|
|
86
84
|
}
|
|
87
85
|
}
|
|
88
86
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Legend } from '../Legend.js';
|
|
1
2
|
import { Chart } from '../Chart.js';
|
|
2
3
|
import { ColorPalette } from '../ColorPalette.js';
|
|
3
4
|
|
|
@@ -65,12 +66,10 @@ export class RadarChart extends Chart {
|
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
if (isMulti && showLegend) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.renderer.text(s.name, legendX + 18, legendY + 6, { fontSize: '12px', fill: '#333', verticalAlign: 'middle' });
|
|
73
|
-
});
|
|
69
|
+
new Legend({
|
|
70
|
+
items: series.map((s, i) => ({ name: s.name, color: palette.colorAt(i) })),
|
|
71
|
+
x: cx + radius + 40, y: cy - radius, markerSize: 12,
|
|
72
|
+
}).draw(this.renderer);
|
|
74
73
|
}
|
|
75
74
|
}
|
|
76
75
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Legend } from '../Legend.js';
|
|
1
2
|
import { Chart } from '../Chart.js';
|
|
2
3
|
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
3
4
|
import { ColorPalette } from '../ColorPalette.js';
|
|
@@ -92,13 +93,10 @@ export class ScatterChart extends Chart {
|
|
|
92
93
|
// Category color legend — same inline pattern as bubbleChart (not the
|
|
93
94
|
// generic showLegend mechanism other 2D charts use).
|
|
94
95
|
if (catField && catColors.size > 0) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.renderer.text(String(cat), legX + 16, legY + 4, { textAnchor: 'start', fontSize: '11px', fill: '#374151' });
|
|
100
|
-
legY += 18;
|
|
101
|
-
}
|
|
96
|
+
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
97
|
+
items: [...catColors].map(([cat, color]) => ({ label: String(cat), color })),
|
|
98
|
+
itemHeight: 18, fontSize: '11px', marker: 'circle',
|
|
99
|
+
}).draw(this.renderer);
|
|
102
100
|
}
|
|
103
101
|
}
|
|
104
102
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
1
2
|
import { BarChartBase } from './BarChartBase.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -13,7 +14,8 @@ export class StackedBarChart extends BarChartBase {
|
|
|
13
14
|
|
|
14
15
|
_renderBars(maxStackValue) {
|
|
15
16
|
const data = this.data;
|
|
16
|
-
const { x, y, height } = this._plotBox;
|
|
17
|
+
const { x, y, width, height } = this._plotBox;
|
|
18
|
+
const labelStep = Axis.categoryLabelStep(data.map(d => d.label), width);
|
|
17
19
|
const { bandWidth, margin } = this._bandScale(data.length);
|
|
18
20
|
const textAnchor = this.renderer.centerTextAnchor;
|
|
19
21
|
|
|
@@ -27,7 +29,7 @@ export class StackedBarChart extends BarChartBase {
|
|
|
27
29
|
tooltip: `${d.label} - Series ${j + 1}: ${value}`,
|
|
28
30
|
});
|
|
29
31
|
});
|
|
30
|
-
this.renderer.text(d.label, barX + bandWidth / 2, y + height + 20, { textAnchor });
|
|
32
|
+
if (i % labelStep === 0) this.renderer.text(d.label, barX + bandWidth / 2, y + height + 20, { textAnchor });
|
|
31
33
|
});
|
|
32
34
|
}
|
|
33
35
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Legend } from '../Legend.js';
|
|
1
2
|
import { ColorPalette } from '../ColorPalette.js';
|
|
2
3
|
import { Chart3D } from '../Chart3D.js';
|
|
3
4
|
|
|
@@ -47,12 +48,9 @@ export class StackedBarChart3DOrbit extends Chart3D {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const maxSegments = Math.max(...data.map(d => d.values.length));
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.renderer.text(`Series ${j + 1}`, legX + 16, legY + 4, { textAnchor: 'start', fontSize: '11px', fill: '#374151' });
|
|
55
|
-
legY += 18;
|
|
56
|
-
}
|
|
51
|
+
new Legend({
|
|
52
|
+
items: Array.from({ length: maxSegments }, (_, j) => ({ label: `Series ${j + 1}`, color: palette.colorAt(j) })),
|
|
53
|
+
x: bx + width - 100, y: by + 5, itemHeight: 18, fontSize: '11px', marker: 'circle',
|
|
54
|
+
}).draw(this.renderer);
|
|
57
55
|
}
|
|
58
56
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
1
2
|
import { Chart } from '../Chart.js';
|
|
2
3
|
import { ColorPalette } from '../ColorPalette.js';
|
|
3
4
|
import { Legend } from '../Legend.js';
|
|
@@ -110,7 +111,9 @@ export class StreamGraphChart extends Chart {
|
|
|
110
111
|
});
|
|
111
112
|
|
|
112
113
|
// X-axis labels.
|
|
114
|
+
const labelStep = Axis.categoryLabelStep(series[0].values.map(d => d.label), width, { fontSize: 10 });
|
|
113
115
|
series[0].values.forEach((d, i) => {
|
|
116
|
+
if (i % labelStep) return;
|
|
114
117
|
this.renderer.text(d.label, x + i * pointMargin, y + height + 16, { textAnchor, fontSize: '10px', fill: '#666' });
|
|
115
118
|
});
|
|
116
119
|
|
|
@@ -122,8 +125,8 @@ export class StreamGraphChart extends Chart {
|
|
|
122
125
|
|
|
123
126
|
if (isMultiline && showLegend) {
|
|
124
127
|
const legendItems = series.map((s, i) => ({ label: s.name, color: palette.colorAt(i) }));
|
|
125
|
-
|
|
126
|
-
items: legendItems,
|
|
128
|
+
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
129
|
+
items: legendItems,
|
|
127
130
|
itemHeight: 20, markerSize: 12,
|
|
128
131
|
}).draw(this.renderer);
|
|
129
132
|
}
|
|
@@ -38,6 +38,7 @@ export class WaterfallChart extends Chart {
|
|
|
38
38
|
const range = (vMax - vMin) || 1;
|
|
39
39
|
const toY = (v) => y + height - ((v - vMin) / range) * height;
|
|
40
40
|
const barWidth = width / data.length * 0.7;
|
|
41
|
+
const labelStep = Axis.categoryLabelStep(data.map(d => d.label), width, { fontSize: 10 });
|
|
41
42
|
const barMargin = width / data.length * 0.3;
|
|
42
43
|
const textAnchor = this.renderer.centerTextAnchor;
|
|
43
44
|
|
|
@@ -60,7 +61,7 @@ export class WaterfallChart extends Chart {
|
|
|
60
61
|
this.renderer.rect(barX, top, barWidth, Math.max(1, bottom - top), color, null, {
|
|
61
62
|
tooltip: `${d.label}: ${band.isTotal ? band.to : (d.value >= 0 ? '+' : '') + d.value} (total: ${Axis.formatValue(band.to)})`,
|
|
62
63
|
});
|
|
63
|
-
this.renderer.text(d.label, barX + barWidth / 2, y + height + 20, { textAnchor, fontSize: '10px' });
|
|
64
|
+
if (i % labelStep === 0) this.renderer.text(d.label, barX + barWidth / 2, y + height + 20, { textAnchor, fontSize: '10px' });
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
67
|
}
|