@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,164 @@
|
|
|
1
|
+
import { Axis } from '../Axis.js';
|
|
2
|
+
import { Statistics } from '../Statistics.js';
|
|
3
|
+
import { GroupedDistributionChart } from './GroupedDistributionChart.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BeeswarmChart — individual sample points deterministically force/circle-packed
|
|
7
|
+
* along each group's vertical slot (`Array<{label, values: number[]}>`) so every
|
|
8
|
+
* observation is visible without random jitter.
|
|
9
|
+
*
|
|
10
|
+
* When `raincloud: true` (default `true` unless disabled via `raincloud: false`),
|
|
11
|
+
* combines:
|
|
12
|
+
* 1. A right-half Gaussian KDE "cloud" silhouette,
|
|
13
|
+
* 2. A compact median + IQR mini-boxplot on the slot axis, and
|
|
14
|
+
* 3. The packed beeswarm "rain" dots on the left.
|
|
15
|
+
*/
|
|
16
|
+
export class BeeswarmChart extends GroupedDistributionChart {
|
|
17
|
+
static chartType = 'beeswarm';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Deterministic 1D circle-packing horizontal offsets for `values` mapped
|
|
21
|
+
* through `toY(v)` with dot radius `r`, bounded to `[-maxOffset, +maxOffset]`.
|
|
22
|
+
* @returns {Array<{value: number, py: number, dx: number}>}
|
|
23
|
+
*/
|
|
24
|
+
static packOffsets(values, toY, r = 4, maxOffset = 28) {
|
|
25
|
+
const indexed = values.map((v, idx) => ({ value: v, py: toY(v), idx, dx: 0 }));
|
|
26
|
+
indexed.sort((a, b) => a.py - b.py);
|
|
27
|
+
const minDist = r * 2.05;
|
|
28
|
+
const minDistSq = minDist * minDist;
|
|
29
|
+
const placed = [];
|
|
30
|
+
|
|
31
|
+
for (const item of indexed) {
|
|
32
|
+
// Find candidate dx with minimal |dx| that does not collide with nearby placed dots
|
|
33
|
+
let bestDx = 0;
|
|
34
|
+
const nearby = placed.filter(p => Math.abs(p.py - item.py) < minDist);
|
|
35
|
+
if (nearby.length > 0) {
|
|
36
|
+
const step = Math.max(1.5, r * 0.65);
|
|
37
|
+
for (let mag = 0; mag <= maxOffset; mag += step) {
|
|
38
|
+
const candidates = mag === 0 ? [0] : (placed.length % 2 === 0 ? [mag, -mag] : [-mag, mag]);
|
|
39
|
+
let found = false;
|
|
40
|
+
for (const cand of candidates) {
|
|
41
|
+
const collides = nearby.some(p => {
|
|
42
|
+
const dy = p.py - item.py;
|
|
43
|
+
const ddx = p.dx - cand;
|
|
44
|
+
return ddx * ddx + dy * dy < minDistSq;
|
|
45
|
+
});
|
|
46
|
+
if (!collides) {
|
|
47
|
+
bestDx = cand;
|
|
48
|
+
found = true;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (found) break;
|
|
53
|
+
bestDx = (placed.length % 2 === 0 ? 1 : -1) * Math.min(maxOffset, mag);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
item.dx = bestDx;
|
|
57
|
+
placed.push(item);
|
|
58
|
+
}
|
|
59
|
+
return placed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_render() {
|
|
63
|
+
if (!Array.isArray(this.data) || !this.data.length) return;
|
|
64
|
+
this._setup();
|
|
65
|
+
this._domain = this._computeDomain();
|
|
66
|
+
this._drawBaseAxis();
|
|
67
|
+
|
|
68
|
+
// Draw Y-axis ticks for readability
|
|
69
|
+
const { x, y, width, height } = this._plotBox;
|
|
70
|
+
const { vMin, vMax, range } = this._domain;
|
|
71
|
+
const ticks = Axis.niceTicks(range, 5).map(t => vMin + t).filter(t => t <= vMax + 1e-6);
|
|
72
|
+
for (const tick of ticks) {
|
|
73
|
+
const ty = this._toY(tick);
|
|
74
|
+
this.renderer.path(`M${x},${ty} L${x + width},${ty}`, 'none', { color: '#eee', width: 1, dasharray: '3,3' });
|
|
75
|
+
this.renderer.text(Axis.formatValue(tick), x - 6, ty, {
|
|
76
|
+
textAnchor: 'end', verticalAlign: 'middle', fontSize: '10px', fill: '#666',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const slotWidth = this._slotWidth();
|
|
81
|
+
this.data.forEach((d, i) => {
|
|
82
|
+
const cx = this._plotBox.x + i * slotWidth + slotWidth / 2;
|
|
83
|
+
this._renderGlyph(cx, slotWidth, i, d);
|
|
84
|
+
this._drawXAxisLabel(cx, d.label);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_renderGlyph(cx, slotWidth, i, d) {
|
|
89
|
+
const values = Array.isArray(d.values) ? d.values : [];
|
|
90
|
+
if (!values.length) return;
|
|
91
|
+
|
|
92
|
+
const color = this._palette.colorAt(i);
|
|
93
|
+
const darkColor = this._palette.darken(color, 0.25);
|
|
94
|
+
const {
|
|
95
|
+
raincloud = true,
|
|
96
|
+
dotRadius = 4,
|
|
97
|
+
} = this.options;
|
|
98
|
+
|
|
99
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
100
|
+
const q1 = Statistics.quantile(sorted, 0.25);
|
|
101
|
+
const median = Statistics.quantile(sorted, 0.5);
|
|
102
|
+
const q3 = Statistics.quantile(sorted, 0.75);
|
|
103
|
+
const halfSlot = slotWidth * 0.42;
|
|
104
|
+
|
|
105
|
+
if (raincloud) {
|
|
106
|
+
// 1. Right-half KDE density "cloud"
|
|
107
|
+
const curve = Statistics.kernelDensityEstimate(values, { min: this._domain.vMin, max: this._domain.vMax });
|
|
108
|
+
const maxDensity = Math.max(...curve.map(p => p.density)) || 1;
|
|
109
|
+
const cloudWidth = halfSlot * 0.85;
|
|
110
|
+
const axisX = cx + 2;
|
|
111
|
+
const rightPts = curve.map(p => ({
|
|
112
|
+
px: axisX + (p.density / maxDensity) * cloudWidth,
|
|
113
|
+
py: this._toY(p.x),
|
|
114
|
+
}));
|
|
115
|
+
const cloudPath = `M${axisX},${rightPts[0].py} L`
|
|
116
|
+
+ rightPts.map(p => `${p.px},${p.py}`).join(' L')
|
|
117
|
+
+ ` L${axisX},${rightPts[rightPts.length - 1].py} Z`;
|
|
118
|
+
const cloudEl = this._datum(this.renderer.path(cloudPath, color, { color: darkColor, width: 1.2 }, {
|
|
119
|
+
'fill-opacity': 0.55,
|
|
120
|
+
tooltip: `${d.label}: n=${values.length}, median=${Axis.formatValue(median)} (IQR ${Axis.formatValue(q1)}–${Axis.formatValue(q3)})`,
|
|
121
|
+
}), { label: d.label, series: d.label, value: median });
|
|
122
|
+
this._wireHoverTransform([cloudEl], { dx: 2, dy: -2 });
|
|
123
|
+
|
|
124
|
+
// 2. Compact IQR mini-box + median dot at cx
|
|
125
|
+
const yQ1 = this._toY(q1);
|
|
126
|
+
const yQ3 = this._toY(q3);
|
|
127
|
+
const yMed = this._toY(median);
|
|
128
|
+
this.renderer.rect(cx - 3, Math.min(yQ1, yQ3), 6, Math.max(2, Math.abs(yQ1 - yQ3)), darkColor, null, {
|
|
129
|
+
rx: 2, ry: 2,
|
|
130
|
+
});
|
|
131
|
+
this.renderer.circle(cx, yMed, 2.5, '#ffffff', { color: darkColor, width: 1 });
|
|
132
|
+
|
|
133
|
+
// 3. Packed beeswarm "rain" dots on the left of cx
|
|
134
|
+
const maxRainSpan = Math.max(8, halfSlot * 0.75);
|
|
135
|
+
const packed = BeeswarmChart.packOffsets(values, v => this._toY(v), dotRadius, maxRainSpan * 0.5);
|
|
136
|
+
const swarmCenterX = cx - halfSlot * 0.45;
|
|
137
|
+
for (const pt of packed) {
|
|
138
|
+
const dotX = swarmCenterX + pt.dx;
|
|
139
|
+
const dotEl = this._datum(this.renderer.circle(dotX, pt.py, dotRadius, color, {
|
|
140
|
+
color: '#ffffff', width: 1,
|
|
141
|
+
}, {
|
|
142
|
+
tooltip: `${d.label}: ${Axis.formatValue(pt.value)}`,
|
|
143
|
+
}), { label: d.label, series: d.label, value: pt.value });
|
|
144
|
+
this._wireHoverTransform([dotEl], { dx: 0, dy: -2 });
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
// Pure symmetrical Beeswarm around cx + median crossbar
|
|
148
|
+
const maxSpan = Math.max(10, halfSlot * 0.85);
|
|
149
|
+
const packed = BeeswarmChart.packOffsets(values, v => this._toY(v), dotRadius, maxSpan);
|
|
150
|
+
const yMed = this._toY(median);
|
|
151
|
+
this.renderer.path(`M${cx - maxSpan * 0.7},${yMed} L${cx + maxSpan * 0.7},${yMed}`, 'none', {
|
|
152
|
+
color: darkColor, width: 2, dasharray: '4,2',
|
|
153
|
+
});
|
|
154
|
+
for (const pt of packed) {
|
|
155
|
+
const dotEl = this._datum(this.renderer.circle(cx + pt.dx, pt.py, dotRadius, color, {
|
|
156
|
+
color: '#ffffff', width: 1,
|
|
157
|
+
}, {
|
|
158
|
+
tooltip: `${d.label}: ${Axis.formatValue(pt.value)}`,
|
|
159
|
+
}), { label: d.label, series: d.label, value: pt.value });
|
|
160
|
+
this._wireHoverTransform([dotEl], { dx: 0, dy: -2 });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -23,9 +23,31 @@ export class BoxPlotChart extends GroupedDistributionChart {
|
|
|
23
23
|
|
|
24
24
|
_renderGlyph(cx, slotWidth, i, d) {
|
|
25
25
|
const s = Statistics.boxPlotStats(d.values);
|
|
26
|
-
const boxWidth = slotWidth * 0.5;
|
|
27
26
|
const boxColor = this.options.boxColor ?? this._palette.colorAt(0);
|
|
28
27
|
|
|
28
|
+
if (this._isHorizontal()) {
|
|
29
|
+
const cy = cx;
|
|
30
|
+
const slotHeight = slotWidth;
|
|
31
|
+
const boxHeight = slotHeight * 0.5;
|
|
32
|
+
const xMin = this._toX(s.min), xMax = this._toX(s.max);
|
|
33
|
+
const xQ1 = this._toX(s.q1), xQ3 = this._toX(s.q3);
|
|
34
|
+
const xMed = this._toX(s.median);
|
|
35
|
+
|
|
36
|
+
this.renderer.path(`M${xMin},${cy} L${xMax},${cy}`, 'none', { color: '#555', width: 1 });
|
|
37
|
+
this.renderer.path(`M${xMin},${cy - boxHeight / 4} L${xMin},${cy + boxHeight / 4}`, 'none', { color: '#555', width: 1 });
|
|
38
|
+
this.renderer.path(`M${xMax},${cy - boxHeight / 4} L${xMax},${cy + boxHeight / 4}`, 'none', { color: '#555', width: 1 });
|
|
39
|
+
this._datum(this.renderer.rect(Math.min(xQ1, xQ3), cy - boxHeight / 2, Math.max(1, Math.abs(xQ3 - xQ1)), boxHeight, boxColor, { color: '#333', width: 1 }, {
|
|
40
|
+
tooltip: `${d.label}: median ${Axis.formatValue(s.median)}, Q1 ${Axis.formatValue(s.q1)}, Q3 ${Axis.formatValue(s.q3)}`,
|
|
41
|
+
}), { label: d.label, series: d.label });
|
|
42
|
+
this.renderer.path(`M${xMed},${cy - boxHeight / 2} L${xMed},${cy + boxHeight / 2}`, 'none', { color: '#fff', width: 2 });
|
|
43
|
+
s.outliers.forEach(v => {
|
|
44
|
+
this.renderer.circle(this._toX(v), cy, 3, 'none', { color: boxColor, width: 1.5 }, { tooltip: `${d.label} outlier: ${Axis.formatValue(v)}` });
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const boxWidth = slotWidth * 0.5;
|
|
50
|
+
|
|
29
51
|
this.renderer.path(`M${cx},${this._toY(s.min)} L${cx},${this._toY(s.max)}`, 'none', { color: '#555', width: 1 });
|
|
30
52
|
this.renderer.path(`M${cx - boxWidth / 4},${this._toY(s.min)} L${cx + boxWidth / 4},${this._toY(s.min)}`, 'none', { color: '#555', width: 1 });
|
|
31
53
|
this.renderer.path(`M${cx - boxWidth / 4},${this._toY(s.max)} L${cx + boxWidth / 4},${this._toY(s.max)}`, 'none', { color: '#555', width: 1 });
|
|
@@ -72,6 +72,7 @@ export class CalendarHeatmapChart extends Chart {
|
|
|
72
72
|
cellSize,
|
|
73
73
|
cellGap = 2,
|
|
74
74
|
showValues = false,
|
|
75
|
+
midValue,
|
|
75
76
|
title,
|
|
76
77
|
monthLabels = true,
|
|
77
78
|
weekdayLabels = true,
|
|
@@ -179,7 +180,7 @@ export class CalendarHeatmapChart extends Chart {
|
|
|
179
180
|
const cx = gridX + week * step;
|
|
180
181
|
const cy = gridY + day * step;
|
|
181
182
|
const color = hasValue
|
|
182
|
-
? ColorPalette.scaleColor(colorScale, vMin, vMax, value)
|
|
183
|
+
? ColorPalette.scaleColor(colorScale, vMin, vMax, value, midValue)
|
|
183
184
|
: emptyColor;
|
|
184
185
|
|
|
185
186
|
this.renderer.rect(cx, cy, size, size, color, null, {
|
|
@@ -5,32 +5,140 @@ import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* CandlestickChart — OHLC financial chart. Each entry draws a vertical wick
|
|
7
7
|
* from low to high and a rectangular body from open to close.
|
|
8
|
+
*
|
|
9
|
+
* Supported options:
|
|
10
|
+
* - volume: boolean (shows volume sub-pane if data has .volume, or if volume: true)
|
|
11
|
+
* - sma: number | number[] | Array<{period: number, color?: string}>
|
|
12
|
+
* - ema: number | number[] | Array<{period: number, color?: string}>
|
|
13
|
+
* - bollinger: boolean | { period?: number, stdDev?: number, color?: string, bandColor?: string }
|
|
8
14
|
*/
|
|
9
15
|
export class CandlestickChart extends Chart {
|
|
10
16
|
static chartType = 'candlestick';
|
|
11
17
|
|
|
18
|
+
static computeSMA(data, period) {
|
|
19
|
+
const result = new Array(data.length).fill(null);
|
|
20
|
+
let sum = 0;
|
|
21
|
+
for (let i = 0; i < data.length; i++) {
|
|
22
|
+
sum += data[i].close;
|
|
23
|
+
if (i >= period) {
|
|
24
|
+
sum -= data[i - period].close;
|
|
25
|
+
}
|
|
26
|
+
if (i >= period - 1) {
|
|
27
|
+
result[i] = sum / period;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static computeEMA(data, period) {
|
|
34
|
+
const result = new Array(data.length).fill(null);
|
|
35
|
+
if (data.length < period) return result;
|
|
36
|
+
const k = 2 / (period + 1);
|
|
37
|
+
let sum = 0;
|
|
38
|
+
for (let i = 0; i < period; i++) sum += data[i].close;
|
|
39
|
+
let prevEma = sum / period;
|
|
40
|
+
result[period - 1] = prevEma;
|
|
41
|
+
for (let i = period; i < data.length; i++) {
|
|
42
|
+
prevEma = data[i].close * k + prevEma * (1 - k);
|
|
43
|
+
result[i] = prevEma;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static computeBollinger(data, period = 20, stdDev = 2) {
|
|
49
|
+
const sma = CandlestickChart.computeSMA(data, period);
|
|
50
|
+
const upper = new Array(data.length).fill(null);
|
|
51
|
+
const lower = new Array(data.length).fill(null);
|
|
52
|
+
for (let i = period - 1; i < data.length; i++) {
|
|
53
|
+
const mean = sma[i];
|
|
54
|
+
let sqDiffSum = 0;
|
|
55
|
+
for (let j = i - period + 1; j <= i; j++) {
|
|
56
|
+
sqDiffSum += Math.pow(data[j].close - mean, 2);
|
|
57
|
+
}
|
|
58
|
+
const sd = Math.sqrt(sqDiffSum / period);
|
|
59
|
+
upper[i] = mean + stdDev * sd;
|
|
60
|
+
lower[i] = mean - stdDev * sd;
|
|
61
|
+
}
|
|
62
|
+
return { middle: sma, upper, lower };
|
|
63
|
+
}
|
|
64
|
+
|
|
12
65
|
_render() {
|
|
13
66
|
const data = this.data;
|
|
14
|
-
const options = this.options;
|
|
67
|
+
const options = this.options || {};
|
|
15
68
|
const box = ChartBox.fromRenderer(this.renderer);
|
|
16
69
|
const {
|
|
17
70
|
x = box.x, y = box.y, width = box.width, height = box.height,
|
|
18
71
|
upColor = '#26a69a', downColor = '#ef5350', title, xAxisLabel, yAxisLabel,
|
|
19
72
|
markers = [],
|
|
73
|
+
volume = (data.some(d => d.volume != null) && options.volume !== false),
|
|
74
|
+
sma, ema, bollinger,
|
|
20
75
|
} = options;
|
|
21
76
|
|
|
77
|
+
if (!data || !data.length) return;
|
|
78
|
+
|
|
79
|
+
const hasVolume = Boolean(volume && data.some(d => d.volume != null));
|
|
80
|
+
const priceHeight = hasVolume ? height * 0.72 : height;
|
|
81
|
+
const volGap = hasVolume ? height * 0.06 : 0;
|
|
82
|
+
const volHeight = hasVolume ? height - priceHeight - volGap : 0;
|
|
83
|
+
const volY = y + priceHeight + volGap;
|
|
84
|
+
|
|
22
85
|
const vMax = Math.max(...data.map(d => d.high));
|
|
23
86
|
const vMin = Math.min(...data.map(d => d.low));
|
|
24
87
|
const range = (vMax - vMin) || 1;
|
|
25
88
|
const slotWidth = width / data.length;
|
|
26
89
|
const labelStep = Axis.categoryLabelStep(data.map(d => d.label), width, { fontSize: 10 });
|
|
27
90
|
const bodyWidth = slotWidth * 0.6;
|
|
28
|
-
const toY = (v) => y +
|
|
91
|
+
const toY = (v) => y + priceHeight - ((v - vMin) / range) * priceHeight;
|
|
29
92
|
const textAnchor = this.renderer.centerTextAnchor;
|
|
30
93
|
|
|
31
94
|
new ChartLabels({ title, xAxisLabel, yAxisLabel }).draw(this.renderer, { x, y, width, height });
|
|
32
|
-
this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: 'black', width: 1 });
|
|
33
95
|
|
|
96
|
+
// Axes boundaries
|
|
97
|
+
this.renderer.path(`M${x},${y} L${x},${y + priceHeight} L${x + width},${y + priceHeight}`, 'none', { color: 'black', width: 1 });
|
|
98
|
+
|
|
99
|
+
// 1. Technical indicator: Bollinger Bands background
|
|
100
|
+
if (bollinger) {
|
|
101
|
+
const bOpts = typeof bollinger === 'object' ? bollinger : {};
|
|
102
|
+
const { period = 20, stdDev = 2, bandColor = 'rgba(33, 150, 243, 0.12)', strokeColor = '#2196f3' } = bOpts;
|
|
103
|
+
const bb = CandlestickChart.computeBollinger(data, period, stdDev);
|
|
104
|
+
|
|
105
|
+
let bandPath = '';
|
|
106
|
+
const validIndices = [];
|
|
107
|
+
for (let i = 0; i < data.length; i++) {
|
|
108
|
+
if (bb.upper[i] != null && bb.lower[i] != null) validIndices.push(i);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (validIndices.length > 1) {
|
|
112
|
+
// Top curve
|
|
113
|
+
validIndices.forEach((idx, i) => {
|
|
114
|
+
const cx = x + idx * slotWidth + slotWidth / 2;
|
|
115
|
+
const cy = toY(bb.upper[idx]);
|
|
116
|
+
bandPath += (i === 0 ? `M${cx},${cy} ` : `L${cx},${cy} `);
|
|
117
|
+
});
|
|
118
|
+
// Bottom curve back
|
|
119
|
+
for (let i = validIndices.length - 1; i >= 0; i--) {
|
|
120
|
+
const idx = validIndices[i];
|
|
121
|
+
const cx = x + idx * slotWidth + slotWidth / 2;
|
|
122
|
+
const cy = toY(bb.lower[idx]);
|
|
123
|
+
bandPath += `L${cx},${cy} `;
|
|
124
|
+
}
|
|
125
|
+
bandPath += 'Z';
|
|
126
|
+
this.renderer.path(bandPath, bandColor, { color: 'none' });
|
|
127
|
+
|
|
128
|
+
// Bollinger middle line
|
|
129
|
+
let midPoints = [];
|
|
130
|
+
validIndices.forEach(idx => {
|
|
131
|
+
const cx = x + idx * slotWidth + slotWidth / 2;
|
|
132
|
+
midPoints.push([cx, toY(bb.middle[idx])]);
|
|
133
|
+
});
|
|
134
|
+
if (midPoints.length > 1) {
|
|
135
|
+
const lineData = midPoints.map((p, i) => `${i === 0 ? 'M' : 'L'}${p[0]},${p[1]}`).join(' ');
|
|
136
|
+
this.renderer.path(lineData, 'none', { color: strokeColor, width: 1, strokeDasharray: '3 3' });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 2. Candlestick wicks & bodies
|
|
34
142
|
data.forEach((d, i) => {
|
|
35
143
|
const cx = x + i * slotWidth + slotWidth / 2;
|
|
36
144
|
const isUp = d.close >= d.open;
|
|
@@ -39,13 +147,73 @@ export class CandlestickChart extends Chart {
|
|
|
39
147
|
const bodyTop = toY(Math.max(d.open, d.close));
|
|
40
148
|
const bodyBottom = toY(Math.min(d.open, d.close));
|
|
41
149
|
this.renderer.rect(cx - bodyWidth / 2, bodyTop, bodyWidth, Math.max(1, bodyBottom - bodyTop), color, null, {
|
|
42
|
-
tooltip: `${d.label}: O ${d.open} H ${d.high} L ${d.low} C ${d.close}`,
|
|
150
|
+
tooltip: `${d.label}: O ${d.open} H ${d.high} L ${d.low} C ${d.close}${d.volume ? ` Vol ${d.volume}` : ''}`,
|
|
43
151
|
});
|
|
44
152
|
if (i % labelStep === 0) {
|
|
45
153
|
this.renderer.text(d.label, cx, y + height + 20, { textAnchor, fontSize: '10px' });
|
|
46
154
|
}
|
|
47
155
|
});
|
|
48
156
|
|
|
157
|
+
// 3. Technical indicators: SMA & EMA overlays
|
|
158
|
+
const drawIndicatorLine = (values, lineColor) => {
|
|
159
|
+
let path = '';
|
|
160
|
+
let started = false;
|
|
161
|
+
values.forEach((v, idx) => {
|
|
162
|
+
if (v == null) return;
|
|
163
|
+
const cx = x + idx * slotWidth + slotWidth / 2;
|
|
164
|
+
const cy = toY(v);
|
|
165
|
+
path += (!started ? `M${cx},${cy} ` : `L${cx},${cy} `);
|
|
166
|
+
started = true;
|
|
167
|
+
});
|
|
168
|
+
if (path) this.renderer.path(path.trim(), 'none', { color: lineColor, width: 1.5 });
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (sma) {
|
|
172
|
+
const smaList = Array.isArray(sma) ? sma : [sma];
|
|
173
|
+
const smaColors = ['#e67e22', '#9b59b6', '#3498db'];
|
|
174
|
+
smaList.forEach((spec, sIdx) => {
|
|
175
|
+
const period = typeof spec === 'object' ? spec.period : spec;
|
|
176
|
+
const color = (typeof spec === 'object' && spec.color) ? spec.color : smaColors[sIdx % smaColors.length];
|
|
177
|
+
const vals = CandlestickChart.computeSMA(data, period);
|
|
178
|
+
drawIndicatorLine(vals, color);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (ema) {
|
|
183
|
+
const emaList = Array.isArray(ema) ? ema : [ema];
|
|
184
|
+
const emaColors = ['#1abc9c', '#f1c40f', '#e91e63'];
|
|
185
|
+
emaList.forEach((spec, eIdx) => {
|
|
186
|
+
const period = typeof spec === 'object' ? spec.period : spec;
|
|
187
|
+
const color = (typeof spec === 'object' && spec.color) ? spec.color : emaColors[eIdx % emaColors.length];
|
|
188
|
+
const vals = CandlestickChart.computeEMA(data, period);
|
|
189
|
+
drawIndicatorLine(vals, color);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 4. Volume sub-pane
|
|
194
|
+
if (hasVolume) {
|
|
195
|
+
this.renderer.path(`M${x},${volY} L${x},${volY + volHeight} L${x + width},${volY + volHeight}`, 'none', { color: '#aaa', width: 1 });
|
|
196
|
+
const maxVol = Math.max(...data.map(d => d.volume || 0)) || 1;
|
|
197
|
+
|
|
198
|
+
data.forEach((d, i) => {
|
|
199
|
+
if (!d.volume) return;
|
|
200
|
+
const cx = x + i * slotWidth + slotWidth / 2;
|
|
201
|
+
const barH = (d.volume / maxVol) * volHeight;
|
|
202
|
+
const barTop = volY + volHeight - barH;
|
|
203
|
+
const isUp = d.close >= d.open;
|
|
204
|
+
const barColor = isUp ? upColor : downColor;
|
|
205
|
+
this.renderer.rect(cx - bodyWidth / 2, barTop, bodyWidth, Math.max(1, barH), barColor, null, {
|
|
206
|
+
opacity: 0.7,
|
|
207
|
+
tooltip: `${d.label} Volume: ${Axis.formatValue(d.volume)}`,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Volume axis label
|
|
212
|
+
this.renderer.text('Vol', x - 6, volY + 10, { textAnchor: 'end', fontSize: '9px', fill: '#666' });
|
|
213
|
+
this.renderer.text(Axis.formatValue(maxVol), x - 6, volY + volHeight, { textAnchor: 'end', fontSize: '9px', fill: '#888' });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 5. Markers
|
|
49
217
|
markers.forEach((m) => {
|
|
50
218
|
const idx = data.findIndex(d => d.label === m.label);
|
|
51
219
|
if (idx < 0) return;
|
|
@@ -80,6 +80,7 @@ export class ChordDiagram extends Chart {
|
|
|
80
80
|
// Draw chords first so they sit under the node rim.
|
|
81
81
|
const maxLink = Math.max(...links.map(l => l.value));
|
|
82
82
|
const maxStroke = radius * 0.08;
|
|
83
|
+
const chordRecords = [];
|
|
83
84
|
links.forEach(l => {
|
|
84
85
|
const s = nodeMap.get(l.source);
|
|
85
86
|
const t = nodeMap.get(l.target);
|
|
@@ -92,17 +93,20 @@ export class ChordDiagram extends Chart {
|
|
|
92
93
|
const c2x = cx + (innerRadius * 0.2) * Math.cos(t.mid);
|
|
93
94
|
const c2y = cy + (innerRadius * 0.2) * Math.sin(t.mid);
|
|
94
95
|
const width = Math.max(1, (l.value / maxLink) * maxStroke);
|
|
95
|
-
this._datum(this.renderer.path(`M${sx},${sy} C${c1x},${c1y} ${c2x},${c2y} ${tx},${ty}`, 'none', {
|
|
96
|
+
const el = this._datum(this.renderer.path(`M${sx},${sy} C${c1x},${c1y} ${c2x},${c2y} ${tx},${ty}`, 'none', {
|
|
96
97
|
color: s.color, width,
|
|
97
98
|
}, {
|
|
98
99
|
tooltip: `${s.label} ↔ ${t.label}: ${l.value}`,
|
|
99
100
|
}), { label: s.label, series: t.label });
|
|
101
|
+
if (el?.style) el.style.opacity = '0.72';
|
|
102
|
+
chordRecords.push({ el, source: s, target: t });
|
|
100
103
|
});
|
|
101
104
|
|
|
102
105
|
// Draw node arcs.
|
|
106
|
+
const arcRecords = [];
|
|
103
107
|
nodes.forEach(n => {
|
|
104
108
|
const path = this._arcPath(cx, cy, radius, innerRadius, n.start, n.end);
|
|
105
|
-
this._datum(this.renderer.path(path, n.color, { color: '#fff', width: 1 }, {
|
|
109
|
+
const arcEl = this._datum(this.renderer.path(path, n.color, { color: '#fff', width: 1 }, {
|
|
106
110
|
tooltip: `${n.label}: ${n.value}`,
|
|
107
111
|
}), { label: n.label });
|
|
108
112
|
|
|
@@ -115,7 +119,106 @@ export class ChordDiagram extends Chart {
|
|
|
115
119
|
textAnchor: 'middle', fontSize: '10px', fill: '#333',
|
|
116
120
|
transform: `rotate(${rotate - 90},${lx},${ly})`,
|
|
117
121
|
});
|
|
122
|
+
arcRecords.push({ el: arcEl, node: n });
|
|
118
123
|
});
|
|
124
|
+
|
|
125
|
+
this._wireChordHover(arcRecords, chordRecords, { cx, cy });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_wireChordHover(arcRecords, chordRecords, center = {}) {
|
|
129
|
+
const { cx = 0, cy = 0 } = center;
|
|
130
|
+
if (this.renderer?.supportsSceneGraph !== true || this.options?.hoverEffect === false) return;
|
|
131
|
+
const resetAll = () => {
|
|
132
|
+
for (const c of chordRecords) {
|
|
133
|
+
if (c.el?.style) {
|
|
134
|
+
c.el.style.opacity = '0.72';
|
|
135
|
+
c.el.style.filter = '';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const a of arcRecords) {
|
|
139
|
+
if (a.el?.style) {
|
|
140
|
+
a.el.style.opacity = '1';
|
|
141
|
+
a.el.style.transform = 'translate(0px, 0px)';
|
|
142
|
+
a.el.style.filter = '';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
for (const aRec of arcRecords) {
|
|
148
|
+
if (typeof aRec.el?.addEventListener !== 'function') continue;
|
|
149
|
+
aRec.el._hoverWired = true;
|
|
150
|
+
const dx = (Math.cos(aRec.node.mid) * 8).toFixed(2);
|
|
151
|
+
const dy = (Math.sin(aRec.node.mid) * 8).toFixed(2);
|
|
152
|
+
if (aRec.el.style) {
|
|
153
|
+
aRec.el.style.transition = 'transform 0.22s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.2s ease, filter 0.2s ease';
|
|
154
|
+
aRec.el.style.cursor = 'pointer';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (this.options?.draggable !== false) {
|
|
158
|
+
aRec.el.addEventListener('pointerdown', (e) => {
|
|
159
|
+
if (e.button !== 0) return;
|
|
160
|
+
e.preventDefault();
|
|
161
|
+
const onMove = (me) => {
|
|
162
|
+
const rect = this.renderer.container?.getBoundingClientRect?.() || { left: 0, top: 0 };
|
|
163
|
+
const mx = me.clientX - rect.left;
|
|
164
|
+
const my = me.clientY - rect.top;
|
|
165
|
+
const angle = Math.atan2(my - cy, mx - cx);
|
|
166
|
+
this.options?.onNodeDrag?.(aRec.node.id, angle);
|
|
167
|
+
};
|
|
168
|
+
const onUp = () => {
|
|
169
|
+
window.removeEventListener('pointermove', onMove);
|
|
170
|
+
window.removeEventListener('pointerup', onUp);
|
|
171
|
+
};
|
|
172
|
+
window.addEventListener('pointermove', onMove);
|
|
173
|
+
window.addEventListener('pointerup', onUp);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
aRec.el.addEventListener('mouseenter', () => {
|
|
177
|
+
const connected = new Set([aRec.node]);
|
|
178
|
+
for (const c of chordRecords) {
|
|
179
|
+
const hit = c.source === aRec.node || c.target === aRec.node;
|
|
180
|
+
if (hit) {
|
|
181
|
+
connected.add(c.source);
|
|
182
|
+
connected.add(c.target);
|
|
183
|
+
}
|
|
184
|
+
if (c.el?.style) {
|
|
185
|
+
c.el.style.opacity = hit ? '0.96' : '0.12';
|
|
186
|
+
c.el.style.filter = hit ? 'drop-shadow(0 2px 5px rgba(0,0,0,0.22))' : '';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const other of arcRecords) {
|
|
190
|
+
if (!other.el?.style) continue;
|
|
191
|
+
other.el.style.opacity = connected.has(other.node) ? '1' : '0.4';
|
|
192
|
+
}
|
|
193
|
+
if (aRec.el.style) {
|
|
194
|
+
aRec.el.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
195
|
+
aRec.el.style.filter = 'drop-shadow(0 4px 7px rgba(0,0,0,0.24))';
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
aRec.el.addEventListener('mouseleave', resetAll);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const cRec of chordRecords) {
|
|
202
|
+
if (typeof cRec.el?.addEventListener !== 'function') continue;
|
|
203
|
+
cRec.el._hoverWired = true;
|
|
204
|
+
if (cRec.el.style) {
|
|
205
|
+
cRec.el.style.transition = 'opacity 0.2s ease, filter 0.2s ease';
|
|
206
|
+
cRec.el.style.cursor = 'pointer';
|
|
207
|
+
}
|
|
208
|
+
cRec.el.addEventListener('mouseenter', () => {
|
|
209
|
+
for (const other of chordRecords) {
|
|
210
|
+
if (!other.el?.style) continue;
|
|
211
|
+
other.el.style.opacity = other === cRec ? '0.98' : '0.12';
|
|
212
|
+
other.el.style.filter = other === cRec ? 'drop-shadow(0 2px 6px rgba(0,0,0,0.25))' : '';
|
|
213
|
+
}
|
|
214
|
+
for (const aRec of arcRecords) {
|
|
215
|
+
if (!aRec.el?.style) continue;
|
|
216
|
+
const hit = aRec.node === cRec.source || aRec.node === cRec.target;
|
|
217
|
+
aRec.el.style.opacity = hit ? '1' : '0.4';
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
cRec.el.addEventListener('mouseleave', resetAll);
|
|
221
|
+
}
|
|
119
222
|
}
|
|
120
223
|
|
|
121
224
|
_arcPath(cx, cy, rOuter, rInner, a0, a1) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ChartBox, ChartLabels } from '../ChartBox.js';
|
|
2
|
+
import { Hierarchy } from '../Hierarchy.js';
|
|
3
|
+
import { HierarchyChart } from './HierarchyChart.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CirclePackChart — nested tangent circles sized by value (`HierarchyChart`
|
|
7
|
+
* subclass over `Array<{label, value?, children?}>`).
|
|
8
|
+
*
|
|
9
|
+
* Branch circles render with a translucent tinted fill and crisp rim so child
|
|
10
|
+
* circles nested inside read clearly as a hierarchy, and hovering any circle
|
|
11
|
+
* elevates it with a drop shadow.
|
|
12
|
+
*/
|
|
13
|
+
export class CirclePackChart extends HierarchyChart {
|
|
14
|
+
static chartType = 'circlePack';
|
|
15
|
+
|
|
16
|
+
_render() {
|
|
17
|
+
const data = this.data;
|
|
18
|
+
if (!Array.isArray(data) || !data.length) return;
|
|
19
|
+
const options = this.options;
|
|
20
|
+
const box = ChartBox.fromRenderer(this.renderer);
|
|
21
|
+
const {
|
|
22
|
+
x = box.x, y = box.y, width = box.width, height = box.height,
|
|
23
|
+
title, padding = 4, showValues = false,
|
|
24
|
+
} = options;
|
|
25
|
+
const plotBox = { x, y, width, height };
|
|
26
|
+
|
|
27
|
+
new ChartLabels({ title }).draw(this.renderer, plotBox);
|
|
28
|
+
|
|
29
|
+
const cx = x + width / 2;
|
|
30
|
+
const cy = y + height / 2;
|
|
31
|
+
const radius = Math.max(10, Math.min(width, height) / 2 - 4);
|
|
32
|
+
const layout = Hierarchy.circlePackLayout(data, cx, cy, radius, 0, null, padding);
|
|
33
|
+
const textAnchor = this.renderer.centerTextAnchor;
|
|
34
|
+
|
|
35
|
+
layout.forEach(entry => {
|
|
36
|
+
const { node, cx: nx, cy: ny, r, depth, isLeaf, topAncestor } = entry;
|
|
37
|
+
const color = this._colorForNode(topAncestor, depth);
|
|
38
|
+
const textColor = this._textColorForNode(topAncestor, depth);
|
|
39
|
+
const valStr = this._formatNodeValue(node);
|
|
40
|
+
const tooltip = `${node.label}: ${valStr}`;
|
|
41
|
+
|
|
42
|
+
if (isLeaf) {
|
|
43
|
+
const circle = this._datum(this.renderer.circle(nx, ny, r, color, { color: '#ffffff', width: 1.5 }, {
|
|
44
|
+
tooltip,
|
|
45
|
+
}), { label: node.label, value: entry.value });
|
|
46
|
+
this._wireHoverTransform([circle], { dx: 0, dy: -2.5 });
|
|
47
|
+
|
|
48
|
+
if (r >= 16) {
|
|
49
|
+
const labelText = node.label.length > Math.floor(r / 3.2)
|
|
50
|
+
? node.label.slice(0, Math.max(3, Math.floor(r / 3.2))) + '…'
|
|
51
|
+
: node.label;
|
|
52
|
+
this.renderer.text(labelText, nx, ny - (showValues && r >= 24 ? 5 : 0), {
|
|
53
|
+
textAnchor, verticalAlign: 'middle',
|
|
54
|
+
fontSize: r >= 28 ? '11px' : '9px', fontWeight: '600',
|
|
55
|
+
fill: textColor, 'pointer-events': 'none',
|
|
56
|
+
});
|
|
57
|
+
if (showValues && r >= 24) {
|
|
58
|
+
this.renderer.text(valStr, nx, ny + 8, {
|
|
59
|
+
textAnchor, verticalAlign: 'middle',
|
|
60
|
+
fontSize: '9px', fill: textColor, opacity: 0.85, 'pointer-events': 'none',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
const branchFill = this._palette().lighten(color, 0.32);
|
|
66
|
+
const circle = this._datum(this.renderer.circle(nx, ny, r, branchFill, { color, width: 1.8 }, {
|
|
67
|
+
tooltip, 'fill-opacity': 0.38,
|
|
68
|
+
}), { label: node.label, value: entry.value });
|
|
69
|
+
this._wireHoverTransform([circle], { dx: 0, dy: -1.5 });
|
|
70
|
+
|
|
71
|
+
if (r >= 26) {
|
|
72
|
+
this.renderer.text(node.label, nx, ny - r + 11, {
|
|
73
|
+
textAnchor, verticalAlign: 'middle',
|
|
74
|
+
fontSize: '10px', fontWeight: 'bold',
|
|
75
|
+
fill: '#333', 'pointer-events': 'none',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|