@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.
- package/README.md +45 -19
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49391 -39748
- package/dist/renderengine.umd.js +25366 -15650
- package/documentation/REFERENCE.md +38 -0
- package/package.json +6 -1
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +219 -0
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +3 -0
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +114 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +52 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +5 -3
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +23 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +105 -2
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +4 -1
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +2 -2
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/RadarChart.js +7 -1
- package/src/charting/charts/RadialBarChart.js +5 -2
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +127 -4
- package/src/charting/charts/ScatterChart.js +13 -0
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +48 -5
- package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
- package/src/charting/charts/StreamGraphChart.js +10 -3
- package/src/charting/charts/SunburstChart.js +27 -7
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +28 -8
- package/src/charting/charts/ViolinChart.js +18 -0
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +109 -4
- package/src/charting/pivot/PivotGridChart.js +321 -33
- package/src/charting/pivot/PivotView.js +82 -0
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2995
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- 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) => ({
|
|
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
|
+
}
|