@peter.prib/renderengine 0.14.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 (109) hide show
  1. package/README.md +45 -19
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49391 -39748
  4. package/dist/renderengine.umd.js +25366 -15650
  5. package/documentation/REFERENCE.md +38 -0
  6. package/package.json +6 -1
  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 +219 -0
  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 +3 -0
  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 +114 -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/charts/AlluvialDiagram.js +311 -0
  34. package/src/charting/charts/AreaChart.js +52 -15
  35. package/src/charting/charts/BarChart.js +38 -3
  36. package/src/charting/charts/BarChart3D.js +5 -3
  37. package/src/charting/charts/BarChartBase.js +26 -6
  38. package/src/charting/charts/BeeswarmChart.js +164 -0
  39. package/src/charting/charts/BoxPlotChart.js +23 -1
  40. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  41. package/src/charting/charts/CandlestickChart.js +172 -4
  42. package/src/charting/charts/ChordDiagram.js +105 -2
  43. package/src/charting/charts/CirclePackChart.js +81 -0
  44. package/src/charting/charts/ComboChart.js +233 -0
  45. package/src/charting/charts/ContourChart.js +310 -0
  46. package/src/charting/charts/DonutChart3D.js +13 -0
  47. package/src/charting/charts/DumbbellChart.js +217 -0
  48. package/src/charting/charts/EcdfChart.js +150 -0
  49. package/src/charting/charts/FunnelChart.js +4 -1
  50. package/src/charting/charts/GeoMapChart.js +132 -11
  51. package/src/charting/charts/GroupedBarChart.js +44 -3
  52. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  53. package/src/charting/charts/HeatmapChart.js +2 -2
  54. package/src/charting/charts/HexbinChart.js +137 -0
  55. package/src/charting/charts/HierarchyChart.js +77 -3
  56. package/src/charting/charts/HorizonChart.js +105 -0
  57. package/src/charting/charts/KagiChart.js +167 -0
  58. package/src/charting/charts/LineChart.js +74 -18
  59. package/src/charting/charts/OhlcChart.js +79 -0
  60. package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
  61. package/src/charting/charts/PieChart.js +195 -17
  62. package/src/charting/charts/PieChart3D.js +137 -0
  63. package/src/charting/charts/PointAndFigureChart.js +198 -0
  64. package/src/charting/charts/PolarAreaChart.js +189 -0
  65. package/src/charting/charts/RadarChart.js +7 -1
  66. package/src/charting/charts/RadialBarChart.js +5 -2
  67. package/src/charting/charts/RenkoChart.js +178 -0
  68. package/src/charting/charts/RidgelineChart.js +113 -0
  69. package/src/charting/charts/SankeyDiagram.js +127 -4
  70. package/src/charting/charts/ScatterChart.js +13 -0
  71. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  72. package/src/charting/charts/SlopegraphChart.js +205 -0
  73. package/src/charting/charts/SparklineTableChart.js +166 -0
  74. package/src/charting/charts/StackedBarChart.js +48 -5
  75. package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
  76. package/src/charting/charts/StreamGraphChart.js +10 -3
  77. package/src/charting/charts/SunburstChart.js +27 -7
  78. package/src/charting/charts/SurfaceChart3D.js +157 -0
  79. package/src/charting/charts/TreemapChart.js +28 -8
  80. package/src/charting/charts/ViolinChart.js +18 -0
  81. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  82. package/src/charting/datamodel/DataLineage.js +150 -0
  83. package/src/charting/datamodel/DataModel.js +83 -1
  84. package/src/charting/datamodel/ModelMapping.js +87 -0
  85. package/src/charting/datamodel/SqlDdl.js +110 -85
  86. package/src/charting/datamodel/index.js +3 -0
  87. package/src/charting/decision/DecisionWorkbench.js +295 -0
  88. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  89. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  90. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  91. package/src/charting/diagrams/SketchView.js +121 -0
  92. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  93. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  94. package/src/charting/modelTree/NavigatorModel.js +410 -0
  95. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  96. package/src/charting/pivot/PivotChart.js +109 -4
  97. package/src/charting/pivot/PivotGridChart.js +321 -33
  98. package/src/charting/pivot/PivotView.js +82 -0
  99. package/src/charting/report/TemplatedReport.js +547 -0
  100. package/src/charting/templates/BoxTemplate.js +28 -27
  101. package/src/charting/templates/PackageTemplate.js +26 -25
  102. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  103. package/src/charting/templates/Template.js +219 -212
  104. package/src/cli.js +146 -0
  105. package/src/index.js +3370 -2995
  106. package/src/workspace/Workspace.js +126 -7
  107. package/src/workspace/archimateServices.js +4 -0
  108. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  109. package/src/workspace/styles.js +36 -1
