@amcharts/amcharts5 5.16.2 → 5.17.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/.internal/charts/map/MapChartDefaultTheme.d.ts.map +1 -1
- package/.internal/charts/map/MapChartDefaultTheme.js +3 -0
- package/.internal/charts/map/MapChartDefaultTheme.js.map +1 -1
- package/.internal/charts/map/MapSankeyNodes.d.ts +155 -0
- package/.internal/charts/map/MapSankeyNodes.d.ts.map +1 -0
- package/.internal/charts/map/MapSankeyNodes.js +151 -0
- package/.internal/charts/map/MapSankeyNodes.js.map +1 -0
- package/.internal/charts/map/MapSankeySeries.d.ts +383 -0
- package/.internal/charts/map/MapSankeySeries.d.ts.map +1 -0
- package/.internal/charts/map/MapSankeySeries.js +794 -0
- package/.internal/charts/map/MapSankeySeries.js.map +1 -0
- package/.internal/core/Classes.d.ts +6 -0
- package/.internal/core/Classes.d.ts.map +1 -1
- package/.internal/core/Classes.js.map +1 -1
- package/.internal/core/Registry.js +1 -1
- package/.internal/core/Registry.js.map +1 -1
- package/.internal/core/util/Entity.d.ts.map +1 -1
- package/.internal/core/util/Entity.js +6 -0
- package/.internal/core/util/Entity.js.map +1 -1
- package/.internal/core/util/Math.d.ts +132 -49
- package/.internal/core/util/Math.d.ts.map +1 -1
- package/.internal/core/util/Math.js +151 -50
- package/.internal/core/util/Math.js.map +1 -1
- package/.internal/core/util/Object.d.ts +24 -1
- package/.internal/core/util/Object.d.ts.map +1 -1
- package/.internal/core/util/Object.js +24 -1
- package/.internal/core/util/Object.js.map +1 -1
- package/.internal/core/util/Utils.d.ts +69 -3
- package/.internal/core/util/Utils.d.ts.map +1 -1
- package/.internal/core/util/Utils.js +72 -18
- package/.internal/core/util/Utils.js.map +1 -1
- package/.internal/plugins/json/Classes-script.d.ts +6 -0
- package/.internal/plugins/json/Classes-script.d.ts.map +1 -1
- package/.internal/plugins/json/Classes-script.js +3 -0
- package/.internal/plugins/json/Classes-script.js.map +1 -1
- package/.internal/plugins/json/Classes.d.ts +6 -0
- package/.internal/plugins/json/Classes.d.ts.map +1 -1
- package/.internal/plugins/json/Classes.js +3 -0
- package/.internal/plugins/json/Classes.js.map +1 -1
- package/CHANGELOG.md +17 -8
- package/examples/javascript/flow-arc-horizontal/package.json +1 -1
- package/examples/javascript/flow-arc-vertical/package.json +1 -1
- package/examples/javascript/flow-chord/package.json +1 -1
- package/examples/javascript/flow-chord-directed/package.json +1 -1
- package/examples/javascript/flow-chord-non-ribbon/package.json +1 -1
- package/examples/javascript/flow-sankey/package.json +1 -1
- package/examples/javascript/gantt/package.json +1 -1
- package/examples/javascript/gantt-multilevel/package.json +1 -1
- package/examples/javascript/gantt-simple/package.json +1 -1
- package/examples/javascript/gauge/package.json +1 -1
- package/examples/javascript/gauge-bands/package.json +1 -1
- package/examples/javascript/hierarchy-force-directed/package.json +1 -1
- package/examples/javascript/hierarchy-pack/package.json +1 -1
- package/examples/javascript/hierarchy-partition/package.json +1 -1
- package/examples/javascript/hierarchy-sunburst/package.json +1 -1
- package/examples/javascript/hierarchy-tree/package.json +1 -1
- package/examples/javascript/hierarchy-treemap/package.json +1 -1
- package/examples/javascript/hierarchy-voronoi-treemap/package.json +1 -1
- package/examples/javascript/jest/package.json +1 -1
- package/examples/javascript/json-pie/package.json +1 -1
- package/examples/javascript/json-xy/package.json +1 -1
- package/examples/javascript/map-animating-along-lines/package.json +1 -1
- package/examples/javascript/map-clustered-points/package.json +1 -1
- package/examples/javascript/map-day-and-night/package.json +1 -1
- package/examples/javascript/map-globe-rotate-to-country/package.json +1 -1
- package/examples/javascript/map-globe-with-projected-circles/package.json +1 -1
- package/examples/javascript/map-sankey/README.md +6 -0
- package/examples/javascript/map-sankey/index.css +11 -0
- package/examples/javascript/map-sankey/index.html +13 -0
- package/examples/javascript/map-sankey/index.js +368 -0
- package/examples/javascript/map-sankey/package.json +17 -0
- package/examples/javascript/map-sankey/webpack.config.js +38 -0
- package/examples/javascript/map-sankey-waypoints/README.md +6 -0
- package/examples/javascript/map-sankey-waypoints/index.css +11 -0
- package/examples/javascript/map-sankey-waypoints/index.html +13 -0
- package/examples/javascript/map-sankey-waypoints/index.js +394 -0
- package/examples/javascript/map-sankey-waypoints/package.json +17 -0
- package/examples/javascript/map-sankey-waypoints/webpack.config.js +38 -0
- package/examples/javascript/map-with-bubbles/package.json +1 -1
- package/examples/javascript/map-zoom-to-country/package.json +1 -1
- package/examples/javascript/misc-40-charts/package.json +1 -1
- package/examples/javascript/misc-microchart-grid/package.json +1 -1
- package/examples/javascript/pie-chart/package.json +1 -1
- package/examples/javascript/pie-donut-chart/package.json +1 -1
- package/examples/javascript/pie-variable-radius/package.json +1 -1
- package/examples/javascript/radar-column-iwatch-style/package.json +1 -1
- package/examples/javascript/radar-heat-map/package.json +1 -1
- package/examples/javascript/radar-line/package.json +1 -1
- package/examples/javascript/radar-time-line/package.json +1 -1
- package/examples/javascript/sliced-funnel/package.json +1 -1
- package/examples/javascript/sliced-pictorial-stacked/package.json +1 -1
- package/examples/javascript/sliced-pyramid/package.json +1 -1
- package/examples/javascript/stock-chart/package.json +1 -1
- package/examples/javascript/stock-chart-comparing-stocks/package.json +1 -1
- package/examples/javascript/stock-chart-data-granularity/package.json +1 -1
- package/examples/javascript/stock-chart-data-grouping/package.json +1 -1
- package/examples/javascript/stock-chart-intraday/package.json +1 -1
- package/examples/javascript/stock-chart-live/package.json +1 -1
- package/examples/javascript/stock-chart-volume-separate-panel/package.json +1 -1
- package/examples/javascript/timeline-horizontal-serpentine-chart/package.json +1 -1
- package/examples/javascript/timeline-linear-process-diagram/package.json +1 -1
- package/examples/javascript/timeline-serpentine-chart/package.json +1 -1
- package/examples/javascript/timeline-spiral-chart/package.json +1 -1
- package/examples/javascript/venn-diagram/package.json +1 -1
- package/examples/javascript/wordcloud-with-data/package.json +1 -1
- package/examples/javascript/wordcloud-with-text/package.json +1 -1
- package/examples/javascript/xy-100-percent-stacked-column/package.json +1 -1
- package/examples/javascript/xy-animated-bullet-at-the-end-of-the-series/package.json +1 -1
- package/examples/javascript/xy-bubble/package.json +1 -1
- package/examples/javascript/xy-candlestick/package.json +1 -1
- package/examples/javascript/xy-clustered-column/package.json +1 -1
- package/examples/javascript/xy-column/package.json +1 -1
- package/examples/javascript/xy-comparing-series-google-analytics-style/package.json +1 -1
- package/examples/javascript/xy-data-grouping/package.json +1 -1
- package/examples/javascript/xy-draggable-range/package.json +1 -1
- package/examples/javascript/xy-drawing-series-with-mouse-or-touch/package.json +1 -1
- package/examples/javascript/xy-dumbbell plot/package.json +1 -1
- package/examples/javascript/xy-evenly-spaced-date-axis/package.json +1 -1
- package/examples/javascript/xy-line/package.json +1 -1
- package/examples/javascript/xy-line-highlight-on-legend-hover/package.json +1 -1
- package/examples/javascript/xy-live-data/package.json +1 -1
- package/examples/javascript/xy-multiple-synced-value-axes/package.json +1 -1
- package/examples/javascript/xy-ohlc/package.json +1 -1
- package/examples/javascript/xy-real-time-data-sorting/package.json +1 -1
- package/examples/javascript/xy-smoothed-line/package.json +1 -1
- package/examples/javascript/xy-stacked-and-clustered-column/package.json +1 -1
- package/examples/javascript/xy-stacked-column/package.json +1 -1
- package/examples/javascript/xy-stacked-step/package.json +1 -1
- package/examples/javascript/xy-stock/package.json +1 -1
- package/examples/javascript/xy-stock-comparing/package.json +1 -1
- package/examples/typescript/flow-arc-horizontal/package.json +1 -1
- package/examples/typescript/flow-arc-vertical/package.json +1 -1
- package/examples/typescript/flow-chord/package.json +1 -1
- package/examples/typescript/flow-chord-directed/package.json +1 -1
- package/examples/typescript/flow-chord-non-ribbon/package.json +1 -1
- package/examples/typescript/flow-sankey/package.json +1 -1
- package/examples/typescript/gantt/package.json +1 -1
- package/examples/typescript/gantt-multilevel/package.json +1 -1
- package/examples/typescript/gantt-simple/package.json +1 -1
- package/examples/typescript/gauge/package.json +1 -1
- package/examples/typescript/gauge-bands/package.json +1 -1
- package/examples/typescript/hierarchy-force-directed/package.json +1 -1
- package/examples/typescript/hierarchy-pack/package.json +1 -1
- package/examples/typescript/hierarchy-partition/package.json +1 -1
- package/examples/typescript/hierarchy-sunburst/package.json +1 -1
- package/examples/typescript/hierarchy-tree/package.json +1 -1
- package/examples/typescript/hierarchy-treemap/package.json +1 -1
- package/examples/typescript/hierarchy-voronoi-treemap/package.json +1 -1
- package/examples/typescript/jest/package.json +1 -1
- package/examples/typescript/json-pie/package.json +1 -1
- package/examples/typescript/json-xy/package.json +1 -1
- package/examples/typescript/map-animating-along-lines/package.json +1 -1
- package/examples/typescript/map-clustered-points/package.json +1 -1
- package/examples/typescript/map-day-and-night/package.json +1 -1
- package/examples/typescript/map-globe-rotate-to-country/package.json +1 -1
- package/examples/typescript/map-globe-with-projected-circles/package.json +1 -1
- package/examples/typescript/map-sankey/README.md +6 -0
- package/examples/typescript/map-sankey/index.css +11 -0
- package/examples/typescript/map-sankey/index.html +13 -0
- package/examples/typescript/map-sankey/index.ts +368 -0
- package/examples/typescript/map-sankey/package.json +19 -0
- package/examples/typescript/map-sankey/tsconfig.json +12 -0
- package/examples/typescript/map-sankey/webpack.config.js +45 -0
- package/examples/typescript/map-sankey-waypoints/README.md +6 -0
- package/examples/typescript/map-sankey-waypoints/index.css +11 -0
- package/examples/typescript/map-sankey-waypoints/index.html +13 -0
- package/examples/typescript/map-sankey-waypoints/index.ts +394 -0
- package/examples/typescript/map-sankey-waypoints/package.json +19 -0
- package/examples/typescript/map-sankey-waypoints/tsconfig.json +12 -0
- package/examples/typescript/map-sankey-waypoints/webpack.config.js +45 -0
- package/examples/typescript/map-with-bubbles/package.json +1 -1
- package/examples/typescript/map-zoom-to-country/package.json +1 -1
- package/examples/typescript/misc-40-charts/package.json +1 -1
- package/examples/typescript/misc-microchart-grid/package.json +1 -1
- package/examples/typescript/pie-chart/package.json +1 -1
- package/examples/typescript/pie-donut-chart/package.json +1 -1
- package/examples/typescript/pie-variable-radius/package.json +1 -1
- package/examples/typescript/radar-column-iwatch-style/package.json +1 -1
- package/examples/typescript/radar-heat-map/package.json +1 -1
- package/examples/typescript/radar-line/package.json +1 -1
- package/examples/typescript/radar-time-line/package.json +1 -1
- package/examples/typescript/sliced-funnel/package.json +1 -1
- package/examples/typescript/sliced-pictorial-stacked/package.json +1 -1
- package/examples/typescript/sliced-pyramid/package.json +1 -1
- package/examples/typescript/stock-chart/package.json +1 -1
- package/examples/typescript/stock-chart-comparing-stocks/package.json +1 -1
- package/examples/typescript/stock-chart-data-granularity/package.json +1 -1
- package/examples/typescript/stock-chart-data-grouping/package.json +1 -1
- package/examples/typescript/stock-chart-intraday/package.json +1 -1
- package/examples/typescript/stock-chart-live/package.json +1 -1
- package/examples/typescript/stock-chart-volume-separate-panel/package.json +1 -1
- package/examples/typescript/timeline-horizontal-serpentine-chart/package.json +1 -1
- package/examples/typescript/timeline-linear-process-diagram/package.json +1 -1
- package/examples/typescript/timeline-serpentine-chart/package.json +1 -1
- package/examples/typescript/timeline-spiral-chart/package.json +1 -1
- package/examples/typescript/venn-diagram/package.json +1 -1
- package/examples/typescript/wordcloud-with-data/package.json +1 -1
- package/examples/typescript/wordcloud-with-text/package.json +1 -1
- package/examples/typescript/xy-100-percent-stacked-column/package.json +1 -1
- package/examples/typescript/xy-animated-bullet-at-the-end-of-the-series/package.json +1 -1
- package/examples/typescript/xy-bubble/package.json +1 -1
- package/examples/typescript/xy-candlestick/package.json +1 -1
- package/examples/typescript/xy-clustered-column/package.json +1 -1
- package/examples/typescript/xy-column/package.json +1 -1
- package/examples/typescript/xy-comparing-series-google-analytics-style/package.json +1 -1
- package/examples/typescript/xy-data-grouping/package.json +1 -1
- package/examples/typescript/xy-draggable-range/package.json +1 -1
- package/examples/typescript/xy-drawing-series-with-mouse-or-touch/package.json +1 -1
- package/examples/typescript/xy-dumbbell plot/package.json +1 -1
- package/examples/typescript/xy-evenly-spaced-date-axis/package.json +1 -1
- package/examples/typescript/xy-line/package.json +1 -1
- package/examples/typescript/xy-line-highlight-on-legend-hover/package.json +1 -1
- package/examples/typescript/xy-live-data/package.json +1 -1
- package/examples/typescript/xy-multiple-synced-value-axes/package.json +1 -1
- package/examples/typescript/xy-ohlc/package.json +1 -1
- package/examples/typescript/xy-real-time-data-sorting/package.json +1 -1
- package/examples/typescript/xy-smoothed-line/package.json +1 -1
- package/examples/typescript/xy-stacked-and-clustered-column/package.json +1 -1
- package/examples/typescript/xy-stacked-column/package.json +1 -1
- package/examples/typescript/xy-stacked-step/package.json +1 -1
- package/examples/typescript/xy-stock/package.json +1 -1
- package/examples/typescript/xy-stock-comparing/package.json +1 -1
- package/map.d.ts +2 -0
- package/map.d.ts.map +1 -1
- package/map.js +2 -0
- package/map.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
import { MapPolygonSeries } from "./MapPolygonSeries";
|
|
2
|
+
import { MapSankeyNodes } from "./MapSankeyNodes";
|
|
3
|
+
import { getGeoCentroid, getGeoCircle } from "./MapUtils";
|
|
4
|
+
import { geoArea } from "d3-geo";
|
|
5
|
+
import * as $array from "../../core/util/Array";
|
|
6
|
+
import * as $math from "../../core/util/Math";
|
|
7
|
+
/**
|
|
8
|
+
* Creates a map series for displaying Sankey-style flow bands on a map.
|
|
9
|
+
*
|
|
10
|
+
* Generates actual GeoJSON polygon geometries for each flow, so they
|
|
11
|
+
* properly follow the map projection during panning, zooming, and rotation.
|
|
12
|
+
*
|
|
13
|
+
* @see {@link https://www.amcharts.com/docs/v5/charts/map-chart/map-sankey-series/} for more info
|
|
14
|
+
* @since 5.17.0
|
|
15
|
+
* @important
|
|
16
|
+
*/
|
|
17
|
+
export class MapSankeySeries extends MapPolygonSeries {
|
|
18
|
+
constructor() {
|
|
19
|
+
super(...arguments);
|
|
20
|
+
/**
|
|
21
|
+
* Don't consume polygon features from geodata - our data comes from user's data array.
|
|
22
|
+
*/
|
|
23
|
+
this._types = [];
|
|
24
|
+
/**
|
|
25
|
+
* A sub-series that manages node data items and visuals.
|
|
26
|
+
*
|
|
27
|
+
* Nodes are auto-created from link data. You can also set
|
|
28
|
+
* `nodes.data.setAll([...])` with custom names and fills.
|
|
29
|
+
*
|
|
30
|
+
* `nodes.mapPolygons.template` configures node appearance.
|
|
31
|
+
*/
|
|
32
|
+
this.nodes = this.addDisposer(MapSankeyNodes.new(this._root, {}));
|
|
33
|
+
/**
|
|
34
|
+
* Stored bezier segment params per data item for bullet positioning.
|
|
35
|
+
* Each link can have multiple segments (when waypoints are used).
|
|
36
|
+
* @ignore
|
|
37
|
+
*/
|
|
38
|
+
this._bezierSegments = new Map();
|
|
39
|
+
/**
|
|
40
|
+
* Cumulative arc-length array per data item for uniform-speed bullet positioning.
|
|
41
|
+
* Entry i = cumulative length from segment 0 through segment i.
|
|
42
|
+
* Last entry = total path length.
|
|
43
|
+
* @ignore
|
|
44
|
+
*/
|
|
45
|
+
this._segmentCumulativeLengths = new Map();
|
|
46
|
+
// Reusable scratch Maps for _generateGeometries — cleared and reused each call
|
|
47
|
+
// to avoid allocating 10 fresh Maps per dirty cycle.
|
|
48
|
+
this._geoSourceCoords = new Map();
|
|
49
|
+
this._geoTargetCoords = new Map();
|
|
50
|
+
this._geoWidths = new Map();
|
|
51
|
+
this._geoSourceGroups = new Map();
|
|
52
|
+
this._geoTargetGroups = new Map();
|
|
53
|
+
this._geoNodeSourceTotal = new Map();
|
|
54
|
+
this._geoNodeTargetTotal = new Map();
|
|
55
|
+
this._geoNodeRange = new Map();
|
|
56
|
+
this._geoSourceOffsets = new Map();
|
|
57
|
+
this._geoTargetOffsets = new Map();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* @ignore
|
|
61
|
+
*/
|
|
62
|
+
_afterNew() {
|
|
63
|
+
this.fields.push("sourceId", "targetId", "sourceLongitude", "sourceLatitude", "targetLongitude", "targetLatitude", "waypoints");
|
|
64
|
+
this.valueFields.push("value");
|
|
65
|
+
this._setRawDefault("sourceIdField", "sourceId");
|
|
66
|
+
this._setRawDefault("targetIdField", "targetId");
|
|
67
|
+
this._setRawDefault("sourceLongitudeField", "sourceLongitude");
|
|
68
|
+
this._setRawDefault("sourceLatitudeField", "sourceLatitude");
|
|
69
|
+
this._setRawDefault("targetLongitudeField", "targetLongitude");
|
|
70
|
+
this._setRawDefault("targetLatitudeField", "targetLatitude");
|
|
71
|
+
this._setRawDefault("valueField", "value");
|
|
72
|
+
this._setRawDefault("waypointsField", "waypoints");
|
|
73
|
+
this.nodes.flow = this;
|
|
74
|
+
super._afterNew();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* @ignore
|
|
78
|
+
*/
|
|
79
|
+
markDirtyProjection() {
|
|
80
|
+
super.markDirtyProjection();
|
|
81
|
+
$array.each(this.nodes.dataItems, (nodeDataItem) => {
|
|
82
|
+
const mapPolygon = nodeDataItem.get("mapPolygon");
|
|
83
|
+
if (mapPolygon) {
|
|
84
|
+
mapPolygon.markDirtyProjection();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Evaluate a single bezier segment at parameter t.
|
|
90
|
+
*
|
|
91
|
+
* Same formula as $math.getPointOnCubicCurve but operates on flat
|
|
92
|
+
* IBezierSegment fields (lon/lat) to avoid IPoint object allocation.
|
|
93
|
+
*/
|
|
94
|
+
_evalBezier(bp, t) {
|
|
95
|
+
const s = 1 - t;
|
|
96
|
+
return {
|
|
97
|
+
longitude: s * s * s * bp.p0Lon + 3 * s * s * t * bp.cp0Lon + 3 * s * t * t * bp.cp1Lon + t * t * t * bp.p1Lon,
|
|
98
|
+
latitude: s * s * s * bp.p0Lat + 3 * s * s * t * bp.cp0Lat + 3 * s * t * t * bp.cp1Lat + t * t * t * bp.p1Lat
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Approximates the arc length of a single bezier segment by sampling
|
|
103
|
+
* points along the curve and summing Haversine distances.
|
|
104
|
+
*
|
|
105
|
+
* @param bp Bezier segment parameters
|
|
106
|
+
* @param samples Number of sample intervals (default 20)
|
|
107
|
+
* @return Approximate arc length in radians
|
|
108
|
+
*/
|
|
109
|
+
_segmentArcLength(bp, samples = 20) {
|
|
110
|
+
let length = 0;
|
|
111
|
+
let prev = this._evalBezier(bp, 0);
|
|
112
|
+
for (let i = 1; i <= samples; i++) {
|
|
113
|
+
const pt = this._evalBezier(bp, i / samples);
|
|
114
|
+
const dLon = (pt.longitude - prev.longitude) * Math.PI / 180;
|
|
115
|
+
const dLat = (pt.latitude - prev.latitude) * Math.PI / 180;
|
|
116
|
+
// Haversine approximation
|
|
117
|
+
const a = Math.pow(Math.sin(dLat / 2), 2) + Math.cos(prev.latitude * Math.PI / 180) * Math.cos(pt.latitude * Math.PI / 180) * Math.pow(Math.sin(dLon / 2), 2);
|
|
118
|
+
length += 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
119
|
+
prev = pt;
|
|
120
|
+
}
|
|
121
|
+
return length;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Computes cumulative arc lengths for a set of bezier segments and
|
|
125
|
+
* stores them in `_segmentCumulativeLengths` for arc-length parameterization.
|
|
126
|
+
*
|
|
127
|
+
* @param dataItem Link data item (used as cache key)
|
|
128
|
+
* @param segments Array of bezier segment parameters
|
|
129
|
+
*/
|
|
130
|
+
_computeCumulativeLengths(dataItem, segments) {
|
|
131
|
+
const cumulative = [];
|
|
132
|
+
let total = 0;
|
|
133
|
+
for (let i = 0; i < segments.length; i++) {
|
|
134
|
+
total += this._segmentArcLength(segments[i]);
|
|
135
|
+
cumulative.push(total);
|
|
136
|
+
}
|
|
137
|
+
this._segmentCumulativeLengths.set(dataItem, cumulative);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Resolves a normalized location (0–1) to a segment index and local t
|
|
141
|
+
* using arc-length parameterization. Delegates to `$math.resolveLocationOnPath`
|
|
142
|
+
* when cumulative lengths are available; falls back to equal distribution.
|
|
143
|
+
*
|
|
144
|
+
* @param dataItem Link data item (for cumulative length lookup)
|
|
145
|
+
* @param location Relative position along the full path (0–1)
|
|
146
|
+
* @param segments Array of bezier segment parameters (used for fallback)
|
|
147
|
+
* @return Segment index and local t (0–1)
|
|
148
|
+
*/
|
|
149
|
+
_resolveLocation(dataItem, location, segments) {
|
|
150
|
+
const cumLengths = this._segmentCumulativeLengths.get(dataItem);
|
|
151
|
+
if (cumLengths && cumLengths.length > 0) {
|
|
152
|
+
const result = $math.resolveLocationOnPath(location, cumLengths);
|
|
153
|
+
return { segIdx: result.index, t: result.t };
|
|
154
|
+
}
|
|
155
|
+
// Fallback: equal distribution
|
|
156
|
+
const n = segments.length;
|
|
157
|
+
const scaled = location * n;
|
|
158
|
+
const segIdx = Math.min(Math.floor(scaled), n - 1);
|
|
159
|
+
return { segIdx, t: scaled - segIdx };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Returns a screen-space point at a relative position (0-1) along
|
|
163
|
+
* the center-line bezier for the given data item.
|
|
164
|
+
*
|
|
165
|
+
* Used internally for bullet positioning, but can also be called
|
|
166
|
+
* directly to get coordinates along a flow path.
|
|
167
|
+
*
|
|
168
|
+
* @param dataItem Target data item
|
|
169
|
+
* @param location Relative position (0 = source, 1 = target)
|
|
170
|
+
* @return Screen coordinates and angle
|
|
171
|
+
*/
|
|
172
|
+
getPoint(dataItem, location) {
|
|
173
|
+
const segments = this._bezierSegments.get(dataItem);
|
|
174
|
+
const chart = this.chart;
|
|
175
|
+
if (!segments || segments.length === 0 || !chart)
|
|
176
|
+
return { x: 0, y: 0, angle: 0 };
|
|
177
|
+
const { segIdx, t } = this._resolveLocation(dataItem, location, segments);
|
|
178
|
+
const bp = segments[segIdx];
|
|
179
|
+
const pt = this._evalBezier(bp, t);
|
|
180
|
+
const screenPoint = chart.convert({ longitude: pt.longitude, latitude: pt.latitude });
|
|
181
|
+
// Angle from a nearby point — use backward difference near t=1 to avoid identical points
|
|
182
|
+
const dt = 0.001;
|
|
183
|
+
let pt2;
|
|
184
|
+
let sign = 1;
|
|
185
|
+
if (t + dt <= 1) {
|
|
186
|
+
pt2 = this._evalBezier(bp, t + dt);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
pt2 = this._evalBezier(bp, t - dt);
|
|
190
|
+
sign = -1;
|
|
191
|
+
}
|
|
192
|
+
const screenPoint2 = chart.convert({ longitude: pt2.longitude, latitude: pt2.latitude });
|
|
193
|
+
const angle = Math.atan2(sign * (screenPoint2.y - screenPoint.y), sign * (screenPoint2.x - screenPoint.x)) * 180 / Math.PI;
|
|
194
|
+
return { x: screenPoint.x, y: screenPoint.y, angle };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Returns a geo point at a relative position (0-1) along the center-line bezier.
|
|
198
|
+
*/
|
|
199
|
+
getGeoPoint(dataItem, location) {
|
|
200
|
+
const segments = this._bezierSegments.get(dataItem);
|
|
201
|
+
if (!segments || segments.length === 0)
|
|
202
|
+
return undefined;
|
|
203
|
+
const { segIdx, t } = this._resolveLocation(dataItem, location, segments);
|
|
204
|
+
return this._evalBezier(segments[segIdx], t);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Returns the approximate arc length of the path for a data item.
|
|
208
|
+
* Useful for scaling animation duration proportionally to distance.
|
|
209
|
+
*/
|
|
210
|
+
getPathLength(dataItem) {
|
|
211
|
+
const cumLengths = this._segmentCumulativeLengths.get(dataItem);
|
|
212
|
+
if (cumLengths && cumLengths.length > 0) {
|
|
213
|
+
return cumLengths[cumLengths.length - 1];
|
|
214
|
+
}
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Combined getPoint + getGeoPoint in a single pass (avoids double
|
|
219
|
+
* _resolveLocation + _evalBezier per bullet per frame).
|
|
220
|
+
* @ignore
|
|
221
|
+
*/
|
|
222
|
+
_getPointWithGeo(dataItem, location) {
|
|
223
|
+
const segments = this._bezierSegments.get(dataItem);
|
|
224
|
+
const chart = this.chart;
|
|
225
|
+
if (!segments || segments.length === 0 || !chart)
|
|
226
|
+
return undefined;
|
|
227
|
+
const { segIdx, t } = this._resolveLocation(dataItem, location, segments);
|
|
228
|
+
const bp = segments[segIdx];
|
|
229
|
+
const geoPoint = this._evalBezier(bp, t);
|
|
230
|
+
const screenPoint = chart.convert({ longitude: geoPoint.longitude, latitude: geoPoint.latitude });
|
|
231
|
+
const dt = 0.001;
|
|
232
|
+
let pt2;
|
|
233
|
+
let sign = 1;
|
|
234
|
+
if (t + dt <= 1) {
|
|
235
|
+
pt2 = this._evalBezier(bp, t + dt);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
pt2 = this._evalBezier(bp, t - dt);
|
|
239
|
+
sign = -1;
|
|
240
|
+
}
|
|
241
|
+
const screenPoint2 = chart.convert({ longitude: pt2.longitude, latitude: pt2.latitude });
|
|
242
|
+
const angle = Math.atan2(sign * (screenPoint2.y - screenPoint.y), sign * (screenPoint2.x - screenPoint.x)) * 180 / Math.PI;
|
|
243
|
+
return { x: screenPoint.x, y: screenPoint.y, angle, geoPoint };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Positions a bullet along its parent link's bezier path.
|
|
247
|
+
*
|
|
248
|
+
* @param bullet Bullet to position
|
|
249
|
+
*/
|
|
250
|
+
_positionBullet(bullet) {
|
|
251
|
+
const sprite = bullet.get("sprite");
|
|
252
|
+
if (sprite) {
|
|
253
|
+
const dataItem = sprite.dataItem;
|
|
254
|
+
if (dataItem) {
|
|
255
|
+
const location = bullet.get("locationX", 0.5);
|
|
256
|
+
const result = this._getPointWithGeo(dataItem, location);
|
|
257
|
+
if (!result)
|
|
258
|
+
return;
|
|
259
|
+
sprite.setAll({ x: result.x, y: result.y });
|
|
260
|
+
if (bullet.get("autoRotate")) {
|
|
261
|
+
sprite.set("rotation", result.angle + bullet.get("autoRotateAngle", 0));
|
|
262
|
+
}
|
|
263
|
+
// Hide bullets on the back side of the globe (like MapPointSeries clipBack)
|
|
264
|
+
const chart = this.chart;
|
|
265
|
+
if (chart) {
|
|
266
|
+
const geoPath = chart.getPrivate("geoPath");
|
|
267
|
+
if (geoPath) {
|
|
268
|
+
const geometry = { type: "Point", coordinates: [result.geoPoint.longitude, result.geoPoint.latitude] };
|
|
269
|
+
sprite.setPrivate("visible", !!geoPath(geometry));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* @ignore
|
|
277
|
+
*/
|
|
278
|
+
disposeDataItem(dataItem) {
|
|
279
|
+
super.disposeDataItem(dataItem);
|
|
280
|
+
this._bezierSegments.delete(dataItem);
|
|
281
|
+
this._segmentCumulativeLengths.delete(dataItem);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* @ignore
|
|
285
|
+
*/
|
|
286
|
+
_dispose() {
|
|
287
|
+
super._dispose();
|
|
288
|
+
this._bezierSegments.clear();
|
|
289
|
+
this._segmentCumulativeLengths.clear();
|
|
290
|
+
this._geoSourceCoords.clear();
|
|
291
|
+
this._geoTargetCoords.clear();
|
|
292
|
+
this._geoWidths.clear();
|
|
293
|
+
this._geoSourceGroups.clear();
|
|
294
|
+
this._geoTargetGroups.clear();
|
|
295
|
+
this._geoNodeSourceTotal.clear();
|
|
296
|
+
this._geoNodeTargetTotal.clear();
|
|
297
|
+
this._geoNodeRange.clear();
|
|
298
|
+
this._geoSourceOffsets.clear();
|
|
299
|
+
this._geoTargetOffsets.clear();
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Processes a newly added data item, creating placeholder geometry and
|
|
303
|
+
* registering it with source/target nodes.
|
|
304
|
+
*
|
|
305
|
+
* @param dataItem Data item to process
|
|
306
|
+
*/
|
|
307
|
+
processDataItem(dataItem) {
|
|
308
|
+
// Set placeholder geometry so parent creates the MapPolygon
|
|
309
|
+
// Use empty coordinates array (no rings) to avoid d3-geo crash on empty ring
|
|
310
|
+
if (!dataItem.get("geometry")) {
|
|
311
|
+
dataItem.set("geometry", { type: "Polygon", coordinates: [] });
|
|
312
|
+
}
|
|
313
|
+
super.processDataItem(dataItem);
|
|
314
|
+
const polygonSeries = this.get("polygonSeries");
|
|
315
|
+
// Resolve source coordinates
|
|
316
|
+
let sourceLon = dataItem.get("sourceLongitude");
|
|
317
|
+
let sourceLat = dataItem.get("sourceLatitude");
|
|
318
|
+
const sourceId = dataItem.get("sourceId");
|
|
319
|
+
if (sourceId && polygonSeries) {
|
|
320
|
+
const polygonDI = polygonSeries.getDataItemById(sourceId);
|
|
321
|
+
if (polygonDI) {
|
|
322
|
+
const geometry = polygonDI.get("geometry");
|
|
323
|
+
if (geometry) {
|
|
324
|
+
const centroid = getGeoCentroid(geometry);
|
|
325
|
+
sourceLon = centroid.longitude;
|
|
326
|
+
sourceLat = centroid.latitude;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Resolve target coordinates
|
|
331
|
+
let targetLon = dataItem.get("targetLongitude");
|
|
332
|
+
let targetLat = dataItem.get("targetLatitude");
|
|
333
|
+
const targetId = dataItem.get("targetId");
|
|
334
|
+
if (targetId && polygonSeries) {
|
|
335
|
+
const polygonDI = polygonSeries.getDataItemById(targetId);
|
|
336
|
+
if (polygonDI) {
|
|
337
|
+
const geometry = polygonDI.get("geometry");
|
|
338
|
+
if (geometry) {
|
|
339
|
+
const centroid = getGeoCentroid(geometry);
|
|
340
|
+
targetLon = centroid.longitude;
|
|
341
|
+
targetLat = centroid.latitude;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Create/find source node
|
|
346
|
+
if (sourceLon != null && sourceLat != null) {
|
|
347
|
+
const sourceKey = sourceId !== null && sourceId !== void 0 ? sourceId : (Number(sourceLon) + "," + Number(sourceLat));
|
|
348
|
+
let sourceNode = this.nodes.getDataItemById(sourceKey);
|
|
349
|
+
if (!sourceNode) {
|
|
350
|
+
const ctx = dataItem.dataContext;
|
|
351
|
+
const name = (ctx && ctx.source) || sourceKey;
|
|
352
|
+
this.nodes.data.push({ id: sourceKey, name, longitude: Number(sourceLon), latitude: Number(sourceLat) });
|
|
353
|
+
sourceNode = this.nodes.dataItems[this.nodes.dataItems.length - 1];
|
|
354
|
+
}
|
|
355
|
+
if (sourceNode) {
|
|
356
|
+
dataItem.setRaw("sourceNode", sourceNode);
|
|
357
|
+
this.nodes.addOutgoingLink(sourceNode, dataItem);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Create/find target node
|
|
361
|
+
if (targetLon != null && targetLat != null) {
|
|
362
|
+
const targetKey = targetId !== null && targetId !== void 0 ? targetId : (Number(targetLon) + "," + Number(targetLat));
|
|
363
|
+
let targetNode = this.nodes.getDataItemById(targetKey);
|
|
364
|
+
if (!targetNode) {
|
|
365
|
+
const ctx = dataItem.dataContext;
|
|
366
|
+
const name = (ctx && ctx.target) || targetKey;
|
|
367
|
+
this.nodes.data.push({ id: targetKey, name, longitude: Number(targetLon), latitude: Number(targetLat) });
|
|
368
|
+
targetNode = this.nodes.dataItems[this.nodes.dataItems.length - 1];
|
|
369
|
+
}
|
|
370
|
+
if (targetNode) {
|
|
371
|
+
dataItem.setRaw("targetNode", targetNode);
|
|
372
|
+
this.nodes.addIncomingLink(targetNode, dataItem);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* @ignore
|
|
378
|
+
*/
|
|
379
|
+
_onDataClear() {
|
|
380
|
+
// Clear link references on preserved nodes
|
|
381
|
+
$array.each(this.nodes.dataItems, (nodeDataItem) => {
|
|
382
|
+
nodeDataItem.setRaw("incomingLinks", []);
|
|
383
|
+
nodeDataItem.setRaw("outgoingLinks", []);
|
|
384
|
+
});
|
|
385
|
+
if (!this.nodes._userDataSet) {
|
|
386
|
+
// setAll([]) triggers _onDataClear which sets _userDataSet = true;
|
|
387
|
+
// reset it so auto-created nodes work again on next data cycle.
|
|
388
|
+
this.nodes.data.setAll([]);
|
|
389
|
+
this.nodes._userDataSet = false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* @ignore
|
|
394
|
+
*/
|
|
395
|
+
_prepareChildren() {
|
|
396
|
+
if (this._valuesDirty || this._sizeDirty) {
|
|
397
|
+
this._generateGeometries();
|
|
398
|
+
}
|
|
399
|
+
super._prepareChildren();
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Rebuilds all link band geometries from current data.
|
|
403
|
+
*
|
|
404
|
+
* Computes bezier control points, stacking offsets, band polygons,
|
|
405
|
+
* and cumulative arc lengths for each link.
|
|
406
|
+
*/
|
|
407
|
+
_generateGeometries() {
|
|
408
|
+
const maxWidthDeg = this.get("maxWidth", 5);
|
|
409
|
+
const cpdBase = Math.min(0.4999, this.get("controlPointDistance", 0.5));
|
|
410
|
+
const cpdSource = Math.min(0.4999, this.get("controlPointDistanceSource", cpdBase));
|
|
411
|
+
const cpdTarget = Math.min(0.4999, this.get("controlPointDistanceTarget", cpdBase));
|
|
412
|
+
const resolution = this.get("resolution", 50);
|
|
413
|
+
const linkColorMode = this.get("linkColorMode", "solid");
|
|
414
|
+
// Find max value
|
|
415
|
+
let maxValue = 0;
|
|
416
|
+
$array.each(this.dataItems, (dataItem) => {
|
|
417
|
+
const value = dataItem.get("value");
|
|
418
|
+
if (value != null && value > maxValue) {
|
|
419
|
+
maxValue = value;
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
if (maxValue === 0)
|
|
423
|
+
maxValue = 1;
|
|
424
|
+
const sourceCoords = this._geoSourceCoords;
|
|
425
|
+
sourceCoords.clear();
|
|
426
|
+
const targetCoords = this._geoTargetCoords;
|
|
427
|
+
targetCoords.clear();
|
|
428
|
+
const widths = this._geoWidths;
|
|
429
|
+
widths.clear();
|
|
430
|
+
const sourceGroups = this._geoSourceGroups;
|
|
431
|
+
sourceGroups.clear();
|
|
432
|
+
const targetGroups = this._geoTargetGroups;
|
|
433
|
+
targetGroups.clear();
|
|
434
|
+
// Build coord maps and groups from node references on links
|
|
435
|
+
$array.each(this.dataItems, (dataItem) => {
|
|
436
|
+
const value = +(dataItem.get("value") || 0);
|
|
437
|
+
widths.set(dataItem, maxWidthDeg * value / maxValue);
|
|
438
|
+
const sourceNode = dataItem.get("sourceNode");
|
|
439
|
+
if (sourceNode) {
|
|
440
|
+
const lon = sourceNode.get("longitude");
|
|
441
|
+
const lat = sourceNode.get("latitude");
|
|
442
|
+
if (lon != null && lat != null) {
|
|
443
|
+
sourceCoords.set(dataItem, { longitude: lon, latitude: lat });
|
|
444
|
+
const key = sourceNode.get("id");
|
|
445
|
+
if (!sourceGroups.has(key))
|
|
446
|
+
sourceGroups.set(key, []);
|
|
447
|
+
sourceGroups.get(key).push(dataItem);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const targetNode = dataItem.get("targetNode");
|
|
451
|
+
if (targetNode) {
|
|
452
|
+
const lon = targetNode.get("longitude");
|
|
453
|
+
const lat = targetNode.get("latitude");
|
|
454
|
+
if (lon != null && lat != null) {
|
|
455
|
+
targetCoords.set(dataItem, { longitude: lon, latitude: lat });
|
|
456
|
+
const key = targetNode.get("id");
|
|
457
|
+
if (!targetGroups.has(key))
|
|
458
|
+
targetGroups.set(key, []);
|
|
459
|
+
targetGroups.get(key).push(dataItem);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
// Coordinated stacking at intermediate nodes:
|
|
464
|
+
// At nodes with both incoming and outgoing flows, use the same
|
|
465
|
+
// vertical range for both sides so bands align like in a Sankey.
|
|
466
|
+
const nodePadding = this.get("nodePadding", 0.3);
|
|
467
|
+
const autoSort = this.get("autoSort", true);
|
|
468
|
+
// First: compute the unified stacking range at each node
|
|
469
|
+
const nodeSourceTotal = this._geoNodeSourceTotal;
|
|
470
|
+
nodeSourceTotal.clear();
|
|
471
|
+
const nodeTargetTotal = this._geoNodeTargetTotal;
|
|
472
|
+
nodeTargetTotal.clear();
|
|
473
|
+
sourceGroups.forEach((items, key) => {
|
|
474
|
+
let total = 0;
|
|
475
|
+
$array.each(items, (item) => { total += widths.get(item) || 0; });
|
|
476
|
+
nodeSourceTotal.set(key, total);
|
|
477
|
+
});
|
|
478
|
+
targetGroups.forEach((items, key) => {
|
|
479
|
+
let total = 0;
|
|
480
|
+
$array.each(items, (item) => { total += widths.get(item) || 0; });
|
|
481
|
+
nodeTargetTotal.set(key, total);
|
|
482
|
+
});
|
|
483
|
+
// Unified range = max(incoming, outgoing) at each node
|
|
484
|
+
const nodeRange = this._geoNodeRange;
|
|
485
|
+
nodeRange.clear();
|
|
486
|
+
const allKeys = new Set([...nodeSourceTotal.keys(), ...nodeTargetTotal.keys()]);
|
|
487
|
+
allKeys.forEach((key) => {
|
|
488
|
+
nodeRange.set(key, Math.max(nodeSourceTotal.get(key) || 0, nodeTargetTotal.get(key) || 0));
|
|
489
|
+
});
|
|
490
|
+
// Stack outgoing flows (sourceGroups) centered within the unified range
|
|
491
|
+
const sourceOffsets = this._geoSourceOffsets;
|
|
492
|
+
sourceOffsets.clear();
|
|
493
|
+
const targetOffsets = this._geoTargetOffsets;
|
|
494
|
+
targetOffsets.clear();
|
|
495
|
+
sourceGroups.forEach((items, key) => {
|
|
496
|
+
if (autoSort) {
|
|
497
|
+
items.sort((a, b) => { var _a, _b; return ((((_a = targetCoords.get(a)) === null || _a === void 0 ? void 0 : _a.latitude) || 0) - (((_b = targetCoords.get(b)) === null || _b === void 0 ? void 0 : _b.latitude) || 0)); });
|
|
498
|
+
}
|
|
499
|
+
const ownTotal = nodeSourceTotal.get(key) || 0;
|
|
500
|
+
const range = nodeRange.get(key) || ownTotal;
|
|
501
|
+
// Center this side's bands within the unified range
|
|
502
|
+
let offset = -range / 2 + (range - ownTotal) / 2;
|
|
503
|
+
$array.each(items, (item) => {
|
|
504
|
+
const w = widths.get(item) || 0;
|
|
505
|
+
sourceOffsets.set(item, offset + w / 2);
|
|
506
|
+
offset += w;
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
// Stack incoming flows (targetGroups) centered within the unified range
|
|
510
|
+
targetGroups.forEach((items, key) => {
|
|
511
|
+
if (autoSort) {
|
|
512
|
+
items.sort((a, b) => { var _a, _b; return ((((_a = sourceCoords.get(a)) === null || _a === void 0 ? void 0 : _a.latitude) || 0) - (((_b = sourceCoords.get(b)) === null || _b === void 0 ? void 0 : _b.latitude) || 0)); });
|
|
513
|
+
}
|
|
514
|
+
const ownTotal = nodeTargetTotal.get(key) || 0;
|
|
515
|
+
const range = nodeRange.get(key) || ownTotal;
|
|
516
|
+
let offset = -range / 2 + (range - ownTotal) / 2;
|
|
517
|
+
$array.each(items, (item) => {
|
|
518
|
+
const w = widths.get(item) || 0;
|
|
519
|
+
targetOffsets.set(item, offset + w / 2);
|
|
520
|
+
offset += w;
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
// Generate polygon for each link
|
|
524
|
+
const orientationSetting = this.get("orientation", "horizontal");
|
|
525
|
+
$array.each(this.dataItems, (dataItem) => {
|
|
526
|
+
var _a, _b, _c, _d, _e;
|
|
527
|
+
const source = sourceCoords.get(dataItem);
|
|
528
|
+
const target = targetCoords.get(dataItem);
|
|
529
|
+
if (!source || !target)
|
|
530
|
+
return;
|
|
531
|
+
const width = widths.get(dataItem) || 0;
|
|
532
|
+
if (width <= 0)
|
|
533
|
+
return;
|
|
534
|
+
const halfWidth = width / 2;
|
|
535
|
+
const sourceOff = sourceOffsets.get(dataItem) || 0;
|
|
536
|
+
const targetOff = targetOffsets.get(dataItem) || 0;
|
|
537
|
+
// Build path points: [source, ...waypoints, target]
|
|
538
|
+
// Apply stacking offset to source/target based on orientation
|
|
539
|
+
let startLon, startLat, endLon, endLat;
|
|
540
|
+
if (orientationSetting === "vertical") {
|
|
541
|
+
startLon = source.longitude + sourceOff;
|
|
542
|
+
startLat = source.latitude;
|
|
543
|
+
endLon = target.longitude + targetOff;
|
|
544
|
+
endLat = target.latitude;
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
startLon = source.longitude;
|
|
548
|
+
startLat = source.latitude + sourceOff;
|
|
549
|
+
endLon = target.longitude;
|
|
550
|
+
endLat = target.latitude + targetOff;
|
|
551
|
+
}
|
|
552
|
+
// Per-link control point distances (fall back to series-level)
|
|
553
|
+
const linkCpd = Math.min(0.4999, (_a = dataItem.get("controlPointDistance")) !== null && _a !== void 0 ? _a : cpdBase);
|
|
554
|
+
const linkCpdSource = Math.min(0.4999, (_c = (_b = dataItem.get("controlPointDistanceSource")) !== null && _b !== void 0 ? _b : dataItem.get("controlPointDistance")) !== null && _c !== void 0 ? _c : cpdSource);
|
|
555
|
+
const linkCpdTarget = Math.min(0.4999, (_e = (_d = dataItem.get("controlPointDistanceTarget")) !== null && _d !== void 0 ? _d : dataItem.get("controlPointDistance")) !== null && _e !== void 0 ? _e : cpdTarget);
|
|
556
|
+
const waypoints = dataItem.get("waypoints");
|
|
557
|
+
const hasWaypoints = waypoints && waypoints.length > 0;
|
|
558
|
+
// Adjust for antimeridian crossing (only without waypoints)
|
|
559
|
+
if (!hasWaypoints) {
|
|
560
|
+
const antimeridian = this.get("antimeridian", "short");
|
|
561
|
+
if (antimeridian === "short") {
|
|
562
|
+
let dLon = endLon - startLon;
|
|
563
|
+
if (dLon > 180)
|
|
564
|
+
endLon -= 360;
|
|
565
|
+
else if (dLon < -180)
|
|
566
|
+
endLon += 360;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const pathPoints = [{ longitude: startLon, latitude: startLat }];
|
|
570
|
+
if (hasWaypoints) {
|
|
571
|
+
for (const wp of waypoints) {
|
|
572
|
+
pathPoints.push({ longitude: wp.longitude, latitude: wp.latitude });
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
pathPoints.push({ longitude: endLon, latitude: endLat });
|
|
576
|
+
// Build bezier segments between consecutive path points
|
|
577
|
+
const segments = [];
|
|
578
|
+
for (let seg = 0; seg < pathPoints.length - 1; seg++) {
|
|
579
|
+
const p0 = pathPoints[seg];
|
|
580
|
+
const p1 = pathPoints[seg + 1];
|
|
581
|
+
// Tangent direction at each point:
|
|
582
|
+
// - First point: direction toward next point
|
|
583
|
+
// - Last point: direction from previous point
|
|
584
|
+
// - Middle points: average of incoming and outgoing directions
|
|
585
|
+
let tangent0Lon, tangent0Lat;
|
|
586
|
+
let tangent1Lon, tangent1Lat;
|
|
587
|
+
if (seg === 0 && pathPoints.length === 2) {
|
|
588
|
+
// Simple case: single segment, use S-curve logic
|
|
589
|
+
if (orientationSetting === "vertical") {
|
|
590
|
+
const dLat = p1.latitude - p0.latitude;
|
|
591
|
+
tangent0Lon = 0;
|
|
592
|
+
tangent0Lat = dLat;
|
|
593
|
+
tangent1Lon = 0;
|
|
594
|
+
tangent1Lat = dLat;
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
const dLon = p1.longitude - p0.longitude;
|
|
598
|
+
tangent0Lon = dLon;
|
|
599
|
+
tangent0Lat = 0;
|
|
600
|
+
tangent1Lon = dLon;
|
|
601
|
+
tangent1Lat = 0;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
// Multi-point: use direction to neighbor
|
|
606
|
+
if (seg === 0) {
|
|
607
|
+
// First segment start: direction toward next
|
|
608
|
+
tangent0Lon = p1.longitude - p0.longitude;
|
|
609
|
+
tangent0Lat = p1.latitude - p0.latitude;
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
// Average of incoming and outgoing
|
|
613
|
+
const prev = pathPoints[seg - 1];
|
|
614
|
+
tangent0Lon = p1.longitude - prev.longitude;
|
|
615
|
+
tangent0Lat = p1.latitude - prev.latitude;
|
|
616
|
+
}
|
|
617
|
+
if (seg === pathPoints.length - 2) {
|
|
618
|
+
// Last segment end: direction from previous
|
|
619
|
+
tangent1Lon = p1.longitude - p0.longitude;
|
|
620
|
+
tangent1Lat = p1.latitude - p0.latitude;
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
const next = pathPoints[seg + 2];
|
|
624
|
+
tangent1Lon = next.longitude - p0.longitude;
|
|
625
|
+
tangent1Lat = next.latitude - p0.latitude;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// Normalize tangents and scale by cpd * segment distance
|
|
629
|
+
// Use cpdSource at the start of the first segment, cpdTarget at the end of the last
|
|
630
|
+
const segDist = Math.sqrt(Math.pow((p1.longitude - p0.longitude), 2) + Math.pow((p1.latitude - p0.latitude), 2));
|
|
631
|
+
const t0Len = Math.sqrt(Math.pow(tangent0Lon, 2) + Math.pow(tangent0Lat, 2));
|
|
632
|
+
const t1Len = Math.sqrt(Math.pow(tangent1Lon, 2) + Math.pow(tangent1Lat, 2));
|
|
633
|
+
const scale0 = segDist * (seg === 0 ? linkCpdSource : linkCpd);
|
|
634
|
+
const scale1 = segDist * (seg === pathPoints.length - 2 ? linkCpdTarget : linkCpd);
|
|
635
|
+
const cp0Lon = p0.longitude + (t0Len > 0 ? tangent0Lon / t0Len * scale0 : 0);
|
|
636
|
+
const cp0Lat = p0.latitude + (t0Len > 0 ? tangent0Lat / t0Len * scale0 : 0);
|
|
637
|
+
const cp1Lon = p1.longitude - (t1Len > 0 ? tangent1Lon / t1Len * scale1 : 0);
|
|
638
|
+
const cp1Lat = p1.latitude - (t1Len > 0 ? tangent1Lat / t1Len * scale1 : 0);
|
|
639
|
+
segments.push({
|
|
640
|
+
p0Lon: p0.longitude, p0Lat: p0.latitude,
|
|
641
|
+
p1Lon: p1.longitude, p1Lat: p1.latitude,
|
|
642
|
+
cp0Lon, cp0Lat, cp1Lon, cp1Lat
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
// Store for bullet positioning
|
|
646
|
+
this._bezierSegments.set(dataItem, segments);
|
|
647
|
+
this._computeCumulativeLengths(dataItem, segments);
|
|
648
|
+
// Sample all segments to build left/right edges
|
|
649
|
+
const leftEdge = [];
|
|
650
|
+
const rightEdge = [];
|
|
651
|
+
for (let seg = 0; seg < segments.length; seg++) {
|
|
652
|
+
const bp = segments[seg];
|
|
653
|
+
const startI = (seg === 0) ? 0 : 1; // avoid duplicate at joins
|
|
654
|
+
for (let i = startI; i <= resolution; i++) {
|
|
655
|
+
const t = i / resolution;
|
|
656
|
+
const s = 1 - t;
|
|
657
|
+
const lon = s * s * s * bp.p0Lon + 3 * s * s * t * bp.cp0Lon + 3 * s * t * t * bp.cp1Lon + t * t * t * bp.p1Lon;
|
|
658
|
+
const lat = s * s * s * bp.p0Lat + 3 * s * s * t * bp.cp0Lat + 3 * s * t * t * bp.cp1Lat + t * t * t * bp.p1Lat;
|
|
659
|
+
const dtLon = 3 * s * s * (bp.cp0Lon - bp.p0Lon) + 6 * s * t * (bp.cp1Lon - bp.cp0Lon) + 3 * t * t * (bp.p1Lon - bp.cp1Lon);
|
|
660
|
+
const dtLat = 3 * s * s * (bp.cp0Lat - bp.p0Lat) + 6 * s * t * (bp.cp1Lat - bp.cp0Lat) + 3 * t * t * (bp.p1Lat - bp.cp1Lat);
|
|
661
|
+
const tLen = Math.sqrt(dtLon * dtLon + dtLat * dtLat);
|
|
662
|
+
if (tLen === 0)
|
|
663
|
+
continue;
|
|
664
|
+
const perpLon = -dtLat / tLen;
|
|
665
|
+
const perpLat = dtLon / tLen;
|
|
666
|
+
leftEdge.push([lon + halfWidth * perpLon, lat + halfWidth * perpLat]);
|
|
667
|
+
rightEdge.push([lon - halfWidth * perpLon, lat - halfWidth * perpLat]);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
// Simple closed polygon: leftEdge forward + rightEdge backward
|
|
671
|
+
const ring = [];
|
|
672
|
+
for (let i = 0; i < leftEdge.length; i++)
|
|
673
|
+
ring.push(leftEdge[i]);
|
|
674
|
+
for (let i = rightEdge.length - 1; i >= 0; i--)
|
|
675
|
+
ring.push(rightEdge[i]);
|
|
676
|
+
ring.push(leftEdge[0]);
|
|
677
|
+
// d3-geo requires clockwise winding for small polygons.
|
|
678
|
+
// If geoArea > 2*PI the winding is inverted — reverse the ring.
|
|
679
|
+
let geometry = {
|
|
680
|
+
type: "Polygon",
|
|
681
|
+
coordinates: [ring]
|
|
682
|
+
};
|
|
683
|
+
if (geoArea(geometry) > 2 * Math.PI) {
|
|
684
|
+
ring.reverse();
|
|
685
|
+
geometry = {
|
|
686
|
+
type: "Polygon",
|
|
687
|
+
coordinates: [ring]
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
dataItem.setRaw("geometry", geometry);
|
|
691
|
+
const mapPolygon = dataItem.get("mapPolygon");
|
|
692
|
+
if (mapPolygon) {
|
|
693
|
+
mapPolygon.set("geometry", geometry);
|
|
694
|
+
if (linkColorMode !== "solid") {
|
|
695
|
+
const nodeDataItem = linkColorMode === "source"
|
|
696
|
+
? dataItem.get("sourceNode")
|
|
697
|
+
: dataItem.get("targetNode");
|
|
698
|
+
if (nodeDataItem) {
|
|
699
|
+
const nodeFill = nodeDataItem.get("fill");
|
|
700
|
+
if (nodeFill) {
|
|
701
|
+
mapPolygon.set("fill", nodeFill);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
// Compute sums and generate geometries for node shapes
|
|
708
|
+
const nodeType = this.get("nodeType", "circle");
|
|
709
|
+
const nodeWidth = this.get("nodeWidth", 1);
|
|
710
|
+
$array.each(this.nodes.dataItems, (nodeDataItem) => {
|
|
711
|
+
const outgoing = nodeDataItem.get("outgoingLinks") || [];
|
|
712
|
+
const incoming = nodeDataItem.get("incomingLinks") || [];
|
|
713
|
+
const nodeKey = nodeDataItem.get("id");
|
|
714
|
+
// Compute sums and store on node data item for tooltip resolution
|
|
715
|
+
let sumOutgoing = 0;
|
|
716
|
+
$array.each(outgoing, (link) => { sumOutgoing += link.get("value") || 0; });
|
|
717
|
+
let sumIncoming = 0;
|
|
718
|
+
$array.each(incoming, (link) => { sumIncoming += link.get("value") || 0; });
|
|
719
|
+
nodeDataItem.setRaw("sumOutgoing", sumOutgoing);
|
|
720
|
+
nodeDataItem.setRaw("sumIncoming", sumIncoming);
|
|
721
|
+
nodeDataItem.setRaw("sum", sumOutgoing + sumIncoming);
|
|
722
|
+
const totalWidth = nodeRange.get(nodeKey) || 0;
|
|
723
|
+
const lon = nodeDataItem.get("longitude");
|
|
724
|
+
const lat = nodeDataItem.get("latitude");
|
|
725
|
+
if (lon == null || lat == null)
|
|
726
|
+
return;
|
|
727
|
+
let nodeGeo;
|
|
728
|
+
if (nodeType === "bar") {
|
|
729
|
+
const steps = 10;
|
|
730
|
+
const halfSpan = totalWidth / 2 + nodePadding;
|
|
731
|
+
const halfW = nodeWidth / 2;
|
|
732
|
+
const ring = [];
|
|
733
|
+
if (orientationSetting === "vertical") {
|
|
734
|
+
for (let i = 0; i <= steps; i++) {
|
|
735
|
+
const t = i / steps;
|
|
736
|
+
ring.push([lon - halfSpan + totalWidth * t, lat - halfW]);
|
|
737
|
+
}
|
|
738
|
+
for (let i = 0; i <= steps; i++) {
|
|
739
|
+
const t = i / steps;
|
|
740
|
+
ring.push([lon + halfSpan, lat - halfW + nodeWidth * t]);
|
|
741
|
+
}
|
|
742
|
+
for (let i = 0; i <= steps; i++) {
|
|
743
|
+
const t = i / steps;
|
|
744
|
+
ring.push([lon + halfSpan - totalWidth * t, lat + halfW]);
|
|
745
|
+
}
|
|
746
|
+
for (let i = 0; i <= steps; i++) {
|
|
747
|
+
const t = i / steps;
|
|
748
|
+
ring.push([lon - halfSpan, lat + halfW - nodeWidth * t]);
|
|
749
|
+
}
|
|
750
|
+
ring.push([lon - halfSpan, lat - halfW]);
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
for (let i = 0; i <= steps; i++) {
|
|
754
|
+
const t = i / steps;
|
|
755
|
+
ring.push([lon - halfW, lat - halfSpan + totalWidth * t]);
|
|
756
|
+
}
|
|
757
|
+
for (let i = 0; i <= steps; i++) {
|
|
758
|
+
const t = i / steps;
|
|
759
|
+
ring.push([lon - halfW + nodeWidth * t, lat + halfSpan]);
|
|
760
|
+
}
|
|
761
|
+
for (let i = 0; i <= steps; i++) {
|
|
762
|
+
const t = i / steps;
|
|
763
|
+
ring.push([lon + halfW, lat + halfSpan - totalWidth * t]);
|
|
764
|
+
}
|
|
765
|
+
for (let i = 0; i <= steps; i++) {
|
|
766
|
+
const t = i / steps;
|
|
767
|
+
ring.push([lon + halfW - nodeWidth * t, lat - halfSpan]);
|
|
768
|
+
}
|
|
769
|
+
ring.push([lon - halfW, lat - halfSpan]);
|
|
770
|
+
}
|
|
771
|
+
nodeGeo = { type: "Polygon", coordinates: [ring] };
|
|
772
|
+
if (geoArea(nodeGeo) > 2 * Math.PI) {
|
|
773
|
+
ring.reverse();
|
|
774
|
+
nodeGeo = { type: "Polygon", coordinates: [ring] };
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
const radius = totalWidth / 2 + nodePadding;
|
|
779
|
+
nodeGeo = getGeoCircle({ longitude: lon, latitude: lat }, radius);
|
|
780
|
+
}
|
|
781
|
+
const mapPolygon = nodeDataItem.get("mapPolygon");
|
|
782
|
+
if (mapPolygon) {
|
|
783
|
+
mapPolygon.set("geometry", nodeGeo);
|
|
784
|
+
// Reorder at display level so nodes render above bands.
|
|
785
|
+
// Avoids Children.moveValue which triggers markDirty() loop.
|
|
786
|
+
this._childrenDisplay.removeChild(mapPolygon._display);
|
|
787
|
+
this._childrenDisplay.addChild(mapPolygon._display);
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
MapSankeySeries.className = "MapSankeySeries";
|
|
793
|
+
MapSankeySeries.classNames = MapPolygonSeries.classNames.concat([MapSankeySeries.className]);
|
|
794
|
+
//# sourceMappingURL=MapSankeySeries.js.map
|