@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
|
@@ -10,11 +10,16 @@ import { ColorPalette } from '../ColorPalette.js';
|
|
|
10
10
|
* x-axis label drawing. Subclasses implement `_renderGlyph(cx, slotWidth, i, d)`.
|
|
11
11
|
*/
|
|
12
12
|
export class GroupedDistributionChart extends Chart {
|
|
13
|
+
_isHorizontal() {
|
|
14
|
+
return this.options.orientation === 'horizontal';
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
_setup() {
|
|
14
18
|
const box = ChartBox.fromRenderer(this.renderer);
|
|
15
19
|
const options = this.options;
|
|
20
|
+
const leftExtra = this._isHorizontal() && options.x == null ? 40 : 0;
|
|
16
21
|
const {
|
|
17
|
-
x = box.x, y = box.y, width = box.width, height = box.height,
|
|
22
|
+
x = box.x + leftExtra, y = box.y, width = box.width - leftExtra, height = box.height,
|
|
18
23
|
colors, title, xAxisLabel, yAxisLabel,
|
|
19
24
|
} = options;
|
|
20
25
|
this._plotBox = { x, y, width, height };
|
|
@@ -36,10 +41,19 @@ export class GroupedDistributionChart extends Chart {
|
|
|
36
41
|
return y + height - ((v - this._domain.vMin) / this._domain.range) * height;
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
_toX(v) {
|
|
45
|
+
const { x, width } = this._plotBox;
|
|
46
|
+
return x + ((v - this._domain.vMin) / this._domain.range) * width;
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
_slotWidth() {
|
|
40
50
|
return this._plotBox.width / this.data.length;
|
|
41
51
|
}
|
|
42
52
|
|
|
53
|
+
_slotHeight() {
|
|
54
|
+
return this._plotBox.height / this.data.length;
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
_drawBaseAxis() {
|
|
44
58
|
const { x, y, width, height } = this._plotBox;
|
|
45
59
|
this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: 'black', width: 1 });
|
|
@@ -54,6 +68,17 @@ export class GroupedDistributionChart extends Chart {
|
|
|
54
68
|
this._setup();
|
|
55
69
|
this._domain = this._computeDomain();
|
|
56
70
|
this._drawBaseAxis();
|
|
71
|
+
if (this._isHorizontal()) {
|
|
72
|
+
const slotHeight = this._slotHeight();
|
|
73
|
+
this.data.forEach((d, i) => {
|
|
74
|
+
const cy = this._plotBox.y + i * slotHeight + slotHeight / 2;
|
|
75
|
+
this._renderGlyph(cy, slotHeight, i, d);
|
|
76
|
+
this.renderer.text(d.label, this._plotBox.x - 8, cy, {
|
|
77
|
+
textAnchor: 'end', verticalAlign: 'middle', fontSize: '11px', fill: '#333',
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
57
82
|
const slotWidth = this._slotWidth();
|
|
58
83
|
const data = this.data;
|
|
59
84
|
data.forEach((d, i) => {
|
|
@@ -17,7 +17,7 @@ export class HeatmapChart extends Chart {
|
|
|
17
17
|
const {
|
|
18
18
|
x = box.x, y = box.y, width = box.width, height = box.height,
|
|
19
19
|
xLabels, yLabels, colorScale = ['#f7fbff', '#4292c6', '#08306b'],
|
|
20
|
-
showValues = false, title,
|
|
20
|
+
showValues = false, midValue, title,
|
|
21
21
|
} = options;
|
|
22
22
|
|
|
23
23
|
// Grid cost is xCats.length * yCats.length, NOT data.length — a row
|
|
@@ -51,7 +51,7 @@ export class HeatmapChart extends Chart {
|
|
|
51
51
|
const v = cellByKey.get(`${xc}|${yc}`);
|
|
52
52
|
const cellX = x + xi * cellW;
|
|
53
53
|
const cellY = y + yi * cellH;
|
|
54
|
-
const color = v == null ? '#eee' : ColorPalette.scaleColor(colorScale, vMin, vMax, v);
|
|
54
|
+
const color = v == null ? '#eee' : ColorPalette.scaleColor(colorScale, vMin, vMax, v, midValue);
|
|
55
55
|
const cellRect = this.renderer.rect(cellX, cellY, Math.max(1, cellW - 1), Math.max(1, cellH - 1), color, null, {
|
|
56
56
|
tooltip: `${xc} / ${yc}: ${v ?? 'n/a'}`,
|
|
57
57
|
});
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
* HexbinChart — 2D hexagonal binning and density plot for dense (x, y) point clouds.
|
|
8
|
+
*
|
|
9
|
+
* Partitions the continuous 2D space into regular hexagon cells and shades each
|
|
10
|
+
* hexagon by observation count (or aggregated value), eliminating overplotting.
|
|
11
|
+
*/
|
|
12
|
+
export class HexbinChart extends Chart {
|
|
13
|
+
static chartType = 'hexbin';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compute regular hexagon vertices centered at (cx, cy) with radius r (pointy-topped).
|
|
17
|
+
*/
|
|
18
|
+
static hexagonPath(cx, cy, r) {
|
|
19
|
+
const pts = [];
|
|
20
|
+
for (let i = 0; i < 6; i++) {
|
|
21
|
+
const angle = (Math.PI / 3) * i - Math.PI / 6;
|
|
22
|
+
const px = cx + r * Math.cos(angle);
|
|
23
|
+
const py = cy + r * Math.sin(angle);
|
|
24
|
+
pts.push(`${px.toFixed(2)},${py.toFixed(2)}`);
|
|
25
|
+
}
|
|
26
|
+
return 'M' + pts.join(' L') + ' Z';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Bin 2D points into pointy-topped hexagons of radius r in screen coordinates.
|
|
31
|
+
* @returns {Map<string, {cx: number, cy: number, count: number, points: Array}>}
|
|
32
|
+
*/
|
|
33
|
+
static binPoints(points, r) {
|
|
34
|
+
const dx = r * Math.sqrt(3);
|
|
35
|
+
const dy = r * 1.5;
|
|
36
|
+
const bins = new Map();
|
|
37
|
+
|
|
38
|
+
for (const pt of points) {
|
|
39
|
+
const px = pt.px;
|
|
40
|
+
const py = pt.py;
|
|
41
|
+
// Pointy-top hex grid coordinate estimation
|
|
42
|
+
const q = (Math.sqrt(3) / 3 * px - 1 / 3 * py) / r;
|
|
43
|
+
const s = (2 / 3 * py) / r;
|
|
44
|
+
// Cube round
|
|
45
|
+
let rx = Math.round(q);
|
|
46
|
+
let rz = Math.round(s);
|
|
47
|
+
let ry = Math.round(-q - s);
|
|
48
|
+
const xDiff = Math.abs(rx - q);
|
|
49
|
+
const yDiff = Math.abs(ry - (-q - s));
|
|
50
|
+
const zDiff = Math.abs(rz - s);
|
|
51
|
+
if (xDiff > yDiff && xDiff > zDiff) {
|
|
52
|
+
rx = -ry - rz;
|
|
53
|
+
} else if (yDiff > zDiff) {
|
|
54
|
+
ry = -rx - rz;
|
|
55
|
+
} else {
|
|
56
|
+
rz = -rx - ry;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Hex center from cube coordinates
|
|
60
|
+
const cx = r * (Math.sqrt(3) * rx + Math.sqrt(3) / 2 * rz);
|
|
61
|
+
const cy = r * (3 / 2 * rz);
|
|
62
|
+
const key = `${rx},${rz}`;
|
|
63
|
+
let b = bins.get(key);
|
|
64
|
+
if (!b) {
|
|
65
|
+
b = { cx, cy, count: 0, sum: 0, points: [] };
|
|
66
|
+
bins.set(key, b);
|
|
67
|
+
}
|
|
68
|
+
b.count += 1;
|
|
69
|
+
b.sum += Number(pt.value ?? 1) || 1;
|
|
70
|
+
b.points.push(pt);
|
|
71
|
+
}
|
|
72
|
+
return bins;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_render() {
|
|
76
|
+
const data = this.data;
|
|
77
|
+
if (!Array.isArray(data) || !data.length) return;
|
|
78
|
+
|
|
79
|
+
const options = this.options;
|
|
80
|
+
const box = ChartBox.fromRenderer(this.renderer);
|
|
81
|
+
const {
|
|
82
|
+
x = box.x, y = box.y, width = box.width, height = box.height,
|
|
83
|
+
title, xAxisLabel, yAxisLabel,
|
|
84
|
+
radius = 14,
|
|
85
|
+
minCount = 1,
|
|
86
|
+
colorScale = ['#e0f3f8', '#67a9cf', '#02818a', '#bd0026'],
|
|
87
|
+
} = options;
|
|
88
|
+
const plotBox = { x, y, width, height };
|
|
89
|
+
|
|
90
|
+
new ChartLabels({ title, xAxisLabel, yAxisLabel }).draw(this.renderer, plotBox);
|
|
91
|
+
|
|
92
|
+
const pts = data.map((d, i) => {
|
|
93
|
+
const rawX = Array.isArray(d) ? d[0] : (d.x ?? i);
|
|
94
|
+
const rawY = Array.isArray(d) ? d[1] : (d.y ?? d.value ?? 0);
|
|
95
|
+
return { rawX: Number(rawX) || 0, rawY: Number(rawY) || 0, label: d.label, value: d.value };
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const xMin = Math.min(...pts.map(p => p.rawX));
|
|
99
|
+
const xMax = Math.max(...pts.map(p => p.rawX));
|
|
100
|
+
const yMin = Math.min(...pts.map(p => p.rawY));
|
|
101
|
+
const yMax = Math.max(...pts.map(p => p.rawY));
|
|
102
|
+
const xRange = (xMax - xMin) || 1;
|
|
103
|
+
const yRange = (yMax - yMin) || 1;
|
|
104
|
+
|
|
105
|
+
// Draw axes
|
|
106
|
+
new Axis().drawYRange(this.renderer, plotBox, yMin, yMax);
|
|
107
|
+
new Axis().drawXRange(this.renderer, plotBox, xMin, xMax);
|
|
108
|
+
this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: 'black', width: 1 });
|
|
109
|
+
|
|
110
|
+
const mapped = pts.map(p => ({
|
|
111
|
+
...p,
|
|
112
|
+
px: x + ((p.rawX - xMin) / xRange) * width,
|
|
113
|
+
py: y + height - ((p.rawY - yMin) / yRange) * height,
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
const bins = HexbinChart.binPoints(mapped, radius);
|
|
117
|
+
const binList = Array.from(bins.values()).filter(b => b.count >= minCount);
|
|
118
|
+
if (!binList.length) return;
|
|
119
|
+
|
|
120
|
+
const maxCount = Math.max(...binList.map(b => b.count));
|
|
121
|
+
const palette = new ColorPalette();
|
|
122
|
+
|
|
123
|
+
binList.forEach(b => {
|
|
124
|
+
// Clip to plot box bounds with a small margin
|
|
125
|
+
if (b.cx < x - radius || b.cx > x + width + radius || b.cy < y - radius || b.cy > y + height + radius) return;
|
|
126
|
+
const t = b.count / maxCount;
|
|
127
|
+
const color = ColorPalette.scaleColor(colorScale, 0, maxCount, b.count);
|
|
128
|
+
const hexD = HexbinChart.hexagonPath(b.cx, b.cy, radius * 0.96);
|
|
129
|
+
const tooltip = `Count: ${b.count} points (peak: ${maxCount})`;
|
|
130
|
+
|
|
131
|
+
const hexEl = this._datum(this.renderer.path(hexD, color, { color: '#ffffff', width: 0.8 }, {
|
|
132
|
+
tooltip,
|
|
133
|
+
}), { count: b.count, value: b.count });
|
|
134
|
+
this._wireHoverTransform([hexEl], { dx: 0, dy: -2 });
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -7,9 +7,82 @@ import { Hierarchy } from '../Hierarchy.js';
|
|
|
7
7
|
* HierarchyChart — abstract base for charts that render a hierarchy of nodes.
|
|
8
8
|
*
|
|
9
9
|
* Owns top-level palette assignment, per-depth color lightening, text-color
|
|
10
|
-
* contrast,
|
|
10
|
+
* contrast, value formatting, and interactive drill-down / breadcrumbs.
|
|
11
11
|
*/
|
|
12
12
|
export class HierarchyChart extends Chart {
|
|
13
|
+
constructor(renderer, data, options = {}) {
|
|
14
|
+
super(renderer, data, options);
|
|
15
|
+
this._drillStack = [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_activeData() {
|
|
19
|
+
if (this._drillStack.length > 0) {
|
|
20
|
+
const currentRoot = this._drillStack[this._drillStack.length - 1];
|
|
21
|
+
return currentRoot.children || [currentRoot];
|
|
22
|
+
}
|
|
23
|
+
return this.data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
drillTo(node) {
|
|
27
|
+
if (!node || !node.children?.length) return;
|
|
28
|
+
this._drillStack.push(node);
|
|
29
|
+
this._topLevelColorMap = null;
|
|
30
|
+
this.render();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
drillUp() {
|
|
34
|
+
if (!this._drillStack.length) return;
|
|
35
|
+
this._drillStack.pop();
|
|
36
|
+
this._topLevelColorMap = null;
|
|
37
|
+
this.render();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
drillToCrumb(index) {
|
|
41
|
+
if (index < 0) {
|
|
42
|
+
this._drillStack = [];
|
|
43
|
+
} else {
|
|
44
|
+
this._drillStack = this._drillStack.slice(0, index + 1);
|
|
45
|
+
}
|
|
46
|
+
this._topLevelColorMap = null;
|
|
47
|
+
this.render();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_drawBreadcrumbs(renderer, { x, y, width }) {
|
|
51
|
+
if (this._drillStack.length === 0) return 0;
|
|
52
|
+
const crumbH = 24;
|
|
53
|
+
const crumbs = [{ label: 'Root', index: -1 }, ...this._drillStack.map((n, i) => ({ label: n.label, index: i }))];
|
|
54
|
+
|
|
55
|
+
let curX = x;
|
|
56
|
+
crumbs.forEach((c, i) => {
|
|
57
|
+
if (i > 0) {
|
|
58
|
+
renderer.text('/', curX + 4, y + crumbH / 2, {
|
|
59
|
+
verticalAlign: 'middle', fontSize: '11px', fill: '#999',
|
|
60
|
+
});
|
|
61
|
+
curX += 14;
|
|
62
|
+
}
|
|
63
|
+
const isLast = i === crumbs.length - 1;
|
|
64
|
+
const textW = c.label.length * 6 + 10;
|
|
65
|
+
const bgRect = renderer.rect(curX, y + 2, textW, crumbH - 4, isLast ? '#e0f2fe' : '#f1f5f9', { color: isLast ? '#0284c7' : '#cbd5e1', width: 1 }, {
|
|
66
|
+
cursor: isLast ? 'default' : 'pointer',
|
|
67
|
+
rx: 3, ry: 3,
|
|
68
|
+
});
|
|
69
|
+
renderer.text(c.label, curX + textW / 2, y + crumbH / 2, {
|
|
70
|
+
textAnchor: 'middle', verticalAlign: 'middle', fontSize: '10px',
|
|
71
|
+
fill: isLast ? '#0369a1' : '#334155', fontWeight: isLast ? 'bold' : 'normal',
|
|
72
|
+
'pointer-events': 'none',
|
|
73
|
+
});
|
|
74
|
+
if (!isLast && typeof bgRect?.addEventListener === 'function') {
|
|
75
|
+
bgRect.addEventListener('click', (e) => {
|
|
76
|
+
e.stopPropagation();
|
|
77
|
+
this.drillToCrumb(c.index);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
curX += textW + 4;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return crumbH + 6;
|
|
84
|
+
}
|
|
85
|
+
|
|
13
86
|
_palette() {
|
|
14
87
|
if (!this._paletteInstance) {
|
|
15
88
|
this._paletteInstance = new ColorPalette(this.options.colors);
|
|
@@ -19,7 +92,8 @@ export class HierarchyChart extends Chart {
|
|
|
19
92
|
|
|
20
93
|
_topLevelColor() {
|
|
21
94
|
if (!this._topLevelColorMap) {
|
|
22
|
-
|
|
95
|
+
const active = this._activeData();
|
|
96
|
+
this._topLevelColorMap = new Map(active.map((n, i) => [n, this._palette().colorAt(i)]));
|
|
23
97
|
}
|
|
24
98
|
return this._topLevelColorMap;
|
|
25
99
|
}
|
|
@@ -30,7 +104,7 @@ export class HierarchyChart extends Chart {
|
|
|
30
104
|
}
|
|
31
105
|
|
|
32
106
|
_textColorForNode(topAncestor, depth) {
|
|
33
|
-
return
|
|
107
|
+
return ColorPalette.isDark(this._colorForNode(topAncestor, depth)) ? '#fff' : '#222';
|
|
34
108
|
}
|
|
35
109
|
|
|
36
110
|
_formatNodeValue(node) {
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
* HorizonChart — 2-band to 4-band layered area chart folding positive and
|
|
8
|
+
* negative magnitude bands into compact horizontal strips.
|
|
9
|
+
*
|
|
10
|
+
* Allows comparing many time series in a single viewport by folding higher
|
|
11
|
+
* amplitude values onto lower layers with progressive color intensity.
|
|
12
|
+
*/
|
|
13
|
+
export class HorizonChart extends Chart {
|
|
14
|
+
static chartType = 'horizon';
|
|
15
|
+
|
|
16
|
+
_render() {
|
|
17
|
+
const data = this.data;
|
|
18
|
+
if (!Array.isArray(data) || !data.length) return;
|
|
19
|
+
|
|
20
|
+
const options = this.options;
|
|
21
|
+
const box = ChartBox.fromRenderer(this.renderer);
|
|
22
|
+
const {
|
|
23
|
+
x = box.x, y = box.y, width = box.width, height = box.height,
|
|
24
|
+
title, bands = 3,
|
|
25
|
+
posColors = ['#bdd7e7', '#6baed6', '#2171b5'],
|
|
26
|
+
negColors = ['#fcae91', '#fb6a4a', '#cb181d'],
|
|
27
|
+
} = options;
|
|
28
|
+
const plotBox = { x, y, width, height };
|
|
29
|
+
|
|
30
|
+
new ChartLabels({ title }).draw(this.renderer, plotBox);
|
|
31
|
+
|
|
32
|
+
// Normalize data into series: Array<{label: string, values: number[]}>
|
|
33
|
+
const series = data.map((d, i) => {
|
|
34
|
+
const label = d.label ?? d.name ?? `Series ${i + 1}`;
|
|
35
|
+
const values = Array.isArray(d.values)
|
|
36
|
+
? d.values.map(v => typeof v === 'object' && v !== null ? v.value : Number(v) || 0)
|
|
37
|
+
: (Array.isArray(d) ? d : [Number(d.value) || 0]);
|
|
38
|
+
return { label, values };
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const numSeries = series.length;
|
|
42
|
+
const rowHeight = height / numSeries;
|
|
43
|
+
const leftLabelW = 90;
|
|
44
|
+
const stripX = x + leftLabelW;
|
|
45
|
+
const stripW = width - leftLabelW;
|
|
46
|
+
|
|
47
|
+
// Global max absolute amplitude across all series to anchor band step
|
|
48
|
+
const allVals = series.flatMap(s => s.values);
|
|
49
|
+
const maxAbs = Math.max(...allVals.map(Math.abs), 1);
|
|
50
|
+
const bandStep = maxAbs / bands;
|
|
51
|
+
|
|
52
|
+
series.forEach((s, sIdx) => {
|
|
53
|
+
const rowY = y + sIdx * rowHeight;
|
|
54
|
+
const values = s.values;
|
|
55
|
+
const N = values.length;
|
|
56
|
+
if (N < 2) return;
|
|
57
|
+
const stepX = stripW / (N - 1);
|
|
58
|
+
|
|
59
|
+
// Strip background & baseline
|
|
60
|
+
this.renderer.rect(stripX, rowY, stripW, rowHeight, 'rgba(0,0,0,0.015)', { color: '#e8e8e8', width: 0.8 });
|
|
61
|
+
this.renderer.text(s.label, stripX - 8, rowY + rowHeight / 2, {
|
|
62
|
+
textAnchor: 'end', verticalAlign: 'middle', fontSize: '11px', fontWeight: '600', fill: '#333',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Draw positive and negative bands
|
|
66
|
+
for (let b = 0; b < bands; b++) {
|
|
67
|
+
const bFloor = b * bandStep;
|
|
68
|
+
const bCeil = (b + 1) * bandStep;
|
|
69
|
+
const posColor = posColors[b % posColors.length];
|
|
70
|
+
const negColor = negColors[b % negColors.length];
|
|
71
|
+
|
|
72
|
+
// Positive band path
|
|
73
|
+
const posPts = values.map((v, i) => {
|
|
74
|
+
const clamped = Math.max(0, Math.min(bandStep, v - bFloor));
|
|
75
|
+
const py = rowY + rowHeight - (clamped / bandStep) * rowHeight;
|
|
76
|
+
return { px: stripX + i * stepX, py };
|
|
77
|
+
});
|
|
78
|
+
const posD = `M${stripX},${rowY + rowHeight} L`
|
|
79
|
+
+ posPts.map(p => `${p.px.toFixed(1)},${p.py.toFixed(1)}`).join(' L')
|
|
80
|
+
+ ` L${stripX + stripW},${rowY + rowHeight} Z`;
|
|
81
|
+
|
|
82
|
+
this.renderer.path(posD, posColor, null, {
|
|
83
|
+
'fill-opacity': 0.85,
|
|
84
|
+
tooltip: `${s.label} +Band ${b + 1}: max ${Axis.formatValue(bCeil)}`,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Negative band path (reflected upward)
|
|
88
|
+
const negPts = values.map((v, i) => {
|
|
89
|
+
const negVal = Math.max(0, -v);
|
|
90
|
+
const clamped = Math.max(0, Math.min(bandStep, negVal - bFloor));
|
|
91
|
+
const py = rowY + rowHeight - (clamped / bandStep) * rowHeight;
|
|
92
|
+
return { px: stripX + i * stepX, py };
|
|
93
|
+
});
|
|
94
|
+
const negD = `M${stripX},${rowY + rowHeight} L`
|
|
95
|
+
+ negPts.map(p => `${p.px.toFixed(1)},${p.py.toFixed(1)}`).join(' L')
|
|
96
|
+
+ ` L${stripX + stripW},${rowY + rowHeight} Z`;
|
|
97
|
+
|
|
98
|
+
this.renderer.path(negD, negColor, null, {
|
|
99
|
+
'fill-opacity': 0.85,
|
|
100
|
+
tooltip: `${s.label} -Band ${b + 1}: -${Axis.formatValue(bCeil)}`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
2
|
+
import { Chart } from '../Chart.js';
|
|
3
|
+
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* KagiChart — Japanese price-action chart independent of time.
|
|
7
|
+
*
|
|
8
|
+
* Tracks price movements with vertical lines connected by short horizontal inflection steps.
|
|
9
|
+
* Switches between Yang (thick, bullish) and Yin (thin, bearish) lines when breaking
|
|
10
|
+
* previous peaks or troughs.
|
|
11
|
+
*/
|
|
12
|
+
export class KagiChart extends Chart {
|
|
13
|
+
static chartType = 'kagi';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compute Kagi line segments and inflection points from prices.
|
|
17
|
+
* @param {Array<number|object>} data - Price sequence
|
|
18
|
+
* @param {number|string} [reversal=0.04] - Reversal amount or percentage (e.g. 4, '4%', 0.04)
|
|
19
|
+
* @returns {Array<object>}
|
|
20
|
+
*/
|
|
21
|
+
static computeKagi(data, reversal = 0.04) {
|
|
22
|
+
if (!Array.isArray(data) || data.length === 0) return { segments: [], inflections: [] };
|
|
23
|
+
|
|
24
|
+
const prices = data.map((d, i) => {
|
|
25
|
+
const val = typeof d === 'number' ? d : Number(d.close ?? d.value ?? d.y ?? 0);
|
|
26
|
+
const label = typeof d === 'object' && d !== null ? (d.label ?? d.date ?? `#${i + 1}`) : `#${i + 1}`;
|
|
27
|
+
return { price: val, label };
|
|
28
|
+
}).filter(p => Number.isFinite(p.price));
|
|
29
|
+
|
|
30
|
+
if (prices.length < 2) return { segments: [], inflections: [] };
|
|
31
|
+
|
|
32
|
+
const p0 = prices[0].price;
|
|
33
|
+
let revAmount = typeof reversal === 'string' && reversal.endsWith('%')
|
|
34
|
+
? p0 * (parseFloat(reversal) / 100)
|
|
35
|
+
: (typeof reversal === 'number' && reversal < 1 ? p0 * reversal : Number(reversal) || (p0 * 0.04));
|
|
36
|
+
|
|
37
|
+
if (revAmount <= 0) revAmount = 1;
|
|
38
|
+
|
|
39
|
+
let dir = prices[1].price >= p0 ? 1 : -1;
|
|
40
|
+
let curStart = p0;
|
|
41
|
+
let curEnd = prices[1].price;
|
|
42
|
+
let prevPeak = dir === 1 ? curEnd : p0;
|
|
43
|
+
let prevTrough = dir === -1 ? curEnd : p0;
|
|
44
|
+
let isYang = dir === 1;
|
|
45
|
+
|
|
46
|
+
const kagiColumns = [];
|
|
47
|
+
kagiColumns.push({
|
|
48
|
+
start: curStart,
|
|
49
|
+
end: curEnd,
|
|
50
|
+
dir,
|
|
51
|
+
isYang,
|
|
52
|
+
label: prices[1].label,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
for (let i = 2; i < prices.length; i++) {
|
|
56
|
+
const p = prices[i].price;
|
|
57
|
+
const curCol = kagiColumns[kagiColumns.length - 1];
|
|
58
|
+
|
|
59
|
+
if (curCol.dir === 1) {
|
|
60
|
+
// Rising
|
|
61
|
+
if (p > curCol.end) {
|
|
62
|
+
curCol.end = p;
|
|
63
|
+
curCol.label = prices[i].label;
|
|
64
|
+
if (p > prevPeak) curCol.isYang = true;
|
|
65
|
+
} else if (curCol.end - p >= revAmount) {
|
|
66
|
+
// Reverse downward
|
|
67
|
+
prevPeak = Math.max(prevPeak, curCol.end);
|
|
68
|
+
const newYang = curCol.isYang && p >= prevTrough;
|
|
69
|
+
kagiColumns.push({
|
|
70
|
+
start: curCol.end,
|
|
71
|
+
end: p,
|
|
72
|
+
dir: -1,
|
|
73
|
+
isYang: newYang,
|
|
74
|
+
label: prices[i].label,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
// Falling
|
|
79
|
+
if (p < curCol.end) {
|
|
80
|
+
curCol.end = p;
|
|
81
|
+
curCol.label = prices[i].label;
|
|
82
|
+
if (p < prevTrough) curCol.isYang = false;
|
|
83
|
+
} else if (p - curCol.end >= revAmount) {
|
|
84
|
+
// Reverse upward
|
|
85
|
+
prevTrough = Math.min(prevTrough, curCol.end);
|
|
86
|
+
const newYang = curCol.isYang || p > prevPeak;
|
|
87
|
+
kagiColumns.push({
|
|
88
|
+
start: curCol.end,
|
|
89
|
+
end: p,
|
|
90
|
+
dir: 1,
|
|
91
|
+
isYang: newYang,
|
|
92
|
+
label: prices[i].label,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { columns: kagiColumns, prevPeak, prevTrough, revAmount };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_render() {
|
|
102
|
+
const data = this.data;
|
|
103
|
+
const options = this.options || {};
|
|
104
|
+
const { columns } = KagiChart.computeKagi(data, options.reversal ?? 0.04);
|
|
105
|
+
if (!columns.length) return;
|
|
106
|
+
|
|
107
|
+
const box = ChartBox.fromRenderer(this.renderer);
|
|
108
|
+
const {
|
|
109
|
+
x = box.x, y = box.y, width = box.width, height = box.height,
|
|
110
|
+
yangColor = '#26a69a', yinColor = '#ef5350',
|
|
111
|
+
yangWidth = 3, yinWidth = 1.2,
|
|
112
|
+
title, xAxisLabel, yAxisLabel,
|
|
113
|
+
} = options;
|
|
114
|
+
|
|
115
|
+
const allPrices = columns.flatMap(c => [c.start, c.end]);
|
|
116
|
+
const vMax = Math.max(...allPrices);
|
|
117
|
+
const vMin = Math.min(...allPrices);
|
|
118
|
+
const plotBox = { x, y, width, height };
|
|
119
|
+
|
|
120
|
+
new ChartLabels({ title, xAxisLabel, yAxisLabel }).draw(this.renderer, plotBox);
|
|
121
|
+
const yDomain = new Axis().drawYRange(this.renderer, plotBox, vMin, vMax);
|
|
122
|
+
|
|
123
|
+
const colCount = columns.length;
|
|
124
|
+
const colWidth = width / Math.max(1, colCount);
|
|
125
|
+
const toY = (val) => y + height - ((val - yDomain.min) / ((yDomain.max - yDomain.min) || 1)) * height;
|
|
126
|
+
|
|
127
|
+
// Draw baseline axis
|
|
128
|
+
this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: 'black', width: 1 });
|
|
129
|
+
|
|
130
|
+
columns.forEach((col, i) => {
|
|
131
|
+
const cx = x + i * colWidth + colWidth / 2;
|
|
132
|
+
const y1 = toY(col.start);
|
|
133
|
+
const y2 = toY(col.end);
|
|
134
|
+
const color = col.isYang ? yangColor : yinColor;
|
|
135
|
+
const strokeWidth = col.isYang ? yangWidth : yinWidth;
|
|
136
|
+
|
|
137
|
+
// Horizontal inflection line from previous column
|
|
138
|
+
if (i > 0) {
|
|
139
|
+
const prevCx = x + (i - 1) * colWidth + colWidth / 2;
|
|
140
|
+
this.renderer.path(`M${prevCx},${y1} L${cx},${y1}`, 'none', { color, width: strokeWidth });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Vertical segment
|
|
144
|
+
const pathEl = this.renderer.path(`M${cx},${y1} L${cx},${y2}`, 'none', { color, width: strokeWidth }, {
|
|
145
|
+
tooltip: `Kagi ${col.dir === 1 ? 'Up' : 'Down'} (${col.isYang ? 'Yang' : 'Yin'}): ${Axis.formatValue(col.start)} → ${Axis.formatValue(col.end)}`,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
this._datum(pathEl, {
|
|
149
|
+
label: col.label,
|
|
150
|
+
start: col.start,
|
|
151
|
+
end: col.end,
|
|
152
|
+
type: col.isYang ? 'Yang' : 'Yin',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Inflection point markers
|
|
156
|
+
this.renderer.circle(cx, y2, 3, color, { color: '#ffffff', width: 1 });
|
|
157
|
+
|
|
158
|
+
// Periodic category/turn labels
|
|
159
|
+
const step = Math.max(1, Math.floor(colCount / 8));
|
|
160
|
+
if (i % step === 0) {
|
|
161
|
+
this.renderer.text(col.label, cx, y + height + 18, {
|
|
162
|
+
textAnchor: this.renderer.centerTextAnchor, fontSize: '9px', fill: '#555',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|