@@ -0,0 +1,198 @@
1
+ import { Axis } from '../Axis.js';
2
+ import { Chart } from '../Chart.js';
3
+ import { ChartBox, ChartLabels } from '../ChartBox.js';
4
+
5
+ /**
6
+ * PointAndFigureChart — classical financial charting using columns of X's (rising)
7
+ * and O's (falling) to filter out time and track supply/demand breakouts.
8
+ */
9
+ export class PointAndFigureChart extends Chart {
10
+ static chartType = 'pointAndFigure';
11
+
12
+ /**
13
+ * Compute Point and Figure columns from price data.
14
+ * @param {Array<number|object>} data
15
+ * @param {object} [options]
16
+ * @param {number} [options.boxSize] - Price increment per box
17
+ * @param {number} [options.reversal=3] - Number of boxes to trigger reversal
18
+ */
19
+ static computePointAndFigure(data, { boxSize, reversal = 3 } = {}) {
20
+ if (!Array.isArray(data) || data.length === 0) return { columns: [], boxSize: 1, minPrice: 0, maxPrice: 1 };
21
+
22
+ const items = data.map((d, i) => {
23
+ const high = Number(d.high ?? d.close ?? d.value ?? d) || 0;
24
+ const low = Number(d.low ?? d.close ?? d.value ?? d) || 0;
25
+ const label = typeof d === 'object' && d !== null ? (d.label ?? d.date ?? `#${i + 1}`) : `#${i + 1}`;
26
+ return { high, low, label };
27
+ });
28
+
29
+ const p0 = items[0].high;
30
+ const bSize = boxSize ?? Math.max(0.5, Math.round(p0 * 0.015 * 10) / 10);
31
+ const toBox = (price) => Math.floor(price / bSize);
32
+
33
+ let curType = items[1]?.high >= p0 ? 'X' : 'O';
34
+ let curBoxes = [toBox(p0)];
35
+ const columns = [{ type: curType, boxes: curBoxes, label: items[0].label }];
36
+
37
+ for (let i = 1; i < items.length; i++) {
38
+ const { high, low, label } = items[i];
39
+ const col = columns[columns.length - 1];
40
+ const topBox = Math.max(...col.boxes);
41
+ const botBox = Math.min(...col.boxes);
42
+
43
+ if (col.type === 'X') {
44
+ const highBox = toBox(high);
45
+ if (highBox > topBox) {
46
+ for (let b = topBox + 1; b <= highBox; b++) col.boxes.push(b);
47
+ col.label = label;
48
+ } else {
49
+ const lowBox = toBox(low);
50
+ if (topBox - lowBox >= reversal) {
51
+ const newBoxes = [];
52
+ for (let b = topBox - 1; b >= lowBox; b--) newBoxes.push(b);
53
+ columns.push({ type: 'O', boxes: newBoxes, label });
54
+ }
55
+ }
56
+ } else {
57
+ const lowBox = toBox(low);
58
+ if (lowBox < botBox) {
59
+ for (let b = botBox - 1; b >= lowBox; b--) col.boxes.push(b);
60
+ col.label = label;
61
+ } else {
62
+ const highBox = toBox(high);
63
+ if (highBox - botBox >= reversal) {
64
+ const newBoxes = [];
65
+ for (let b = botBox + 1; b <= highBox; b++) newBoxes.push(b);
66
+ columns.push({ type: 'X', boxes: newBoxes, label });
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ let minB = Infinity;
73
+ let maxB = -Infinity;
74
+ columns.forEach(col => {
75
+ col.boxes.forEach(b => {
76
+ if (b < minB) minB = b;
77
+ if (b > maxB) maxB = b;
78
+ });
79
+ });
80
+
81
+ if (!Number.isFinite(minB)) { minB = 0; maxB = 10; }
82
+
83
+ return {
84
+ columns,
85
+ boxSize: bSize,
86
+ minBox: minB,
87
+ maxBox: maxB,
88
+ minPrice: minB * bSize,
89
+ maxPrice: (maxB + 1) * bSize,
90
+ };
91
+ }
92
+
93
+ _render() {
94
+ const options = this.options || {};
95
+ const { columns, boxSize, minBox, maxBox, minPrice, maxPrice } = PointAndFigureChart.computePointAndFigure(this.data, options);
96
+ if (!columns.length) return;
97
+
98
+ const box = ChartBox.fromRenderer(this.renderer);
99
+ const {
100
+ x = box.x, y = box.y, width = box.width, height = box.height,
101
+ xColor = '#26a69a', oColor = '#ef5350',
102
+ showGrid = true, showTrendlines = true,
103
+ title, xAxisLabel, yAxisLabel,
104
+ } = options;
105
+
106
+ const plotBox = { x, y, width, height };
107
+ new ChartLabels({ title, xAxisLabel, yAxisLabel }).draw(this.renderer, plotBox);
108
+
109
+ const totalBoxes = (maxBox - minBox + 1) || 1;
110
+ const totalCols = columns.length;
111
+ const colW = width / Math.max(1, totalCols);
112
+ const boxH = height / totalBoxes;
113
+
114
+ const toScreenY = (bIndex) => y + (maxBox - bIndex) * boxH;
115
+
116
+ // 1. Grid lines
117
+ if (showGrid) {
118
+ for (let b = minBox; b <= maxBox + 1; b++) {
119
+ const gy = toScreenY(b);
120
+ this.renderer.path(`M${x},${gy} L${x + width},${gy}`, 'none', { color: '#f0f0f0', width: 0.75 });
121
+ }
122
+ for (let c = 0; c <= totalCols; c++) {
123
+ const gx = x + c * colW;
124
+ this.renderer.path(`M${gx},${y} L${gx},${y + height}`, 'none', { color: '#f0f0f0', width: 0.75 });
125
+ }
126
+ }
127
+
128
+ // 2. Y-axis price labels
129
+ const axisStep = Math.max(1, Math.round(totalBoxes / 8));
130
+ for (let b = minBox; b <= maxBox; b += axisStep) {
131
+ const py = toScreenY(b) + boxH / 2;
132
+ const priceVal = b * boxSize;
133
+ this.renderer.text(Axis.formatValue(priceVal), x - 8, py, {
134
+ textAnchor: 'end', verticalAlign: 'middle', fontSize: '9px', fill: '#666',
135
+ });
136
+ }
137
+
138
+ // 3. Columns of X and O
139
+ columns.forEach((col, cIdx) => {
140
+ const cx = x + cIdx * colW + colW / 2;
141
+ const isX = col.type === 'X';
142
+ const color = isX ? xColor : oColor;
143
+ const glyphSize = Math.min(colW, boxH) * 0.7;
144
+
145
+ col.boxes.forEach(bIndex => {
146
+ const cy = toScreenY(bIndex) + boxH / 2;
147
+ const price = bIndex * boxSize;
148
+
149
+ if (isX) {
150
+ // Draw X
151
+ const hs = glyphSize / 2;
152
+ const pathD = `M${cx - hs},${cy - hs} L${cx + hs},${cy + hs} M${cx - hs},${cy + hs} L${cx + hs},${cy - hs}`;
153
+ const el = this.renderer.path(pathD, 'none', { color, width: 2 }, {
154
+ tooltip: `X (${col.label}): ${Axis.formatValue(price)}`,
155
+ });
156
+ this._datum(el, { label: col.label, type: 'X', price });
157
+ } else {
158
+ // Draw O
159
+ const r = glyphSize / 2;
160
+ const el = this.renderer.circle(cx, cy, r, 'none', { color, width: 2 }, {
161
+ tooltip: `O (${col.label}): ${Axis.formatValue(price)}`,
162
+ });
163
+ this._datum(el, { label: col.label, type: 'O', price });
164
+ }
165
+ });
166
+
167
+ // Column label along X axis
168
+ const step = Math.max(1, Math.floor(totalCols / 8));
169
+ if (cIdx % step === 0) {
170
+ this.renderer.text(col.label, cx, y + height + 18, {
171
+ textAnchor: this.renderer.centerTextAnchor, fontSize: '9px', fill: '#555',
172
+ });
173
+ }
174
+ });
175
+
176
+ // 4. Optional 45-degree support/resistance trendlines
177
+ if (showTrendlines && totalCols > 3) {
178
+ // Find major low for bullish support (45 deg up)
179
+ let lowestBox = Infinity, lowestCol = 0;
180
+ columns.forEach((c, idx) => {
181
+ const minB = Math.min(...c.boxes);
182
+ if (minB < lowestBox) { lowestBox = minB; lowestCol = idx; }
183
+ });
184
+
185
+ if (lowestCol < totalCols - 1) {
186
+ const x1 = x + lowestCol * colW + colW / 2;
187
+ const y1 = toScreenY(lowestBox) + boxH;
188
+ const steps = totalCols - 1 - lowestCol;
189
+ const x2 = x1 + steps * colW;
190
+ const y2 = y1 - steps * boxH;
191
+ this.renderer.path(`M${x1},${y1} L${x2},${y2}`, 'none', { color: '#26a69a', width: 1.5, strokeDasharray: '4 4' });
192
+ }
193
+ }
194
+
195
+ // Draw baseline axis
196
+ this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: 'black', width: 1 });
197
+ }
198
+ }
@@ -0,0 +1,189 @@
1
+ import { Chart } from '../Chart.js';
2
+ import { PieChart } from './PieChart.js';
3
+ import { Legend } from '../Legend.js';
4
+ import { ColorPalette } from '../ColorPalette.js';
5
+ import { ShapePaths } from '../ShapePaths.js';
6
+
7
+ /**
8
+ * PolarAreaChart — Nightingale Rose / Coxcomb chart where each category occupies an
9
+ * equal angular sector and its outer radius (or area when `scale: 'area'`) encodes `value`.
10
+ *
11
+ * Supports:
12
+ * - 2D and 3D extruded wedges (`extrude3D: true` or `depth > 0` with `tilt`)
13
+ * - Pre-exploded wedges (`exploded`: `'max'`, index, label, array, or per-item `d.exploded`)
14
+ * - Interactive radial hover pull-out (`hoverExplode = true`, `hoverOffset = 14`)
15
+ * - Concentric reference rings (`showGrid = true`)
16
+ */
17
+ export class PolarAreaChart extends Chart {
18
+ static chartType = 'polarArea';
19
+
20
+ _render() {
21
+ const rawData = Array.isArray(this.data) ? this.data.filter(d => (d?.value ?? 0) >= 0) : [];
22
+ const options = this.options || {};
23
+ const w = this.renderer?.width ?? 640;
24
+ const h = this.renderer?.height ?? 400;
25
+ const {
26
+ extrude3D = false,
27
+ depth = extrude3D ? 22 : 0,
28
+ tilt = depth > 0 ? 0.62 : 1,
29
+ innerRadiusRatio = 0.12,
30
+ padAngle = 0.02,
31
+ scale = 'radius', // 'radius' | 'area'
32
+ showGrid = true,
33
+ showValues = false,
34
+ legend: showLegend = true,
35
+ hoverExplode = true,
36
+ hoverOffset = 14,
37
+ colors,
38
+ title,
39
+ } = options;
40
+
41
+ const rxMax = options.radius ?? (Math.min(w, h) / 2 - 58);
42
+ const ryMax = rxMax * tilt;
43
+ const rxInner = rxMax * innerRadiusRatio;
44
+ const ryInner = ryMax * innerRadiusRatio;
45
+ const cx = options.cx ?? (showLegend ? Math.max(rxMax + 45, w / 2 - 45) : w / 2);
46
+ const cy = options.cy ?? (h / 2 - depth / 2 + 10);
47
+
48
+ const palette = new ColorPalette(colors);
49
+ if (title) {
50
+ this.renderer.text(title, w / 2, Math.max(24, cy - ryMax - 26), {
51
+ textAnchor: 'middle',
52
+ fontSize: '18px',
53
+ fill: '#333',
54
+ });
55
+ }
56
+ if (!rawData.length) return;
57
+
58
+ const maxValue = Math.max(...rawData.map(d => d.value), 1);
59
+ const total = rawData.reduce((s, d) => s + d.value, 0) || 1;
60
+ const n = rawData.length;
61
+ const sectorSpan = (Math.PI * 2) / n;
62
+
63
+ // Draw subtle concentric reference rings
64
+ if (showGrid) {
65
+ for (const frac of [0.33, 0.66, 1.0]) {
66
+ const grx = rxInner + (rxMax - rxInner) * frac;
67
+ const gry = ryInner + (ryMax - ryInner) * frac;
68
+ const ringPath = ShapePaths.ellipticalSlicePath(cx, cy, grx, gry, Math.max(0.1, grx - 0.5), Math.max(0.1, gry - 0.5), 0, Math.PI * 2);
69
+ const ringEl = this.renderer.path(ringPath, 'none', { color: '#e2e8f0', width: 1 });
70
+ ringEl?.setAttribute?.('stroke-dasharray', '3 3');
71
+ }
72
+ }
73
+
74
+ const sectors = rawData.map((d, i) => {
75
+ const startAngle = -Math.PI / 2 + i * sectorSpan + padAngle / 2;
76
+ const endAngle = -Math.PI / 2 + (i + 1) * sectorSpan - padAngle / 2;
77
+ const midAngle = (startAngle + endAngle) / 2;
78
+ const norm = Math.max(0, d.value) / maxValue;
79
+ const ratio = scale === 'area' ? Math.sqrt(norm) : norm;
80
+ const rx = rxInner + ratio * (rxMax - rxInner);
81
+ const ry = ryInner + ratio * (ryMax - ryInner);
82
+ const explodeDistance = PieChart.resolveExplodeDistance(d, i, rawData, {
83
+ explodeOffset: 14,
84
+ ...options,
85
+ });
86
+ return {
87
+ index: i,
88
+ datum: d,
89
+ label: d.label,
90
+ value: d.value,
91
+ percent: (d.value / total) * 100,
92
+ color: d.color || palette.colorAt(i),
93
+ startAngle,
94
+ endAngle,
95
+ midAngle,
96
+ rx,
97
+ ry,
98
+ explodeDistance,
99
+ };
100
+ });
101
+
102
+ // Sort back-to-front when extruded in 3D so front wedges occlude back wedges cleanly
103
+ const ordered = depth > 0
104
+ ? [...sectors].sort((a, b) => Math.sin(a.midAngle) - Math.sin(b.midAngle))
105
+ : sectors;
106
+
107
+ for (const s of ordered) {
108
+ const dx = Math.cos(s.midAngle) * s.explodeDistance;
109
+ const dy = Math.sin(s.midAngle) * s.explodeDistance * tilt;
110
+ const scx = cx + dx;
111
+ const scy = cy + dy;
112
+ const tooltip = `${s.label}: ${s.value} (${s.percent.toFixed(1)}%)`;
113
+ const datum = { label: s.label };
114
+ const wedgeEls = [];
115
+
116
+ if (depth > 0) {
117
+ const stroke = { color: ColorPalette.lighten(s.color, -0.32), width: 0.8 };
118
+ const walls = [
119
+ { angle: s.startAngle, depthOrder: Math.sin(s.startAngle), color: ColorPalette.lighten(s.color, -0.15) },
120
+ { angle: s.endAngle, depthOrder: Math.sin(s.endAngle), color: ColorPalette.lighten(s.color, -0.25) },
121
+ ].sort((a, b) => a.depthOrder - b.depthOrder);
122
+
123
+ const w0 = this._datum(this.renderer.path(
124
+ ShapePaths.radialWallPath(scx, scy, rxInner, ryInner, s.rx, s.ry, depth, walls[0].angle),
125
+ walls[0].color, stroke, { tooltip }
126
+ ), datum);
127
+ if (w0) wedgeEls.push(w0);
128
+
129
+ if (rxInner > 0) {
130
+ for (const [a0, a1] of ShapePaths.visibleArcIntervals(s.startAngle, s.endAngle, 'back')) {
131
+ const innerSkirt = ShapePaths.cylinderSkirtPath(scx, scy, rxInner, ryInner, depth, a0, a1);
132
+ if (innerSkirt) {
133
+ const el = this._datum(this.renderer.path(innerSkirt, ColorPalette.lighten(s.color, -0.28), stroke, { tooltip }), datum);
134
+ if (el) wedgeEls.push(el);
135
+ }
136
+ }
137
+ }
138
+
139
+ const w1 = this._datum(this.renderer.path(
140
+ ShapePaths.radialWallPath(scx, scy, rxInner, ryInner, s.rx, s.ry, depth, walls[1].angle),
141
+ walls[1].color, stroke, { tooltip }
142
+ ), datum);
143
+ if (w1) wedgeEls.push(w1);
144
+
145
+ for (const [a0, a1] of ShapePaths.visibleArcIntervals(s.startAngle, s.endAngle, 'front')) {
146
+ const outerSkirt = ShapePaths.cylinderSkirtPath(scx, scy, s.rx, s.ry, depth, a0, a1);
147
+ if (outerSkirt) {
148
+ const el = this._datum(this.renderer.path(outerSkirt, ColorPalette.lighten(s.color, -0.22), stroke, { tooltip }), datum);
149
+ if (el) wedgeEls.push(el);
150
+ }
151
+ }
152
+ }
153
+
154
+ const topColor = depth > 0 ? ColorPalette.lighten(s.color, 0.06) : s.color;
155
+ const topPath = ShapePaths.ellipticalSlicePath(scx, scy, s.rx, s.ry, rxInner, ryInner, s.startAngle, s.endAngle);
156
+ const topEl = this._datum(this.renderer.path(topPath, topColor, { color: '#fff', width: 1 }, { tooltip }), datum);
157
+ if (topEl) wedgeEls.push(topEl);
158
+
159
+ if (showValues) {
160
+ const lx = scx + (s.rx + 14) * Math.cos(s.midAngle);
161
+ const ly = scy + (s.ry + 14 * tilt) * Math.sin(s.midAngle);
162
+ const txtEl = this.renderer.text(`${s.label} (${s.value})`, lx, ly + 4, {
163
+ textAnchor: Math.cos(s.midAngle) >= 0 ? 'start' : 'end',
164
+ fontSize: '10px',
165
+ fill: '#334155',
166
+ });
167
+ if (txtEl) wedgeEls.push(txtEl);
168
+ }
169
+
170
+ if (hoverExplode && hoverOffset > 0) {
171
+ this._wireRadialHover(wedgeEls, s.midAngle, { offset: hoverOffset, ryRatio: tilt });
172
+ }
173
+ }
174
+
175
+ if (showLegend) {
176
+ const legendItems = sectors.map(s => ({
177
+ label: `${s.label} (${s.value})`,
178
+ color: s.color,
179
+ }));
180
+ new Legend({
181
+ items: legendItems,
182
+ x: cx + rxMax + 28,
183
+ y: cy - ryMax,
184
+ itemHeight: 20,
185
+ markerSize: 12,
186
+ }).draw(this.renderer);
187
+ }
188
+ }
189
+ }
@@ -53,6 +53,7 @@ export class RadarChart extends Chart {
53
53
  });
54
54
 
55
55
  series.forEach((s, seriesIndex) => {
56
+ if (this._dataFilter.has(seriesIndex)) return;
56
57
  const color = palette.colorAt(seriesIndex);
57
58
  const pts = s.values.map((v, i) => pointFor(i, v.value));
58
59
  const path = 'M' + pts.map(p => `${p.px},${p.py}`).join(' L') + ' Z';
@@ -68,7 +69,12 @@ export class RadarChart extends Chart {
68
69
 
69
70
  if (isMulti && showLegend) {
70
71
  new Legend({
71
- items: series.map((s, i) => ({ name: s.name, color: palette.colorAt(i) })),
72
+ items: series.map((s, i) => ({
73
+ name: s.name,
74
+ color: palette.colorAt(i),
75
+ hidden: this._dataFilter.has(i),
76
+ dataIndex: i,
77
+ })),
72
78
  x: cx + radius + 40, y: cy - radius, markerSize: 12,
73
79
  }).draw(this.renderer);
74
80
  }
@@ -56,7 +56,7 @@ export class RadialBarChart extends Chart {
56
56
  const rOuter = innerRadius + barLen;
57
57
  const color = d.color ?? palette.colorAt(i);
58
58
  const path = ShapePaths.donutSlicePath(cx, cy, rOuter, innerRadius, a0, a1);
59
- this._datum(this.renderer.path(path, color, { color: '#fff', width: 1 }, {
59
+ const sliceEl = this._datum(this.renderer.path(path, color, { color: '#fff', width: 1 }, {
60
60
  tooltip: `${d.label}: ${d.value}`,
61
61
  }), { label: d.label });
62
62
 
@@ -71,15 +71,18 @@ export class RadialBarChart extends Chart {
71
71
  transform: `rotate(${rotate - 90},${lx},${ly})`,
72
72
  });
73
73
 
74
+ const hoverEls = [sliceEl];
74
75
  if (showValues && barLen > 16) {
75
76
  const vmid = (innerRadius + rOuter) / 2;
76
77
  const vx = cx + vmid * Math.cos(midAngle);
77
78
  const vy = cy + vmid * Math.sin(midAngle);
78
- this.renderer.text(String(d.value), vx, vy, {
79
+ const valEl = this.renderer.text(String(d.value), vx, vy, {
79
80
  textAnchor: 'middle', verticalAlign: 'middle', fontSize: '9px',
80
81
  fill: ColorPalette.isDark(color) ? '#fff' : '#333',
81
82
  });
83
+ hoverEls.push(valEl);
82
84
  }
85
+ this._wireRadialHover(hoverEls, midAngle, { offset: options.hoverOffset ?? 10 });
83
86
  });
84
87
 
85
88
  if (showLegend) {
@@ -0,0 +1,178 @@
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
+
6
+ /**
7
+ * RenkoChart — financial price-action brick and Heikin-Ashi chart.
8
+ *
9
+ * Filters out time noise by plotting price movements of fixed box size.
10
+ * In `heikinAshi: true` mode, renders smoothed trend momentum candlesticks.
11
+ */
12
+ export class RenkoChart extends Chart {
13
+ static chartType = 'renko';
14
+
15
+ /**
16
+ * Compute Heikin-Ashi smoothed candles from raw OHLC data.
17
+ */
18
+ static computeHeikinAshi(candles) {
19
+ if (!Array.isArray(candles) || !candles.length) return [];
20
+ const result = [];
21
+ let prevOpen = (candles[0].open + candles[0].close) / 2;
22
+ let prevClose = (candles[0].open + candles[0].high + candles[0].low + candles[0].close) / 4;
23
+
24
+ for (let i = 0; i < candles.length; i++) {
25
+ const c = candles[i];
26
+ const haClose = (c.open + c.high + c.low + c.close) / 4;
27
+ const haOpen = i === 0 ? prevOpen : (prevOpen + prevClose) / 2;
28
+ const haHigh = Math.max(c.high, haOpen, haClose);
29
+ const haLow = Math.min(c.low, haOpen, haClose);
30
+
31
+ result.push({
32
+ ...c,
33
+ open: haOpen,
34
+ high: haHigh,
35
+ low: haLow,
36
+ close: haClose,
37
+ raw: c,
38
+ });
39
+ prevOpen = haOpen;
40
+ prevClose = haClose;
41
+ }
42
+ return result;
43
+ }
44
+
45
+ /**
46
+ * Compute discrete Renko bricks from price sequence.
47
+ * @param {Array<{close?: number, value?: number, label?: string, date?: string}>} data
48
+ * @param {number} [boxSize]
49
+ * @returns {Array<{index: number, bottom: number, top: number, isUp: boolean, date: string}>}
50
+ */
51
+ static computeRenkoBricks(data, boxSize) {
52
+ if (!Array.isArray(data) || !data.length) return [];
53
+ const prices = data.map(d => d.close ?? d.value ?? d.price ?? d.open ?? 0);
54
+ const dates = data.map(d => d.date ?? d.label ?? d.time ?? '');
55
+
56
+ let computedBox = boxSize;
57
+ if (!computedBox || computedBox <= 0) {
58
+ const minP = Math.min(...prices);
59
+ const maxP = Math.max(...prices);
60
+ computedBox = Math.max(0.1, (maxP - minP) / 24);
61
+ }
62
+
63
+ const bricks = [];
64
+ let lastPrice = prices[0];
65
+ let trend = 0; // +1 up, -1 down
66
+
67
+ for (let i = 1; i < prices.length; i++) {
68
+ const p = prices[i];
69
+ const diff = p - lastPrice;
70
+
71
+ if (diff >= computedBox) {
72
+ const count = Math.floor(diff / computedBox);
73
+ for (let k = 0; k < count; k++) {
74
+ const bottom = lastPrice;
75
+ const top = lastPrice + computedBox;
76
+ bricks.push({ bottom, top, isUp: true, date: dates[i] });
77
+ lastPrice = top;
78
+ }
79
+ trend = 1;
80
+ } else if (diff <= -computedBox) {
81
+ const count = Math.floor(-diff / computedBox);
82
+ for (let k = 0; k < count; k++) {
83
+ const top = lastPrice;
84
+ const bottom = lastPrice - computedBox;
85
+ bricks.push({ bottom, top, isUp: false, date: dates[i] });
86
+ lastPrice = bottom;
87
+ }
88
+ trend = -1;
89
+ }
90
+ }
91
+ return bricks;
92
+ }
93
+
94
+ _render() {
95
+ const data = this.data;
96
+ if (!Array.isArray(data) || !data.length) return;
97
+
98
+ const options = this.options;
99
+ const box = ChartBox.fromRenderer(this.renderer);
100
+ const {
101
+ x = box.x, y = box.y, width = box.width, height = box.height,
102
+ title, xAxisLabel, yAxisLabel,
103
+ upColor = '#27ae60', downColor = '#e74c3c',
104
+ boxSize, heikinAshi = false,
105
+ } = options;
106
+ const plotBox = { x, y, width, height };
107
+
108
+ new ChartLabels({ title, xAxisLabel, yAxisLabel }).draw(this.renderer, plotBox);
109
+
110
+ // 1. Heikin-Ashi mode: Render smoothed candlestick bars
111
+ if (heikinAshi) {
112
+ const candles = RenkoChart.computeHeikinAshi(data);
113
+ const highPrices = candles.map(c => c.high);
114
+ const lowPrices = candles.map(c => c.low);
115
+ const rawMax = Math.max(...highPrices, 1);
116
+ const rawMin = Math.min(...lowPrices);
117
+
118
+ new Axis().drawYRange(this.renderer, plotBox, rawMin, rawMax);
119
+ this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: '#999', width: 1 });
120
+
121
+ const n = candles.length;
122
+ const candleSlot = width / n;
123
+ const bodyWidth = Math.max(3, candleSlot * 0.7);
124
+ const toY = (v) => y + height - ((v - rawMin) / (rawMax - rawMin || 1)) * height;
125
+
126
+ candles.forEach((c, i) => {
127
+ const cx = x + i * candleSlot + candleSlot / 2;
128
+ const isBull = c.close >= c.open;
129
+ const color = isBull ? upColor : downColor;
130
+
131
+ // Wicks
132
+ const yHigh = toY(c.high);
133
+ const yLow = toY(c.low);
134
+ this.renderer.path(`M${cx},${yHigh} L${cx},${yLow}`, 'none', { color, width: 1.5 });
135
+
136
+ // Body
137
+ const yTop = toY(Math.max(c.open, c.close));
138
+ const yBottom = toY(Math.min(c.open, c.close));
139
+ const bodyH = Math.max(2, yBottom - yTop);
140
+ const bodyX = cx - bodyWidth / 2;
141
+
142
+ this._datum(this.renderer.rect(bodyX, yTop, bodyWidth, bodyH, color, null, {
143
+ tooltip: `${c.date || c.label || `Bar ${i + 1}`}: Open=${Axis.formatValue(c.open)}, High=${Axis.formatValue(c.high)}, Low=${Axis.formatValue(c.low)}, Close=${Axis.formatValue(c.close)}`,
144
+ }), { label: c.date || c.label, value: c.close });
145
+ });
146
+ return;
147
+ }
148
+
149
+ // 2. Renko Brick mode
150
+ const bricks = RenkoChart.computeRenkoBricks(data, boxSize);
151
+ if (!bricks.length) return;
152
+
153
+ const allBottoms = bricks.map(b => b.bottom);
154
+ const allTops = bricks.map(b => b.top);
155
+ const rawMin = Math.min(...allBottoms);
156
+ const rawMax = Math.max(...allTops, 1);
157
+
158
+ new Axis().drawYRange(this.renderer, plotBox, rawMin, rawMax);
159
+ this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: '#999', width: 1 });
160
+
161
+ const toY = (v) => y + height - ((v - rawMin) / (rawMax - rawMin || 1)) * height;
162
+ const n = bricks.length;
163
+ const brickSlot = width / n;
164
+ const brickW = Math.max(4, brickSlot * 0.85);
165
+
166
+ bricks.forEach((b, i) => {
167
+ const bx = x + i * brickSlot + (brickSlot - brickW) / 2;
168
+ const byTop = toY(b.top);
169
+ const byBottom = toY(b.bottom);
170
+ const bh = Math.max(2, byBottom - byTop);
171
+ const color = b.isUp ? upColor : downColor;
172
+
173
+ this._datum(this.renderer.rect(bx, byTop, brickW, bh, color, { color: '#ffffff', width: 1 }, {
174
+ tooltip: `${b.date || `Brick ${i + 1}`}: ${b.isUp ? 'Up' : 'Down'} [${Axis.formatValue(b.bottom)} - ${Axis.formatValue(b.top)}]`,
175
+ }), { label: b.date || `Brick ${i + 1}`, value: b.top });
176
+ });
177
+ }
178
+ }