@peter.prib/renderengine 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +480 -302
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49217 -37484
- package/dist/renderengine.umd.js +24899 -13093
- package/documentation/REFERENCE.md +68 -1
- package/package.json +13 -3
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +269 -3
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +7 -2
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +142 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/ask/AskBar.js +388 -0
- package/src/charting/ask/AskView.js +400 -0
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +69 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +6 -4
- package/src/charting/charts/BarChart3DOrbit.js +3 -1
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +25 -3
- package/src/charting/charts/BubbleChart.js +4 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +107 -4
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DensityChart.js +1 -1
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +5 -2
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +6 -3
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HistogramChart.js +2 -2
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/IcicleChart.js +47 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/LollipopChart.js +104 -0
- package/src/charting/charts/MarimekkoChart.js +75 -0
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/OutlineChart.js +10 -8
- package/src/charting/charts/ParallelCoordinatesChart.js +11 -3
- package/src/charting/charts/ParetoChart.js +92 -0
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PipeChart.js +5 -2
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/PopulationPyramidChart.js +4 -4
- package/src/charting/charts/RadarChart.js +12 -5
- package/src/charting/charts/RadialBarChart.js +6 -3
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +129 -6
- package/src/charting/charts/ScatterChart.js +19 -3
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +125 -11
- package/src/charting/charts/StackedBarChart3DOrbit.js +14 -6
- package/src/charting/charts/StreamGraphChart.js +18 -3
- package/src/charting/charts/SunburstChart.js +28 -8
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +31 -11
- package/src/charting/charts/ViolinChart.js +20 -2
- package/src/charting/charts/WaterfallChart.js +2 -2
- package/src/charting/charts/WordCloudChart.js +2 -2
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +310 -13
- package/src/charting/pivot/PivotGridChart.js +322 -34
- package/src/charting/pivot/PivotView.js +626 -21
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2899
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/styles.js +36 -1
|
@@ -4,6 +4,7 @@ import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
|
4
4
|
import { Legend } from '../Legend.js';
|
|
5
5
|
import { ColorPalette } from '../ColorPalette.js';
|
|
6
6
|
import { ErrorBars } from '../ErrorBars.js';
|
|
7
|
+
import { Annotations } from '../Annotations.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* LineChart — single or multi-series line chart with optional 3D ribbon
|
|
@@ -25,27 +26,59 @@ export class LineChart extends Chart {
|
|
|
25
26
|
// Check if data is multiline (array of series) or single line
|
|
26
27
|
const isMultiline = Array.isArray(data[0]?.values);
|
|
27
28
|
const series = isMultiline ? data : [{ name: 'default', values: data }];
|
|
28
|
-
const
|
|
29
|
+
const lineColors = colors || ColorPalette.DEFAULT;
|
|
30
|
+
const palette = new ColorPalette(lineColors);
|
|
31
|
+
|
|
32
|
+
const legendOptions = {
|
|
33
|
+
items: series.map((s, idx) => ({
|
|
34
|
+
label: s.name,
|
|
35
|
+
color: palette.colorAt(idx),
|
|
36
|
+
hidden: this._dataFilter.has(idx),
|
|
37
|
+
dataIndex: idx,
|
|
38
|
+
})),
|
|
39
|
+
itemHeight: 20,
|
|
40
|
+
markerSize: 12,
|
|
41
|
+
};
|
|
29
42
|
// With a legend, the plot leaves room for it beside, so it does not cover the last points.
|
|
30
43
|
const width = options.width ?? (isMultiline && showLegend
|
|
31
44
|
? Legend.plotWidthBeside({ x, width: box.width }, this.renderer.width, legendOptions) : box.width);
|
|
32
45
|
const plotBox = { x, y, width, height };
|
|
33
46
|
|
|
34
|
-
// Determine max
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
// Determine max values per axis (supports secondary right Y-axis and active series)
|
|
48
|
+
const activeSeries = series.filter((_, idx) => !this._dataFilter.has(idx));
|
|
49
|
+
const workingSeries = activeSeries.length ? activeSeries : series;
|
|
50
|
+
const leftSeries = workingSeries.filter(s => s.yAxis !== 'right');
|
|
51
|
+
const rightSeries = workingSeries.filter(s => s.yAxis === 'right');
|
|
52
|
+
const hasSecondaryY = rightSeries.length > 0;
|
|
53
|
+
|
|
54
|
+
const maxLeftValue = Math.max(...leftSeries.flatMap(s => s.values.map(d => d.value)), 1);
|
|
55
|
+
const maxRightValue = hasSecondaryY ? Math.max(...rightSeries.flatMap(s => s.values.map(d => d.value)), 1) : 1;
|
|
38
56
|
|
|
39
57
|
const pointMargin = (isMultiline ? series[0].values.length : data.length) > 1
|
|
40
58
|
? width / ((isMultiline ? series[0].values.length : data.length) - 1)
|
|
41
59
|
: width;
|
|
42
60
|
|
|
43
61
|
const depth = zAxis ? zDepth : 0;
|
|
44
|
-
const lineColors = colors || ColorPalette.DEFAULT;
|
|
45
|
-
const palette = new ColorPalette(lineColors);
|
|
46
62
|
|
|
47
63
|
new ChartLabels({ title, xAxisLabel, yAxisLabel }).draw(this.renderer, plotBox);
|
|
48
64
|
|
|
65
|
+
// Draw Left Y Axis and optional Secondary Right Y Axis
|
|
66
|
+
let leftAxisRes;
|
|
67
|
+
if (!zAxis) {
|
|
68
|
+
leftAxisRes = new Axis().drawYRange(this.renderer, plotBox, 0, maxLeftValue, { scale: options.scale });
|
|
69
|
+
if (hasSecondaryY) {
|
|
70
|
+
new Axis().drawSecondaryYRange(this.renderer, plotBox, 0, maxRightValue);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const toY = leftAxisRes?.yOf ?? ((v) => y + height - (v / maxLeftValue) * height);
|
|
75
|
+
const toX = (idx) => x + idx * pointMargin;
|
|
76
|
+
|
|
77
|
+
// Draw threshold bands and reference lines
|
|
78
|
+
Annotations.drawThresholdBands(this.renderer, plotBox, options.thresholdBands, { toY });
|
|
79
|
+
const refLines = options.referenceLines ?? options.referenceLine;
|
|
80
|
+
Annotations.drawReferenceLines(this.renderer, plotBox, refLines, { toY, toX });
|
|
81
|
+
|
|
49
82
|
// 3D Axes
|
|
50
83
|
const axisPath = zAxis
|
|
51
84
|
? `M${x + depth},${y - depth} L${x},${y} L${x},${y + height} L${x + width},${y + height} L${x + width + depth},${y + height - depth} M${x},${y + height} L${x + depth},${y + height - depth}`
|
|
@@ -55,35 +88,55 @@ export class LineChart extends Chart {
|
|
|
55
88
|
|
|
56
89
|
// Draw each series
|
|
57
90
|
series.forEach((seriesData, seriesIndex) => {
|
|
91
|
+
if (this._dataFilter.has(seriesIndex)) return;
|
|
58
92
|
const seriesValues = isMultiline ? seriesData.values : seriesData.values;
|
|
59
93
|
const seriesColor = palette.colorAt(seriesIndex);
|
|
60
94
|
const seriesDepth = zAxis ? (depth / series.length) * (series.length - 1 - seriesIndex) : 0;
|
|
95
|
+
const seriesMax = seriesData.yAxis === 'right' ? maxRightValue : maxLeftValue;
|
|
96
|
+
const stepMode = seriesData.step ?? options.step;
|
|
61
97
|
|
|
62
98
|
// Draw line
|
|
63
99
|
if (zAxis) {
|
|
64
100
|
// 3D effect: draw a "ribbon"
|
|
65
101
|
const ribbonPath = seriesValues.map((d, i) => {
|
|
66
|
-
const pY =
|
|
102
|
+
const pY = toY(d.value);
|
|
67
103
|
return `L${x + i * pointMargin + seriesDepth},${pY - seriesDepth}`;
|
|
68
104
|
}).join(' ').replace('L', 'M');
|
|
69
105
|
this.renderer.path(ribbonPath, palette.lighten(seriesColor, -0.2), null);
|
|
70
106
|
}
|
|
71
107
|
let pathData = '';
|
|
108
|
+
let prevX = 0, prevY = 0;
|
|
72
109
|
seriesValues.forEach((d, i) => {
|
|
73
110
|
const pointX = x + i * pointMargin + seriesDepth;
|
|
74
|
-
const pointY =
|
|
111
|
+
const pointY = toY(d.value) - seriesDepth;
|
|
75
112
|
if (i === 0) {
|
|
76
|
-
pathData
|
|
113
|
+
pathData = `M${pointX},${pointY}`;
|
|
114
|
+
} else if (stepMode === 'before') {
|
|
115
|
+
pathData += ` V${pointY} H${pointX}`;
|
|
116
|
+
} else if (stepMode === 'center') {
|
|
117
|
+
const midX = (prevX + pointX) / 2;
|
|
118
|
+
pathData += ` H${midX} V${pointY} H${pointX}`;
|
|
119
|
+
} else if (stepMode) {
|
|
120
|
+
// 'after' or true
|
|
121
|
+
pathData += ` H${pointX} V${pointY}`;
|
|
77
122
|
} else {
|
|
78
123
|
pathData += ` L${pointX},${pointY}`;
|
|
79
124
|
}
|
|
125
|
+
prevX = pointX;
|
|
126
|
+
prevY = pointY;
|
|
80
127
|
});
|
|
81
128
|
this.renderer.path(pathData, 'none', { color: seriesColor, width: 2 });
|
|
82
129
|
|
|
130
|
+
// Optional trendline per series or for primary
|
|
131
|
+
if (options.trendline && (seriesIndex === 0 || options.trendline?.series === seriesData.name)) {
|
|
132
|
+
const trendPts = seriesValues.map((d, i) => ({ x: i, y: d.value }));
|
|
133
|
+
Annotations.drawTrendline(this.renderer, plotBox, trendPts, options.trendline, { toX, toY });
|
|
134
|
+
}
|
|
135
|
+
|
|
83
136
|
// Draw points + optional error bars
|
|
84
137
|
seriesValues.forEach((d, i) => {
|
|
85
138
|
const pointX = x + i * pointMargin + seriesDepth;
|
|
86
|
-
const pointY =
|
|
139
|
+
const pointY = toY(d.value) - seriesDepth;
|
|
87
140
|
const textAnchor = this.renderer.centerTextAnchor;
|
|
88
141
|
|
|
89
142
|
this._datum(this.renderer.circle(
|
|
@@ -94,20 +147,24 @@ export class LineChart extends Chart {
|
|
|
94
147
|
{ tooltip: `${isMultiline ? seriesData.name + ': ' : ''}${d.label}: ${d.value}` }
|
|
95
148
|
), { label: d.label, series: isMultiline ? seriesData.name : null });
|
|
96
149
|
|
|
150
|
+
if (options.showValues) {
|
|
151
|
+
this.renderer.text(Axis.formatValue(d.value), pointX, pointY - 8, {
|
|
152
|
+
textAnchor, fontSize: '10px', fill: '#333',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
97
156
|
const errMinus = d.errorMinus != null ? d.errorMinus : d.error != null ? d.error : null;
|
|
98
157
|
const errPlus = d.errorPlus != null ? d.errorPlus : d.error != null ? d.error : null;
|
|
99
158
|
if (errMinus != null || errPlus != null) {
|
|
100
159
|
const yHighVal = d.value + (errPlus ?? 0);
|
|
101
160
|
const yLowVal = d.value - (errMinus ?? 0);
|
|
102
|
-
const yHigh =
|
|
103
|
-
const yLow =
|
|
161
|
+
const yHigh = toY(yHighVal) - seriesDepth;
|
|
162
|
+
const yLow = toY(yLowVal) - seriesDepth;
|
|
104
163
|
ErrorBars.draw(this.renderer, pointX, pointY, null, null, yLow, yHigh, seriesColor);
|
|
105
164
|
}
|
|
106
165
|
});
|
|
107
166
|
|
|
108
|
-
// Draw labels only for first series to avoid clutter.
|
|
109
|
-
// series' own seriesDepth (not the raw max `depth`) for the x-shift
|
|
110
|
-
// so labels line up under series 0's actual (depth-shifted) points.
|
|
167
|
+
// Draw labels only for first series to avoid clutter.
|
|
111
168
|
if (seriesIndex === 0) {
|
|
112
169
|
const step = Axis.categoryLabelStep(seriesValues.map(d => d.label), width);
|
|
113
170
|
seriesValues.forEach((d, i) => {
|
|
@@ -121,8 +178,7 @@ export class LineChart extends Chart {
|
|
|
121
178
|
|
|
122
179
|
// Draw legend for multiline
|
|
123
180
|
if (isMultiline && showLegend) {
|
|
124
|
-
|
|
125
|
-
Legend.beside({ x, y, width }, this.renderer.width, { ...legendOptions, items: legendItems }).draw(this.renderer);
|
|
181
|
+
Legend.beside({ x, y, width }, this.renderer.width, legendOptions).draw(this.renderer);
|
|
126
182
|
}
|
|
127
183
|
}
|
|
128
184
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
2
|
+
import { Legend } from '../Legend.js';
|
|
3
|
+
import { BarChartBase } from './BarChartBase.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* LollipopChart — stem-and-dot alternative to a bar chart for high-cardinality or multi-series
|
|
7
|
+
* categorical comparisons with minimal ink density.
|
|
8
|
+
*
|
|
9
|
+
* Accepts either:
|
|
10
|
+
* - 1D `Array<{label: string, value: number}>`, or
|
|
11
|
+
* - Multi-series `Array<{name: string, values: Array<{label: string, value: number}>}>`.
|
|
12
|
+
*/
|
|
13
|
+
export class LollipopChart extends BarChartBase {
|
|
14
|
+
static chartType = 'lollipop';
|
|
15
|
+
|
|
16
|
+
static _normalize(data) {
|
|
17
|
+
if (!Array.isArray(data) || data.length === 0) return { isMulti: false, series: [], labels: [] };
|
|
18
|
+
if (Array.isArray(data[0]?.values)) {
|
|
19
|
+
const labels = [...new Set(data.flatMap(s => s.values.map(v => v.label)))];
|
|
20
|
+
return { isMulti: true, series: data, labels };
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
isMulti: false,
|
|
24
|
+
series: [{ name: 'default', values: data }],
|
|
25
|
+
labels: data.map(d => d.label),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_setup() {
|
|
30
|
+
this._norm = LollipopChart._normalize(this.data);
|
|
31
|
+
super._setup();
|
|
32
|
+
const showLegend = this.options.legend ?? this._norm.isMulti;
|
|
33
|
+
if (showLegend && this._norm.isMulti && this.options.width === undefined) {
|
|
34
|
+
this._plotBox.width = Legend.plotWidthBeside(this._plotBox, this.renderer.width, this._legendOptions());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_legendOptions() {
|
|
39
|
+
return {
|
|
40
|
+
items: this._norm.series.map((s, i) => ({ label: s.name, color: this._palette.colorAt(i), dataIndex: i })),
|
|
41
|
+
itemHeight: 20, markerSize: 10,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_computeMaxValue() {
|
|
46
|
+
const allValues = this._norm.series.flatMap(s => s.values.map(v => v.value)).filter(v => typeof v === 'number');
|
|
47
|
+
const rawMax = Math.max(0, ...allValues) || 1;
|
|
48
|
+
const rawMin = Math.min(0, ...allValues);
|
|
49
|
+
return this._drawYAxis(rawMax, rawMin);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_renderBars() {
|
|
53
|
+
const { isMulti, series, labels } = this._norm;
|
|
54
|
+
if (labels.length === 0) return;
|
|
55
|
+
const { x, y, width, height } = this._plotBox;
|
|
56
|
+
const {
|
|
57
|
+
color: singleColor = '#2563eb',
|
|
58
|
+
colors,
|
|
59
|
+
radius = 5,
|
|
60
|
+
stemWidth = 2,
|
|
61
|
+
} = this.options;
|
|
62
|
+
const { bandWidth, margin } = this._bandScale(labels.length);
|
|
63
|
+
const labelStep = Axis.categoryLabelStep(labels, width);
|
|
64
|
+
const textAnchor = this.renderer.centerTextAnchor;
|
|
65
|
+
const zeroY = this._barSpan(0).end;
|
|
66
|
+
const seriesCount = series.length;
|
|
67
|
+
|
|
68
|
+
labels.forEach((label, i) => {
|
|
69
|
+
const bandX = x + i * (bandWidth + margin);
|
|
70
|
+
series.forEach((s, sIdx) => {
|
|
71
|
+
const pt = s.values.find(v => v.label === label);
|
|
72
|
+
if (!pt || typeof pt.value !== 'number') return;
|
|
73
|
+
const cx = seriesCount === 1
|
|
74
|
+
? bandX + bandWidth / 2
|
|
75
|
+
: bandX + ((sIdx + 0.5) / seriesCount) * bandWidth;
|
|
76
|
+
const span = this._barSpan(pt.value);
|
|
77
|
+
const col = isMulti || colors ? this._palette.colorAt(sIdx) : singleColor;
|
|
78
|
+
const tip = isMulti ? `${s.name} - ${label}: ${pt.value}` : `${label}: ${pt.value}`;
|
|
79
|
+
|
|
80
|
+
this.renderer.path(`M${cx},${zeroY} L${cx},${span.end}`, 'none', { color: col, width: stemWidth });
|
|
81
|
+
if (typeof this.renderer.circle === 'function') {
|
|
82
|
+
this._datum(this.renderer.circle(cx, span.end, radius, col, { color: '#fff', width: 1.5 }, {
|
|
83
|
+
tooltip: tip,
|
|
84
|
+
}), { label, series: isMulti ? s.name : null });
|
|
85
|
+
} else {
|
|
86
|
+
this._datum(this.renderer.rect(cx - radius, span.end - radius, radius * 2, radius * 2, col, null, {
|
|
87
|
+
tooltip: tip,
|
|
88
|
+
}), { label, series: isMulti ? s.name : null });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (i % labelStep === 0) {
|
|
92
|
+
this.renderer.text(label, bandX + bandWidth / 2, y + height + 20, { textAnchor });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_afterBars() {
|
|
98
|
+
const showLegend = this.options.legend ?? this._norm.isMulti;
|
|
99
|
+
if (showLegend && this._norm.isMulti) {
|
|
100
|
+
const { x, y, width } = this._plotBox;
|
|
101
|
+
Legend.beside({ x, y, width }, this.renderer.width, this._legendOptions()).draw(this.renderer);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
2
|
+
import { Chart } from '../Chart.js';
|
|
3
|
+
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
4
|
+
import { ColorPalette } from '../ColorPalette.js';
|
|
5
|
+
import { Legend } from '../Legend.js';
|
|
6
|
+
import { StackedBarChart } from './StackedBarChart.js';
|
|
7
|
+
import { TableChart } from './TableChart.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* MarimekkoChart — two-dimensional 100%-stacked mosaic chart where column width is proportional to
|
|
11
|
+
* each category's total across series and segment height is each series' percentage share within
|
|
12
|
+
* that category.
|
|
13
|
+
*/
|
|
14
|
+
export class MarimekkoChart extends Chart {
|
|
15
|
+
static chartType = 'marimekko';
|
|
16
|
+
|
|
17
|
+
_render() {
|
|
18
|
+
const normalized = StackedBarChart._normalize(this.data, this.options.seriesLabels);
|
|
19
|
+
const { categories, seriesNames } = normalized;
|
|
20
|
+
const box = ChartBox.fromRenderer(this.renderer);
|
|
21
|
+
const {
|
|
22
|
+
x = box.x, y = box.y, width: rawWidth = box.width, height = box.height,
|
|
23
|
+
colors, title, xAxisLabel, yAxisLabel, legend: showLegend = true,
|
|
24
|
+
} = this.options;
|
|
25
|
+
const palette = new ColorPalette(colors);
|
|
26
|
+
const legendOpts = {
|
|
27
|
+
items: seriesNames.map((name, i) => ({ label: name, color: palette.colorAt(i), dataIndex: i })),
|
|
28
|
+
itemHeight: 20, markerSize: 10,
|
|
29
|
+
};
|
|
30
|
+
const width = showLegend && this.options.width === undefined && seriesNames.length > 0
|
|
31
|
+
? Legend.plotWidthBeside({ x, y, width: rawWidth, height }, this.renderer.width, legendOpts)
|
|
32
|
+
: rawWidth;
|
|
33
|
+
const plotBox = { x, y, width, height };
|
|
34
|
+
|
|
35
|
+
new ChartLabels({ title, xAxisLabel, yAxisLabel }).draw(this.renderer, plotBox);
|
|
36
|
+
new Axis().drawYRange(this.renderer, plotBox, 0, 100);
|
|
37
|
+
|
|
38
|
+
const colTotals = categories.map(c => c.values.reduce((sum, v) => sum + Math.max(0, v ?? 0), 0));
|
|
39
|
+
const grandTotal = colTotals.reduce((sum, v) => sum + v, 0) || 1;
|
|
40
|
+
const textAnchor = this.renderer.centerTextAnchor;
|
|
41
|
+
const measure = (s) => this.renderer.measureText?.(s, { fontSize: '11px' }) ?? s.length * 11 * 0.55;
|
|
42
|
+
|
|
43
|
+
let cursorX = x;
|
|
44
|
+
categories.forEach((cat, i) => {
|
|
45
|
+
const colTotal = colTotals[i];
|
|
46
|
+
if (colTotal <= 0) return;
|
|
47
|
+
const colW = (colTotal / grandTotal) * width;
|
|
48
|
+
let cursorY = y + height;
|
|
49
|
+
|
|
50
|
+
cat.values.forEach((value, j) => {
|
|
51
|
+
if (value == null || value <= 0) return;
|
|
52
|
+
const share = (value / colTotal) * 100;
|
|
53
|
+
const segH = (share / 100) * height;
|
|
54
|
+
cursorY -= segH;
|
|
55
|
+
const series = seriesNames[j] ?? `Series ${j + 1}`;
|
|
56
|
+
const color = palette.colorAt(j);
|
|
57
|
+
this._datum(this.renderer.rect(cursorX, cursorY, colW, segH, color, { color: '#fff', width: 1 }, {
|
|
58
|
+
tooltip: `${cat.label} - ${series}: ${value} (${share.toFixed(1)}%)`,
|
|
59
|
+
}), { label: cat.label, series });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (colW >= 24) {
|
|
63
|
+
const labelText = TableChart.fit(String(cat.label), colW - 4, measure);
|
|
64
|
+
this.renderer.text(labelText, cursorX + colW / 2, y + height + 18, { textAnchor, fontSize: '11px' });
|
|
65
|
+
}
|
|
66
|
+
cursorX += colW;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: 'black', width: 1 });
|
|
70
|
+
|
|
71
|
+
if (showLegend && seriesNames.length > 0) {
|
|
72
|
+
Legend.beside({ x, y, width }, this.renderer.width, legendOpts).draw(this.renderer);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
2
|
+
import { Chart } from '../Chart.js';
|
|
3
|
+
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OhlcChart — Open-High-Low-Close financial price bar chart.
|
|
7
|
+
*
|
|
8
|
+
* Visualizes asset price action with a vertical high-to-low spine, a left tick
|
|
9
|
+
* at open, and a right tick at close, shaded green (up) or red (down).
|
|
10
|
+
*/
|
|
11
|
+
export class OhlcChart extends Chart {
|
|
12
|
+
static chartType = 'ohlc';
|
|
13
|
+
|
|
14
|
+
_render() {
|
|
15
|
+
const data = this.data;
|
|
16
|
+
if (!Array.isArray(data) || !data.length) return;
|
|
17
|
+
|
|
18
|
+
const options = this.options;
|
|
19
|
+
const box = ChartBox.fromRenderer(this.renderer);
|
|
20
|
+
const {
|
|
21
|
+
x = box.x, y = box.y, width = box.width, height = box.height,
|
|
22
|
+
title, xAxisLabel, yAxisLabel,
|
|
23
|
+
upColor = '#26a69a', downColor = '#ef5350',
|
|
24
|
+
tickWidth = 5,
|
|
25
|
+
} = options;
|
|
26
|
+
const plotBox = { x, y, width, height };
|
|
27
|
+
|
|
28
|
+
new ChartLabels({ title, xAxisLabel, yAxisLabel }).draw(this.renderer, plotBox);
|
|
29
|
+
|
|
30
|
+
const allHighs = data.map(d => Number(d.high) || 0);
|
|
31
|
+
const allLows = data.map(d => Number(d.low) || 0);
|
|
32
|
+
const minVal = Math.min(...allLows);
|
|
33
|
+
const maxVal = Math.max(...allHighs);
|
|
34
|
+
|
|
35
|
+
// Draw axes
|
|
36
|
+
new Axis().drawYRange(this.renderer, plotBox, minVal, maxVal);
|
|
37
|
+
this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: 'black', width: 1 });
|
|
38
|
+
|
|
39
|
+
const toY = v => y + height - ((v - minVal) / ((maxVal - minVal) || 1)) * height;
|
|
40
|
+
const N = data.length;
|
|
41
|
+
const slotW = width / N;
|
|
42
|
+
|
|
43
|
+
data.forEach((bar, i) => {
|
|
44
|
+
const cx = x + i * slotW + slotW / 2;
|
|
45
|
+
const yHigh = toY(bar.high);
|
|
46
|
+
const yLow = toY(bar.low);
|
|
47
|
+
const yOpen = toY(bar.open);
|
|
48
|
+
const yClose = toY(bar.close);
|
|
49
|
+
const isUp = bar.close >= bar.open;
|
|
50
|
+
const color = isUp ? upColor : downColor;
|
|
51
|
+
const tooltip = `${bar.label}: Open ${bar.open}, High ${bar.high}, Low ${bar.low}, Close ${bar.close}`;
|
|
52
|
+
|
|
53
|
+
// Vertical spine (high to low)
|
|
54
|
+
const spine = this._datum(this.renderer.path(`M${cx},${yHigh} L${cx},${yLow}`, 'none', {
|
|
55
|
+
color, width: 2,
|
|
56
|
+
}, { tooltip }), { label: bar.label, ...bar });
|
|
57
|
+
|
|
58
|
+
// Left open tick
|
|
59
|
+
const openTick = this.renderer.path(`M${cx - tickWidth},${yOpen} L${cx},${yOpen}`, 'none', {
|
|
60
|
+
color, width: 2,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Right close tick
|
|
64
|
+
const closeTick = this.renderer.path(`M${cx},${yClose} L${cx + tickWidth},${yClose}`, 'none', {
|
|
65
|
+
color, width: 2,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this._wireHoverTransform([spine, openTick, closeTick], { dx: 0, dy: -2 });
|
|
69
|
+
|
|
70
|
+
// Category label along X
|
|
71
|
+
const step = Axis.categoryLabelStep(data.map(d => d.label), width);
|
|
72
|
+
if (i % step === 0) {
|
|
73
|
+
this.renderer.text(bar.label, cx, y + height + 20, {
|
|
74
|
+
textAnchor: this.renderer.centerTextAnchor, fontSize: '10px', fill: '#333',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -35,15 +35,16 @@ export class OutlineChart extends HierarchyChart {
|
|
|
35
35
|
const accentColor = this._colorForNode(topAncestor, depth);
|
|
36
36
|
|
|
37
37
|
const g = this.renderer.group();
|
|
38
|
-
|
|
38
|
+
const addChild = (child) => g?.appendChild?.(child);
|
|
39
|
+
addChild(this.renderer.rect(indentX - 2, rowCenterY - 5, 10, 10, accentColor, null));
|
|
39
40
|
|
|
40
41
|
let labelX = indentX + 16;
|
|
41
42
|
if (hasChildren) {
|
|
42
43
|
const collapsedHere = this.collapsed ? this.collapsed.has(node) : false;
|
|
43
44
|
const badgeX = indentX + 26;
|
|
44
45
|
const badge = this.renderer.circle(badgeX, rowCenterY, 7, '#ffffff', { color: '#888', width: 1.2 });
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
addChild(badge);
|
|
47
|
+
addChild(this.renderer.text(collapsedHere ? '+' : '−', badgeX, rowCenterY, {
|
|
47
48
|
textAnchor: 'middle', verticalAlign: 'middle', fontSize: '10px', fill: '#555', 'pointer-events': 'none',
|
|
48
49
|
}));
|
|
49
50
|
badge.style.cursor = 'pointer';
|
|
@@ -57,22 +58,23 @@ export class OutlineChart extends HierarchyChart {
|
|
|
57
58
|
labelX = badgeX + 16;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
const labelEl = this._datum(this.renderer.text(node.label, labelX, rowCenterY, {
|
|
61
62
|
textAnchor: 'start', verticalAlign: 'middle', fontSize: '12px', fill: '#222',
|
|
62
|
-
}));
|
|
63
|
+
}), { label: node.label, series: null });
|
|
64
|
+
addChild(labelEl);
|
|
63
65
|
|
|
64
66
|
if (hasChildren) {
|
|
65
|
-
|
|
67
|
+
addChild(this.renderer.text(this._formatNodeValue(node), countX, rowCenterY, {
|
|
66
68
|
textAnchor: 'end', verticalAlign: 'middle', fontSize: '11px', fill: '#888', 'pointer-events': 'none',
|
|
67
69
|
}));
|
|
68
70
|
} else if (Number.isFinite(node.value)) {
|
|
69
|
-
|
|
71
|
+
addChild(this.renderer.text(Axis.formatValue(node.value), countX, rowCenterY, {
|
|
70
72
|
textAnchor: 'end', verticalAlign: 'middle', fontSize: '11px', fill: '#888', 'pointer-events': 'none',
|
|
71
73
|
}));
|
|
72
74
|
} else {
|
|
73
75
|
const extra = Hierarchy.outlineExtraText(node, structuralKeys);
|
|
74
76
|
if (extra) {
|
|
75
|
-
|
|
77
|
+
addChild(this.renderer.text(extra, detailX, rowCenterY, {
|
|
76
78
|
textAnchor: 'start', verticalAlign: 'middle', fontSize: '11px', fill: '#888', 'pointer-events': 'none',
|
|
77
79
|
}));
|
|
78
80
|
}
|
|
@@ -64,6 +64,9 @@ export class ParallelCoordinatesChart extends Chart {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
data.forEach((row, rowIndex) => {
|
|
67
|
+
const catIdx = hasCategories ? categories.indexOf(row.category) : -1;
|
|
68
|
+
if (catIdx >= 0 && this._dataFilter.has(catIdx)) return;
|
|
69
|
+
|
|
67
70
|
const pts = [];
|
|
68
71
|
let valid = true;
|
|
69
72
|
dimensions.forEach((dim, i) => {
|
|
@@ -85,15 +88,20 @@ export class ParallelCoordinatesChart extends Chart {
|
|
|
85
88
|
pathData += `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`;
|
|
86
89
|
});
|
|
87
90
|
const label = row.label || (hasCategories ? row.category : null);
|
|
88
|
-
this.renderer.path(pathData, 'none', {
|
|
91
|
+
this._datum(this.renderer.path(pathData, 'none', {
|
|
89
92
|
color, width: lineWidth,
|
|
90
93
|
opacity: lineOpacity,
|
|
91
94
|
tooltip: label ? `${label}: ${dimensions.map(d => `${d}=${this._fmt(row[d])}`).join(', ')}` : undefined,
|
|
92
|
-
});
|
|
95
|
+
}), { label: label ?? String(rowIndex) });
|
|
93
96
|
});
|
|
94
97
|
|
|
95
98
|
if (legendVisible && hasCategories) {
|
|
96
|
-
const legendItems = categories.map((c, i) => ({
|
|
99
|
+
const legendItems = categories.map((c, i) => ({
|
|
100
|
+
name: c,
|
|
101
|
+
color: palette.colorAt(i),
|
|
102
|
+
hidden: this._dataFilter.has(i),
|
|
103
|
+
dataIndex: i,
|
|
104
|
+
}));
|
|
97
105
|
Legend.beside({ x, y, width }, this.renderer.width, {
|
|
98
106
|
items: legendItems,
|
|
99
107
|
itemHeight: 20, markerSize: 12,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
2
|
+
import { BarChartBase } from './BarChartBase.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ParetoChart — descending bar chart combined with a cumulative percentage line (0–100%)
|
|
6
|
+
* on a right-hand axis and an 80% cutoff reference line.
|
|
7
|
+
*/
|
|
8
|
+
export class ParetoChart extends BarChartBase {
|
|
9
|
+
static chartType = 'pareto';
|
|
10
|
+
|
|
11
|
+
_setup() {
|
|
12
|
+
super._setup();
|
|
13
|
+
if (this.options.width === undefined) {
|
|
14
|
+
this._plotBox.width = Math.max(80, this._plotBox.width - 36);
|
|
15
|
+
}
|
|
16
|
+
const items = (this.data ?? []).filter(d => d && typeof d.value === 'number' && d.value >= 0);
|
|
17
|
+
this._sorted = this.options.sorted === false ? [...items] : [...items].sort((a, b) => b.value - a.value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_computeMaxValue() {
|
|
21
|
+
const rawMax = Math.max(0, ...this._sorted.map(d => d.value)) || 1;
|
|
22
|
+
return this._drawYAxis(rawMax, 0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_renderBars(maxValue) {
|
|
26
|
+
const items = this._sorted;
|
|
27
|
+
if (items.length === 0) return;
|
|
28
|
+
const { x, y, width, height } = this._plotBox;
|
|
29
|
+
const {
|
|
30
|
+
barColor = '#3b82f6',
|
|
31
|
+
lineColor = '#dc2626',
|
|
32
|
+
cutoff = 80,
|
|
33
|
+
colors,
|
|
34
|
+
} = this.options;
|
|
35
|
+
const { bandWidth, margin } = this._bandScale(items.length);
|
|
36
|
+
const labelStep = Axis.categoryLabelStep(items.map(d => d.label), width);
|
|
37
|
+
const textAnchor = this.renderer.centerTextAnchor;
|
|
38
|
+
const total = items.reduce((sum, d) => sum + d.value, 0) || 1;
|
|
39
|
+
|
|
40
|
+
// Right-hand percentage axis (0%..100%).
|
|
41
|
+
const rightX = x + width;
|
|
42
|
+
this.renderer.path(`M${rightX},${y} L${rightX},${y + height}`, 'none', { color: '#64748b', width: 1 });
|
|
43
|
+
for (const pct of [0, 20, 40, 60, 80, 100]) {
|
|
44
|
+
const py = y + height - (pct / 100) * height;
|
|
45
|
+
this.renderer.path(`M${rightX},${py} L${rightX + 4},${py}`, 'none', { color: '#64748b', width: 1 });
|
|
46
|
+
this.renderer.text(`${pct}%`, rightX + 8, py + 3, { textAnchor: 'start', fontSize: '10px', fill: '#475569' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Dashed 80% cutoff reference line.
|
|
50
|
+
if (cutoff > 0 && cutoff < 100) {
|
|
51
|
+
const cutY = y + height - (cutoff / 100) * height;
|
|
52
|
+
this.renderer.path(`M${x},${cutY} L${rightX},${cutY}`, 'none', {
|
|
53
|
+
color: lineColor, width: 1, dashArray: '4,4',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let running = 0;
|
|
58
|
+
const linePts = [];
|
|
59
|
+
items.forEach((d, i) => {
|
|
60
|
+
running += d.value;
|
|
61
|
+
const cumPct = (running / total) * 100;
|
|
62
|
+
const color = colors ? this._palette.colorAt(i) : barColor;
|
|
63
|
+
const bar = this._barSpan(d.value);
|
|
64
|
+
const barX = x + i * (bandWidth + margin);
|
|
65
|
+
const cx = barX + bandWidth / 2;
|
|
66
|
+
const py = y + height - (cumPct / 100) * height;
|
|
67
|
+
linePts.push({ cx, py, label: d.label, value: d.value, cumPct });
|
|
68
|
+
|
|
69
|
+
this._datum(this.renderer.rect(barX, bar.top, bandWidth, bar.height, color, null, {
|
|
70
|
+
bar: true,
|
|
71
|
+
dataIndex: i,
|
|
72
|
+
tooltip: `${d.label}: ${d.value} (${cumPct.toFixed(1)}% cumulative)`,
|
|
73
|
+
}), { label: d.label });
|
|
74
|
+
|
|
75
|
+
if (i % labelStep === 0) {
|
|
76
|
+
this.renderer.text(d.label, cx, y + height + 20, { textAnchor });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (linePts.length > 0) {
|
|
81
|
+
const dPath = 'M' + linePts.map(p => `${p.cx},${p.py}`).join(' L');
|
|
82
|
+
this.renderer.path(dPath, 'none', { color: lineColor, width: 2 });
|
|
83
|
+
if (typeof this.renderer.circle === 'function') {
|
|
84
|
+
linePts.forEach(p => {
|
|
85
|
+
this._datum(this.renderer.circle(p.cx, p.py, 3.5, lineColor, { color: '#fff', width: 1.5 }, {
|
|
86
|
+
tooltip: `${p.label}: ${p.cumPct.toFixed(1)}% cumulative`,
|
|
87
|
+
}), { label: p.label });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|