@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.
Files changed (124) hide show
  1. package/README.md +480 -302
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49217 -37484
  4. package/dist/renderengine.umd.js +24899 -13093
  5. package/documentation/REFERENCE.md +68 -1
  6. package/package.json +13 -3
  7. package/src/CanvasRenderer.js +225 -200
  8. package/src/SVGRenderer.js +476 -454
  9. package/src/archimate.js +22 -2
  10. package/src/archimateArchiFormat.js +1735 -1701
  11. package/src/archimateArchive.js +36 -7
  12. package/src/archimateCSV.js +93 -37
  13. package/src/archimateDrawio.js +2 -0
  14. package/src/archimateExchangeFormat.js +3 -1
  15. package/src/chartCatalog.js +269 -3
  16. package/src/charting/Annotations.js +169 -0
  17. package/src/charting/ArchimateDiagramEditor.js +54 -10
  18. package/src/charting/Axis.js +217 -5
  19. package/src/charting/Chart.js +99 -9
  20. package/src/charting/Chart3D.js +7 -2
  21. package/src/charting/ChartBox.js +8 -1
  22. package/src/charting/ColorPalette.js +42 -4
  23. package/src/charting/DiagramEditor.js +174 -39
  24. package/src/charting/DiagramEditorCommands.js +28 -0
  25. package/src/charting/Edge.js +7 -0
  26. package/src/charting/Hierarchy.js +142 -0
  27. package/src/charting/InteractionManager.js +4 -0
  28. package/src/charting/ModelFileFormat.js +5 -1
  29. package/src/charting/SettingsManager.js +35 -0
  30. package/src/charting/ShapePaths.js +82 -10
  31. package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
  32. package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
  33. package/src/charting/ask/AskBar.js +388 -0
  34. package/src/charting/ask/AskView.js +400 -0
  35. package/src/charting/charts/AlluvialDiagram.js +311 -0
  36. package/src/charting/charts/AreaChart.js +69 -15
  37. package/src/charting/charts/BarChart.js +38 -3
  38. package/src/charting/charts/BarChart3D.js +6 -4
  39. package/src/charting/charts/BarChart3DOrbit.js +3 -1
  40. package/src/charting/charts/BarChartBase.js +26 -6
  41. package/src/charting/charts/BeeswarmChart.js +164 -0
  42. package/src/charting/charts/BoxPlotChart.js +25 -3
  43. package/src/charting/charts/BubbleChart.js +4 -1
  44. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  45. package/src/charting/charts/CandlestickChart.js +172 -4
  46. package/src/charting/charts/ChordDiagram.js +107 -4
  47. package/src/charting/charts/CirclePackChart.js +81 -0
  48. package/src/charting/charts/ComboChart.js +233 -0
  49. package/src/charting/charts/ContourChart.js +310 -0
  50. package/src/charting/charts/DensityChart.js +1 -1
  51. package/src/charting/charts/DonutChart3D.js +13 -0
  52. package/src/charting/charts/DumbbellChart.js +217 -0
  53. package/src/charting/charts/EcdfChart.js +150 -0
  54. package/src/charting/charts/FunnelChart.js +5 -2
  55. package/src/charting/charts/GeoMapChart.js +132 -11
  56. package/src/charting/charts/GroupedBarChart.js +44 -3
  57. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  58. package/src/charting/charts/HeatmapChart.js +6 -3
  59. package/src/charting/charts/HexbinChart.js +137 -0
  60. package/src/charting/charts/HierarchyChart.js +77 -3
  61. package/src/charting/charts/HistogramChart.js +2 -2
  62. package/src/charting/charts/HorizonChart.js +105 -0
  63. package/src/charting/charts/IcicleChart.js +47 -0
  64. package/src/charting/charts/KagiChart.js +167 -0
  65. package/src/charting/charts/LineChart.js +74 -18
  66. package/src/charting/charts/LollipopChart.js +104 -0
  67. package/src/charting/charts/MarimekkoChart.js +75 -0
  68. package/src/charting/charts/OhlcChart.js +79 -0
  69. package/src/charting/charts/OutlineChart.js +10 -8
  70. package/src/charting/charts/ParallelCoordinatesChart.js +11 -3
  71. package/src/charting/charts/ParetoChart.js +92 -0
  72. package/src/charting/charts/PieChart.js +195 -17
  73. package/src/charting/charts/PieChart3D.js +137 -0
  74. package/src/charting/charts/PipeChart.js +5 -2
  75. package/src/charting/charts/PointAndFigureChart.js +198 -0
  76. package/src/charting/charts/PolarAreaChart.js +189 -0
  77. package/src/charting/charts/PopulationPyramidChart.js +4 -4
  78. package/src/charting/charts/RadarChart.js +12 -5
  79. package/src/charting/charts/RadialBarChart.js +6 -3
  80. package/src/charting/charts/RenkoChart.js +178 -0
  81. package/src/charting/charts/RidgelineChart.js +113 -0
  82. package/src/charting/charts/SankeyDiagram.js +129 -6
  83. package/src/charting/charts/ScatterChart.js +19 -3
  84. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  85. package/src/charting/charts/SlopegraphChart.js +205 -0
  86. package/src/charting/charts/SparklineTableChart.js +166 -0
  87. package/src/charting/charts/StackedBarChart.js +125 -11
  88. package/src/charting/charts/StackedBarChart3DOrbit.js +14 -6
  89. package/src/charting/charts/StreamGraphChart.js +18 -3
  90. package/src/charting/charts/SunburstChart.js +28 -8
  91. package/src/charting/charts/SurfaceChart3D.js +157 -0
  92. package/src/charting/charts/TreemapChart.js +31 -11
  93. package/src/charting/charts/ViolinChart.js +20 -2
  94. package/src/charting/charts/WaterfallChart.js +2 -2
  95. package/src/charting/charts/WordCloudChart.js +2 -2
  96. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  97. package/src/charting/datamodel/DataLineage.js +150 -0
  98. package/src/charting/datamodel/DataModel.js +83 -1
  99. package/src/charting/datamodel/ModelMapping.js +87 -0
  100. package/src/charting/datamodel/SqlDdl.js +110 -85
  101. package/src/charting/datamodel/index.js +3 -0
  102. package/src/charting/decision/DecisionWorkbench.js +295 -0
  103. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  104. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  105. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  106. package/src/charting/diagrams/SketchView.js +121 -0
  107. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  108. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  109. package/src/charting/modelTree/NavigatorModel.js +410 -0
  110. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  111. package/src/charting/pivot/PivotChart.js +310 -13
  112. package/src/charting/pivot/PivotGridChart.js +322 -34
  113. package/src/charting/pivot/PivotView.js +626 -21
  114. package/src/charting/report/TemplatedReport.js +547 -0
  115. package/src/charting/templates/BoxTemplate.js +28 -27
  116. package/src/charting/templates/PackageTemplate.js +26 -25
  117. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  118. package/src/charting/templates/Template.js +219 -212
  119. package/src/cli.js +146 -0
  120. package/src/index.js +3370 -2899
  121. package/src/workspace/Workspace.js +126 -7
  122. package/src/workspace/archimateServices.js +4 -0
  123. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  124. 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 legendOptions = { items: series.map(s => ({ name: s.name })), itemHeight: 20, markerSize: 12 };
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 value across all series
35
- const maxValue = Math.max(...series.flatMap(s =>
36
- isMultiline ? s.values.map(d => d.value) : s.values.map(d => d.value)
37
- )) || 1;
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 = y + height - (d.value / maxValue) * height;
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 = y + height - (d.value / maxValue) * height - seriesDepth;
111
+ const pointY = toY(d.value) - seriesDepth;
75
112
  if (i === 0) {
76
- pathData += `M${pointX},${pointY}`;
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 = y + height - (d.value / maxValue) * height - seriesDepth;
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 = y + height - (yHighVal / maxValue) * height - seriesDepth;
103
- const yLow = y + height - (yLowVal / maxValue) * height - seriesDepth;
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. Use this
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
- const legendItems = series.map((s, i) => ({ name: s.name, color: palette.colorAt(i) }));
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
- g.appendChild(this.renderer.rect(indentX - 2, rowCenterY - 5, 10, 10, accentColor, null));
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
- g.appendChild(badge);
46
- g.appendChild(this.renderer.text(collapsedHere ? '+' : '−', badgeX, rowCenterY, {
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
- g.appendChild(this.renderer.text(node.label, labelX, rowCenterY, {
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
- g.appendChild(this.renderer.text(this._formatNodeValue(node), countX, rowCenterY, {
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
- g.appendChild(this.renderer.text(Axis.formatValue(node.value), countX, rowCenterY, {
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
- g.appendChild(this.renderer.text(extra, detailX, rowCenterY, {
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) => ({ name: c, color: palette.colorAt(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
+ }