@unovis/ts 1.6.5-topojson.8 → 1.6.5
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/components/topojson-map/index.d.ts +4 -3
- package/components/topojson-map/index.js +112 -199
- package/components/topojson-map/index.js.map +1 -1
- package/components/topojson-map/modules/background.d.ts +2 -0
- package/components/topojson-map/modules/background.js +13 -0
- package/components/topojson-map/modules/background.js.map +1 -0
- package/components/topojson-map/modules/flow.d.ts +19 -0
- package/components/topojson-map/modules/flow.js +134 -0
- package/components/topojson-map/modules/flow.js.map +1 -0
- package/components/topojson-map/modules/selectionRing.d.ts +5 -0
- package/components/topojson-map/modules/selectionRing.js +33 -0
- package/components/topojson-map/modules/selectionRing.js.map +1 -0
- package/components/topojson-map/style.d.ts +77 -2
- package/components/topojson-map/style.js +72 -85
- package/components/topojson-map/style.js.map +1 -1
- package/components/topojson-map/types.d.ts +21 -1
- package/components/topojson-map/types.js.map +1 -1
- package/components/topojson-map/utils.d.ts +16 -12
- package/components/topojson-map/utils.js +20 -12
- package/components/topojson-map/utils.js.map +1 -1
- package/components/treemap/config.d.ts +5 -1
- package/components/treemap/config.js +1 -1
- package/components/treemap/config.js.map +1 -1
- package/components/treemap/index.js +28 -19
- package/components/treemap/index.js.map +1 -1
- package/components/treemap/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -2,10 +2,11 @@ import { Selection } from 'd3-selection';
|
|
|
2
2
|
import { D3ZoomEvent, ZoomTransform } from 'd3-zoom';
|
|
3
3
|
import { ComponentCore } from "../../core/component";
|
|
4
4
|
import { MapGraphDataModel } from "../../data-models/map-graph";
|
|
5
|
+
import { GenericDataRecord } from "../../types/data";
|
|
5
6
|
import { MapData } from './types';
|
|
6
7
|
import { TopoJSONMapConfigInterface } from './config';
|
|
7
8
|
import * as s from './style';
|
|
8
|
-
export declare class TopoJSONMap<AreaDatum, PointDatum =
|
|
9
|
+
export declare class TopoJSONMap<AreaDatum, PointDatum = GenericDataRecord, LinkDatum = GenericDataRecord> extends ComponentCore<MapData<AreaDatum, PointDatum, LinkDatum>, TopoJSONMapConfigInterface<AreaDatum, PointDatum, LinkDatum>> {
|
|
9
10
|
static selectors: typeof s;
|
|
10
11
|
protected _defaultConfig: TopoJSONMapConfigInterface<AreaDatum, PointDatum, LinkDatum>;
|
|
11
12
|
config: TopoJSONMapConfigInterface<AreaDatum, PointDatum, LinkDatum>;
|
|
@@ -65,10 +66,10 @@ export declare class TopoJSONMap<AreaDatum, PointDatum = unknown, LinkDatum = un
|
|
|
65
66
|
_fitToPoints(points?: PointDatum[], pad?: number): void;
|
|
66
67
|
_applyZoom(): void;
|
|
67
68
|
_onResize(): void;
|
|
68
|
-
_onZoom(event: D3ZoomEvent<
|
|
69
|
+
_onZoom(event: D3ZoomEvent<SVGGElement, unknown>): void;
|
|
69
70
|
_onZoomEnd(): void;
|
|
70
71
|
private _runCollisionDetection;
|
|
71
|
-
_onZoomHandler(transform: ZoomTransform, isMouseEvent: boolean, isExternalEvent: boolean): void;
|
|
72
|
+
_onZoomHandler(transform: ZoomTransform, isMouseEvent: boolean, isExternalEvent: boolean, isResizeEvent?: boolean): void;
|
|
72
73
|
zoomIn(increment?: number): void;
|
|
73
74
|
zoomOut(increment?: number): void;
|
|
74
75
|
setZoom(zoomLevel: number): void;
|
|
@@ -17,8 +17,11 @@ import { MapProjection, TopoJSONMapPointShape, MapPointLabelPosition } from './t
|
|
|
17
17
|
import { TopoJSONMapDefaultConfig } from './config.js';
|
|
18
18
|
import { calculateClusterIndex, getClusterRadius, getLonLat, arc, getDonutData, getPointPathData, getClustersAndPoints, geoJsonPointToScreenPoint, collideAreaLabels, collidePointBottomLabels, getNextZoomLevelOnClusterClick, getPointRadius } from './utils.js';
|
|
19
19
|
import { updateDonut } from './modules/donut.js';
|
|
20
|
+
import { renderBackground } from './modules/background.js';
|
|
21
|
+
import { updateSelectionRing } from './modules/selectionRing.js';
|
|
22
|
+
import { initFlowFeatures, updateFlowParticles } from './modules/flow.js';
|
|
20
23
|
import * as style from './style.js';
|
|
21
|
-
import { background, features, areaLabel, links, clusterBackground, points, pointSelectionRing, pointSelection, flowParticles, sourcePoints, feature as feature$1, clusterBackgroundCircle, link, point, pointShape, clusterDonut, pointDonut, pointPathRing, pointLabel, pointBottomLabel, sourcePoint, flowParticle } from './style.js';
|
|
24
|
+
import { background, features, areaLabel, links, clusterBackground, points, pointSelectionRing, pointSelection, flowParticles, sourcePoints, feature as feature$1, variables, clusterBackgroundCircle, link, point, pointShape, clusterDonut, pointDonut, pointPathRing, pointLabel, pointBottomLabel, sourcePoint, flowParticle } from './style.js';
|
|
22
25
|
|
|
23
26
|
// Supercluster expects zoom levels 0-22 (like map tiles). zoomExtent[1] is a scale factor
|
|
24
27
|
// for the map, so we cap the cluster maxZoom to avoid excessive quadtree depth and wrong
|
|
@@ -171,15 +174,7 @@ class TopoJSONMap extends ComponentCore {
|
|
|
171
174
|
}
|
|
172
175
|
}
|
|
173
176
|
_renderBackground() {
|
|
174
|
-
this._backgroundRect
|
|
175
|
-
.attr('width', '100%')
|
|
176
|
-
.attr('height', '100%')
|
|
177
|
-
.attr('transform', `translate(${-this.bleed.left}, ${-this.bleed.top})`)
|
|
178
|
-
.style('cursor', 'default')
|
|
179
|
-
.on('click', () => {
|
|
180
|
-
// Collapse expanded cluster when clicking on background
|
|
181
|
-
this._collapseExpandedCluster();
|
|
182
|
-
});
|
|
177
|
+
renderBackground(this._backgroundRect, this.bleed.left, this.bleed.top, () => this._collapseExpandedCluster());
|
|
183
178
|
}
|
|
184
179
|
_renderGroups(duration) {
|
|
185
180
|
const transformString = this._transform.toString();
|
|
@@ -200,7 +195,7 @@ class TopoJSONMap extends ComponentCore {
|
|
|
200
195
|
.attr('transform', transformString);
|
|
201
196
|
}
|
|
202
197
|
_renderMap(duration) {
|
|
203
|
-
var _a, _b, _c;
|
|
198
|
+
var _a, _b, _c, _d, _e;
|
|
204
199
|
const { bleed, config, datamodel } = this;
|
|
205
200
|
this.g.attr('transform', `translate(${bleed.left}, ${bleed.top})`);
|
|
206
201
|
const mapData = config.topojson;
|
|
@@ -213,14 +208,18 @@ class TopoJSONMap extends ComponentCore {
|
|
|
213
208
|
if (this._firstRender) {
|
|
214
209
|
// Rendering the map for the first time.
|
|
215
210
|
this._projection.fitExtent([[0, 0], [this._width, this._height]], this._featureCollection);
|
|
216
|
-
this._initialScale = this._projection.scale();
|
|
217
|
-
this._center = this._projection.translate();
|
|
218
211
|
if (config.mapFitToPoints) {
|
|
212
|
+
// Re-fit projection to points instead of full topojson
|
|
219
213
|
this._fitToPoints();
|
|
220
214
|
}
|
|
215
|
+
// After initial fit (to features or points), treat the current projection
|
|
216
|
+
// scale as the baseline for zoom level 1.
|
|
217
|
+
this._initialScale = this._projection.scale();
|
|
218
|
+
this._center = this._projection.translate();
|
|
221
219
|
const zoomExtent = config.zoomExtent;
|
|
222
220
|
this._zoomBehavior.scaleExtent([zoomExtent[0] * this._initialScale, zoomExtent[1] * this._initialScale]);
|
|
223
|
-
this.
|
|
221
|
+
this._currentZoomLevel = config.zoomFactor || 1;
|
|
222
|
+
(_e = (_d = this.g.node()) === null || _d === void 0 ? void 0 : _d.parentElement) === null || _e === void 0 ? void 0 : _e.style.setProperty('--vis-map-current-zoom-level', String(this._currentZoomLevel));
|
|
224
223
|
if (!config.disableZoom) {
|
|
225
224
|
this.g.call(this._zoomBehavior);
|
|
226
225
|
this._applyZoom();
|
|
@@ -252,10 +251,12 @@ class TopoJSONMap extends ComponentCore {
|
|
|
252
251
|
.data(featureData);
|
|
253
252
|
const featuresEnter = features.enter().append('path').attr('class', feature$1);
|
|
254
253
|
const featuresMerged = featuresEnter.merge(features);
|
|
254
|
+
// Animate geometry changes, but apply fill immediately to avoid "delay then jump"
|
|
255
255
|
smartTransition(featuresMerged, duration)
|
|
256
256
|
.attr('d', this._path)
|
|
257
|
-
.style('fill', (d, i) => d.data ? getColor(d.data, config.areaColor, i) : null)
|
|
258
257
|
.style('cursor', d => d.data ? getString(d.data, config.areaCursor) : null);
|
|
258
|
+
// Set fill directly so color updates are instantaneous instead of snapping at the end of a transition
|
|
259
|
+
featuresMerged.style('fill', (d, i) => d.data ? getColor(d.data, config.areaColor, i) : null);
|
|
259
260
|
// Add click handler to collapse expanded cluster when clicking on map features
|
|
260
261
|
featuresMerged.on('click', () => {
|
|
261
262
|
this._collapseExpandedCluster();
|
|
@@ -312,7 +313,7 @@ class TopoJSONMap extends ComponentCore {
|
|
|
312
313
|
labelsMerged
|
|
313
314
|
.text(d => d.labelText)
|
|
314
315
|
.attr('transform', d => `translate(${d.centroid[0]},${d.centroid[1]})`)
|
|
315
|
-
.style('font-size', `calc(var(
|
|
316
|
+
.style('font-size', `calc(var(${variables.mapPointLabelFontSize}) / ${this._currentZoomLevel})`)
|
|
316
317
|
.style('text-anchor', 'middle')
|
|
317
318
|
.style('dominant-baseline', 'middle');
|
|
318
319
|
// Handle exiting labels
|
|
@@ -342,7 +343,7 @@ class TopoJSONMap extends ComponentCore {
|
|
|
342
343
|
.attr('cx', pos[0])
|
|
343
344
|
.attr('cy', pos[1])
|
|
344
345
|
.attr('r', 0)
|
|
345
|
-
.style('fill',
|
|
346
|
+
.style('fill', `var(${variables.mapClusterExpandedBackgroundFillColor})`)
|
|
346
347
|
.style('opacity', 0)
|
|
347
348
|
.style('cursor', 'pointer')
|
|
348
349
|
.on('click', () => {
|
|
@@ -423,12 +424,13 @@ class TopoJSONMap extends ComponentCore {
|
|
|
423
424
|
const expandedClusterId = this._expandedCluster.cluster.properties.clusterId;
|
|
424
425
|
// Remove the expanded cluster if it still exists at this zoom level
|
|
425
426
|
geoJsonPoints = geoJsonPoints.filter((c) => {
|
|
426
|
-
const
|
|
427
|
+
const props = c.properties;
|
|
428
|
+
const isExpandedCluster = props.cluster && props.clusterId === expandedClusterId;
|
|
427
429
|
return !isExpandedCluster;
|
|
428
430
|
});
|
|
429
431
|
// Remove any individual points and subclusters that are part of the expanded cluster to avoid duplicates
|
|
430
|
-
const expandedPointIds = new Set(this._expandedCluster.points.map(
|
|
431
|
-
geoJsonPoints = geoJsonPoints.filter(
|
|
432
|
+
const expandedPointIds = new Set(this._expandedCluster.points.map(p => p.id.toString()));
|
|
433
|
+
geoJsonPoints = geoJsonPoints.filter(c => !this._shouldFilterPointOrCluster(geoJsonPointToScreenPoint(c, 0, this._projection, this.config, this._currentZoomLevel || 1), expandedPointIds));
|
|
432
434
|
// Add points from the expanded cluster
|
|
433
435
|
geoJsonPoints = geoJsonPoints.concat(this._expandedCluster.points);
|
|
434
436
|
}
|
|
@@ -436,7 +438,10 @@ class TopoJSONMap extends ComponentCore {
|
|
|
436
438
|
// When collapsed, restore the original cluster point instead of relying on clustering algorithm
|
|
437
439
|
const collapsedClusterId = this._collapsedCluster.properties.clusterId;
|
|
438
440
|
// Check if the clustering algorithm has recreated a similar cluster at this zoom level
|
|
439
|
-
const hasNaturalCluster = geoJsonPoints.some(
|
|
441
|
+
const hasNaturalCluster = geoJsonPoints.some(c => {
|
|
442
|
+
const props = c.properties;
|
|
443
|
+
return props.cluster && props.clusterId === collapsedClusterId;
|
|
444
|
+
});
|
|
440
445
|
if (hasNaturalCluster) {
|
|
441
446
|
// Natural cluster exists, we can safely clear the collapsed cluster
|
|
442
447
|
this._collapsedCluster = null;
|
|
@@ -444,7 +449,7 @@ class TopoJSONMap extends ComponentCore {
|
|
|
444
449
|
}
|
|
445
450
|
else {
|
|
446
451
|
// Remove any individual points and subclusters that were part of the collapsed cluster
|
|
447
|
-
geoJsonPoints = geoJsonPoints.filter(
|
|
452
|
+
geoJsonPoints = geoJsonPoints.filter(c => !this._shouldFilterPointOrCluster(geoJsonPointToScreenPoint(c, 0, this._projection, this.config, this._currentZoomLevel || 1), this._collapsedClusterPointIds));
|
|
448
453
|
// Add the original cluster back
|
|
449
454
|
geoJsonPoints.push(this._collapsedCluster);
|
|
450
455
|
}
|
|
@@ -452,14 +457,14 @@ class TopoJSONMap extends ComponentCore {
|
|
|
452
457
|
return geoJsonPoints.map((geoPoint, i) => geoJsonPointToScreenPoint(geoPoint, i, this._projection, this.config, this._currentZoomLevel || 1));
|
|
453
458
|
}
|
|
454
459
|
_renderPoints(duration) {
|
|
455
|
-
var _a, _b, _c;
|
|
456
460
|
const { config } = this;
|
|
457
461
|
const pointData = this._getPointData();
|
|
458
462
|
const currentZoomLevel = this._currentZoomLevel || 1;
|
|
459
463
|
// Set z-index for expanded cluster points to ensure proper layering
|
|
460
464
|
if (this._expandedCluster && config.clusterBackground) {
|
|
461
465
|
pointData.forEach((d) => {
|
|
462
|
-
|
|
466
|
+
const expandedPoint = d;
|
|
467
|
+
expandedPoint._zIndex = expandedPoint.expandedClusterPoint ? 2 : 0;
|
|
463
468
|
});
|
|
464
469
|
}
|
|
465
470
|
const points = this._pointsGroup
|
|
@@ -468,11 +473,12 @@ class TopoJSONMap extends ComponentCore {
|
|
|
468
473
|
// Enter
|
|
469
474
|
const pointsEnter = points.enter().append('g').attr('class', point)
|
|
470
475
|
.attr('transform', d => {
|
|
476
|
+
var _a, _b;
|
|
471
477
|
const pos = this._projection(d.geometry.coordinates);
|
|
472
478
|
const expandedPoint = d;
|
|
473
479
|
// Divide by zoom level to compensate for the group's zoom transform
|
|
474
|
-
const dx = (expandedPoint.dx
|
|
475
|
-
const dy = (expandedPoint.dy
|
|
480
|
+
const dx = ((_a = expandedPoint.dx) !== null && _a !== void 0 ? _a : 0) / currentZoomLevel;
|
|
481
|
+
const dy = ((_b = expandedPoint.dy) !== null && _b !== void 0 ? _b : 0) / currentZoomLevel;
|
|
476
482
|
return `translate(${pos[0] + dx},${pos[1] + dy})`;
|
|
477
483
|
})
|
|
478
484
|
.style('opacity', 1e-6);
|
|
@@ -528,11 +534,12 @@ class TopoJSONMap extends ComponentCore {
|
|
|
528
534
|
const pointsMerged = pointsEnter.merge(points);
|
|
529
535
|
smartTransition(pointsMerged, duration)
|
|
530
536
|
.attr('transform', d => {
|
|
537
|
+
var _a, _b;
|
|
531
538
|
const pos = this._projection(d.geometry.coordinates);
|
|
532
539
|
const expandedPoint = d;
|
|
533
540
|
// Divide by zoom level to compensate for the group's zoom transform
|
|
534
|
-
const dx = (expandedPoint.dx
|
|
535
|
-
const dy = (expandedPoint.dy
|
|
541
|
+
const dx = ((_a = expandedPoint.dx) !== null && _a !== void 0 ? _a : 0) / currentZoomLevel;
|
|
542
|
+
const dy = ((_b = expandedPoint.dy) !== null && _b !== void 0 ? _b : 0) / currentZoomLevel;
|
|
536
543
|
return `translate(${pos[0] + dx},${pos[1] + dy})`;
|
|
537
544
|
})
|
|
538
545
|
.style('cursor', d => {
|
|
@@ -643,7 +650,7 @@ class TopoJSONMap extends ComponentCore {
|
|
|
643
650
|
})
|
|
644
651
|
.style('font-size', d => {
|
|
645
652
|
if (config.pointLabelPosition === MapPointLabelPosition.Bottom) {
|
|
646
|
-
return `calc(var(
|
|
653
|
+
return `calc(var(${variables.mapPointLabelFontSize}) / ${currentZoomLevel})`;
|
|
647
654
|
}
|
|
648
655
|
const radius = d.isCluster
|
|
649
656
|
? (d.radius / currentZoomLevel)
|
|
@@ -679,7 +686,9 @@ class TopoJSONMap extends ComponentCore {
|
|
|
679
686
|
var _a;
|
|
680
687
|
if (d.donutData.length > 0) {
|
|
681
688
|
// Cluster background is white, so use dark text
|
|
682
|
-
return d.isCluster
|
|
689
|
+
return d.isCluster
|
|
690
|
+
? `var(${variables.mapPointLabelTextColorDark})`
|
|
691
|
+
: `var(${variables.mapPointLabelTextColorLight})`;
|
|
683
692
|
}
|
|
684
693
|
else {
|
|
685
694
|
if (config.pointLabelColor) {
|
|
@@ -692,7 +701,9 @@ class TopoJSONMap extends ComponentCore {
|
|
|
692
701
|
if (!hex)
|
|
693
702
|
return null;
|
|
694
703
|
const brightness = hexToBrightness(hex);
|
|
695
|
-
return brightness > config.pointLabelTextBrightnessRatio
|
|
704
|
+
return brightness > config.pointLabelTextBrightnessRatio
|
|
705
|
+
? `var(${variables.mapPointLabelTextColorDark})`
|
|
706
|
+
: `var(${variables.mapPointLabelTextColorLight})`;
|
|
696
707
|
}
|
|
697
708
|
})
|
|
698
709
|
.style('opacity', 1)
|
|
@@ -722,7 +733,7 @@ class TopoJSONMap extends ComponentCore {
|
|
|
722
733
|
return radius + (10 / this._currentZoomLevel); // offset below the point/cluster, scaled with zoom
|
|
723
734
|
})
|
|
724
735
|
.attr('dy', '0.32em')
|
|
725
|
-
.style('font-size', `calc(var(
|
|
736
|
+
.style('font-size', `calc(var(${variables.mapPointBottomLabelFontSize}) / ${this._currentZoomLevel})`);
|
|
726
737
|
smartTransition(bottomLabelsMerged, duration)
|
|
727
738
|
.style('opacity', d => d.expandedClusterPoint ? 0 : 1);
|
|
728
739
|
// Sort elements by z-index to ensure expanded cluster points appear above everything else
|
|
@@ -738,36 +749,16 @@ class TopoJSONMap extends ComponentCore {
|
|
|
738
749
|
this._pointsGroup.selectAll(`.${pointLabel}`).style('display', (config.heatmapMode && (this._currentZoomLevel < config.heatmapModeZoomLevelThreshold)) ? 'none' : null);
|
|
739
750
|
this._pointsGroup.selectAll(`.${pointBottomLabel}`).style('display', (config.heatmapMode && (this._currentZoomLevel < config.heatmapModeZoomLevelThreshold)) ? 'none' : null);
|
|
740
751
|
// Update selection ring
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
? (d.id === this._selectedPoint.id)
|
|
746
|
-
: (selectedPointId && getString(d.properties, config.pointId) === selectedPointId));
|
|
747
|
-
const pos = this._projection((foundPoint !== null && foundPoint !== void 0 ? foundPoint : this._selectedPoint).geometry.coordinates);
|
|
748
|
-
if (pos) {
|
|
749
|
-
const dx = (((_a = foundPoint) === null || _a === void 0 ? void 0 : _a.dx) || 0) / currentZoomLevel;
|
|
750
|
-
const dy = (((_b = foundPoint) === null || _b === void 0 ? void 0 : _b.dy) || 0) / currentZoomLevel;
|
|
751
|
-
this._pointSelectionRing.attr('transform', `translate(${pos[0] + dx},${pos[1] + dy})`);
|
|
752
|
-
}
|
|
753
|
-
pointSelection$1
|
|
754
|
-
.classed('active', Boolean(foundPoint))
|
|
755
|
-
.attr('d', (foundPoint === null || foundPoint === void 0 ? void 0 : foundPoint.path) || null)
|
|
756
|
-
.style('fill', 'transparent')
|
|
757
|
-
.style('stroke-width', 1)
|
|
758
|
-
.style('stroke', (_c = (foundPoint || this._selectedPoint)) === null || _c === void 0 ? void 0 : _c.color)
|
|
759
|
-
.style('transform', `scale(${1.25 / currentZoomLevel})`);
|
|
760
|
-
}
|
|
761
|
-
else {
|
|
762
|
-
pointSelection$1.classed('active', false);
|
|
763
|
-
}
|
|
752
|
+
updateSelectionRing(this._pointSelectionRing, this._selectedPoint, pointData, this.config, this._projection, currentZoomLevel);
|
|
753
|
+
// Set up events and custom attributes for rendered points (match LeafletMap behavior)
|
|
754
|
+
this._setUpComponentEventsThrottled();
|
|
755
|
+
this._setCustomAttributesThrottled();
|
|
764
756
|
}
|
|
765
757
|
_fitToPoints(points, pad = 0.1) {
|
|
766
758
|
const { config, datamodel } = this;
|
|
767
759
|
const pointData = points || datamodel.points;
|
|
768
760
|
if (pointData.length === 0)
|
|
769
761
|
return;
|
|
770
|
-
this.fitView();
|
|
771
762
|
const coordinates = pointData.map(p => [
|
|
772
763
|
getNumber(p, d => getNumber(d, config.longitude)),
|
|
773
764
|
getNumber(p, d => getNumber(d, config.latitude)),
|
|
@@ -802,9 +793,7 @@ class TopoJSONMap extends ComponentCore {
|
|
|
802
793
|
]);
|
|
803
794
|
}
|
|
804
795
|
}
|
|
805
|
-
// If we don't update the center, the next zoom will be centered around the previous value
|
|
806
796
|
this._center = this._projection.translate();
|
|
807
|
-
this._applyZoom();
|
|
808
797
|
}
|
|
809
798
|
_applyZoom() {
|
|
810
799
|
var _a;
|
|
@@ -819,10 +808,25 @@ class TopoJSONMap extends ComponentCore {
|
|
|
819
808
|
const prevTranslate = this._projection.translate();
|
|
820
809
|
this._projection.fitExtent([[0, 0], [this._width, this._height]], this._featureCollection);
|
|
821
810
|
this._initialScale = this._projection.scale();
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
811
|
+
// If a point is selected, center the view on it after resize
|
|
812
|
+
if (this._selectedPoint) {
|
|
813
|
+
const coords = this._selectedPoint.geometry.coordinates;
|
|
814
|
+
const pos = this._projection(coords);
|
|
815
|
+
const projTranslate = this._projection.translate();
|
|
816
|
+
if (pos) {
|
|
817
|
+
const k = this._currentZoomLevel;
|
|
818
|
+
this._center = [
|
|
819
|
+
this._width / 2 - (pos[0] - projTranslate[0]) * k,
|
|
820
|
+
this._height / 2 - (pos[1] - projTranslate[1]) * k,
|
|
821
|
+
];
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
this._center = [
|
|
826
|
+
this._projection.translate()[0] * this._center[0] / prevTranslate[0],
|
|
827
|
+
this._projection.translate()[1] * this._center[1] / prevTranslate[1],
|
|
828
|
+
];
|
|
829
|
+
}
|
|
826
830
|
this._applyZoom();
|
|
827
831
|
this._isResizing = false;
|
|
828
832
|
this._prevWidth = this._width;
|
|
@@ -837,8 +841,9 @@ class TopoJSONMap extends ComponentCore {
|
|
|
837
841
|
(_b = (_a = this.g.node()) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.style.setProperty('--vis-map-current-zoom-level', String(this._currentZoomLevel));
|
|
838
842
|
return; // To prevent double render because of binding zoom behaviour
|
|
839
843
|
}
|
|
840
|
-
const isMouseEvent = event.sourceEvent
|
|
844
|
+
const isMouseEvent = !!event.sourceEvent;
|
|
841
845
|
const isExternalEvent = !(event === null || event === void 0 ? void 0 : event.sourceEvent) && !this._isResizing;
|
|
846
|
+
const isResizeEvent = this._isResizing && !(event === null || event === void 0 ? void 0 : event.sourceEvent);
|
|
842
847
|
this._isZooming = true;
|
|
843
848
|
// Clear any pending zoom end timeout
|
|
844
849
|
if (this._zoomEndTimeoutId) {
|
|
@@ -852,7 +857,7 @@ class TopoJSONMap extends ComponentCore {
|
|
|
852
857
|
this._collapsedClusterPointIds = null;
|
|
853
858
|
}
|
|
854
859
|
window.cancelAnimationFrame(this._animFrameId);
|
|
855
|
-
this._animFrameId = window.requestAnimationFrame(this._onZoomHandler.bind(this, event.transform, isMouseEvent, isExternalEvent));
|
|
860
|
+
this._animFrameId = window.requestAnimationFrame(this._onZoomHandler.bind(this, event.transform, isMouseEvent, isExternalEvent, isResizeEvent));
|
|
856
861
|
if (isMouseEvent) {
|
|
857
862
|
// Update the center coordinate so that the next call to _applyZoom()
|
|
858
863
|
// will zoom with respect to the current view
|
|
@@ -876,21 +881,22 @@ class TopoJSONMap extends ComponentCore {
|
|
|
876
881
|
_runCollisionDetection() {
|
|
877
882
|
window.cancelAnimationFrame(this._collisionDetectionAnimFrameId);
|
|
878
883
|
this._collisionDetectionAnimFrameId = window.requestAnimationFrame(() => {
|
|
884
|
+
const duration = this.config.duration;
|
|
879
885
|
// Run collision detection for area labels
|
|
880
886
|
const areaLabels = this._areaLabelsGroup.selectAll(`.${areaLabel}`);
|
|
881
|
-
collideAreaLabels(areaLabels);
|
|
887
|
+
collideAreaLabels(areaLabels, duration);
|
|
882
888
|
// Run collision detection for point bottom labels
|
|
883
889
|
const pointBottomLabels = this._pointsGroup.selectAll(`.${point} .${pointBottomLabel}`);
|
|
884
|
-
collidePointBottomLabels(pointBottomLabels);
|
|
890
|
+
collidePointBottomLabels(pointBottomLabels, duration);
|
|
885
891
|
});
|
|
886
892
|
}
|
|
887
|
-
_onZoomHandler(transform, isMouseEvent, isExternalEvent) {
|
|
893
|
+
_onZoomHandler(transform, isMouseEvent, isExternalEvent, isResizeEvent = false) {
|
|
888
894
|
const scale = transform.k / this._initialScale || 1;
|
|
889
895
|
const center = this._projection.translate();
|
|
890
896
|
this._transform = zoomIdentity
|
|
891
897
|
.translate(transform.x - center[0] * scale, transform.y - center[1] * scale)
|
|
892
898
|
.scale(scale);
|
|
893
|
-
const customDuration = isExternalEvent
|
|
899
|
+
const customDuration = (isExternalEvent || isResizeEvent)
|
|
894
900
|
? this.config.zoomDuration
|
|
895
901
|
: (isMouseEvent ? 0 : null);
|
|
896
902
|
// Call render functions that depend on this._transform
|
|
@@ -904,11 +910,13 @@ class TopoJSONMap extends ComponentCore {
|
|
|
904
910
|
zoomIn(increment = 0.5) {
|
|
905
911
|
if (this._isZooming)
|
|
906
912
|
return;
|
|
913
|
+
this._resetExpandedCluster();
|
|
907
914
|
this.setZoom(this._currentZoomLevel + increment);
|
|
908
915
|
}
|
|
909
916
|
zoomOut(increment = 0.5) {
|
|
910
917
|
if (this._isZooming)
|
|
911
918
|
return;
|
|
919
|
+
this._resetExpandedCluster();
|
|
912
920
|
this.setZoom(this._currentZoomLevel - increment);
|
|
913
921
|
}
|
|
914
922
|
setZoom(zoomLevel) {
|
|
@@ -935,113 +943,34 @@ class TopoJSONMap extends ComponentCore {
|
|
|
935
943
|
this._applyZoom();
|
|
936
944
|
}
|
|
937
945
|
fitView() {
|
|
938
|
-
var _a, _b, _c;
|
|
939
|
-
|
|
940
|
-
this.
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
946
|
+
var _a, _b, _c, _d, _e;
|
|
947
|
+
const { config } = this;
|
|
948
|
+
this._resetExpandedCluster();
|
|
949
|
+
if (config.mapFitToPoints) {
|
|
950
|
+
// When mapFitToPoints is enabled, fitView should refit the projection to the points
|
|
951
|
+
// and reset the zoom level relative to that fitted scale.
|
|
952
|
+
this._fitToPoints();
|
|
953
|
+
// Treat the newly fitted scale as the baseline and reset zoom to 1
|
|
954
|
+
this._initialScale = this._projection.scale();
|
|
955
|
+
this._currentZoomLevel = 1;
|
|
956
|
+
(_b = (_a = this.g.node()) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.style.setProperty('--vis-map-current-zoom-level', '1');
|
|
957
|
+
this._center = this._projection.translate();
|
|
958
|
+
this._applyZoom();
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
this._projection.fitExtent([[0, 0], [this._width, this._height]], this._featureCollection);
|
|
962
|
+
this._currentZoomLevel = (((_c = this._projection) === null || _c === void 0 ? void 0 : _c.scale()) / this._initialScale) || 1;
|
|
963
|
+
// Set CSS custom property for zoom level
|
|
964
|
+
(_e = (_d = this.g.node()) === null || _d === void 0 ? void 0 : _d.parentElement) === null || _e === void 0 ? void 0 : _e.style.setProperty('--vis-map-current-zoom-level', String(this._currentZoomLevel));
|
|
965
|
+
this._center = this._projection.translate();
|
|
966
|
+
// We are using this._applyZoom() instead of directly calling this._render(config.zoomDuration) because
|
|
967
|
+
// we've to "attach" new transform to the map group element. Otherwise zoomBehavior will not know
|
|
968
|
+
// that the zoom state has changed
|
|
969
|
+
this._applyZoom();
|
|
970
|
+
}
|
|
948
971
|
}
|
|
949
972
|
_initFlowFeatures() {
|
|
950
|
-
|
|
951
|
-
const { config, datamodel } = this;
|
|
952
|
-
// Use raw links data instead of processed links to avoid point lookup issues for flows
|
|
953
|
-
const rawLinks = ((_a = datamodel.data) === null || _a === void 0 ? void 0 : _a.links) || [];
|
|
954
|
-
// Clear existing flow data
|
|
955
|
-
this._flowParticles = [];
|
|
956
|
-
this._sourcePoints = [];
|
|
957
|
-
if (!rawLinks || rawLinks.length === 0)
|
|
958
|
-
return;
|
|
959
|
-
// Create source points and flow particles for each link
|
|
960
|
-
rawLinks.forEach((link, i) => {
|
|
961
|
-
var _a, _b;
|
|
962
|
-
// Try to get coordinates from flow-specific accessors first, then fall back to link endpoints
|
|
963
|
-
let sourceLon, sourceLat, targetLon, targetLat;
|
|
964
|
-
if (config.sourceLongitude && config.sourceLatitude) {
|
|
965
|
-
sourceLon = getNumber(link, config.sourceLongitude);
|
|
966
|
-
sourceLat = getNumber(link, config.sourceLatitude);
|
|
967
|
-
}
|
|
968
|
-
else {
|
|
969
|
-
// Fall back to using linkSource point coordinates
|
|
970
|
-
const sourcePoint = (_a = config.linkSource) === null || _a === void 0 ? void 0 : _a.call(config, link);
|
|
971
|
-
if (typeof sourcePoint === 'object' && sourcePoint !== null) {
|
|
972
|
-
sourceLon = getNumber(sourcePoint, config.longitude);
|
|
973
|
-
sourceLat = getNumber(sourcePoint, config.latitude);
|
|
974
|
-
}
|
|
975
|
-
else {
|
|
976
|
-
return; // Skip if can't resolve source coordinates
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
if (config.targetLongitude && config.targetLatitude) {
|
|
980
|
-
targetLon = getNumber(link, config.targetLongitude);
|
|
981
|
-
targetLat = getNumber(link, config.targetLatitude);
|
|
982
|
-
}
|
|
983
|
-
else {
|
|
984
|
-
// Fall back to using linkTarget point coordinates
|
|
985
|
-
const targetPoint = (_b = config.linkTarget) === null || _b === void 0 ? void 0 : _b.call(config, link);
|
|
986
|
-
if (typeof targetPoint === 'object' && targetPoint !== null) {
|
|
987
|
-
targetLon = getNumber(targetPoint, config.longitude);
|
|
988
|
-
targetLat = getNumber(targetPoint, config.latitude);
|
|
989
|
-
}
|
|
990
|
-
else {
|
|
991
|
-
return; // Skip if can't resolve target coordinates
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
if (!isNumber(sourceLon) || !isNumber(sourceLat) || !isNumber(targetLon) || !isNumber(targetLat)) {
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
|
-
// Create source point
|
|
998
|
-
const sourcePos = this._projection([sourceLon, sourceLat]);
|
|
999
|
-
if (sourcePos) {
|
|
1000
|
-
const sourcePoint = {
|
|
1001
|
-
lat: sourceLat,
|
|
1002
|
-
lon: sourceLon,
|
|
1003
|
-
x: sourcePos[0],
|
|
1004
|
-
y: sourcePos[1],
|
|
1005
|
-
radius: getNumber(link, config.sourcePointRadius),
|
|
1006
|
-
color: getColor(link, config.sourcePointColor, i),
|
|
1007
|
-
flowData: link,
|
|
1008
|
-
};
|
|
1009
|
-
this._sourcePoints.push(sourcePoint);
|
|
1010
|
-
}
|
|
1011
|
-
// Use the same arc as _renderLinks for flow animation
|
|
1012
|
-
const sourceProj = this._projection([sourceLon, sourceLat]);
|
|
1013
|
-
const targetProj = this._projection([targetLon, targetLat]);
|
|
1014
|
-
if (!sourceProj || !targetProj)
|
|
1015
|
-
return;
|
|
1016
|
-
// Generate SVG arc path string using the same arc() function
|
|
1017
|
-
const arcPath = arc(sourceProj, targetProj);
|
|
1018
|
-
// Create a temporary SVG path element for sampling
|
|
1019
|
-
const tempPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1020
|
-
tempPath.setAttribute('d', arcPath);
|
|
1021
|
-
const pathLength = tempPath.getTotalLength();
|
|
1022
|
-
const dist = Math.sqrt(Math.pow((targetLat - sourceLat), 2) + Math.pow((targetLon - sourceLon), 2));
|
|
1023
|
-
const numParticles = Math.max(1, Math.round(dist * getNumber(link, config.flowParticleDensity)));
|
|
1024
|
-
const velocity = getNumber(link, config.flowParticleSpeed);
|
|
1025
|
-
const radius = getNumber(link, config.flowParticleRadius);
|
|
1026
|
-
const color = getColor(link, config.flowParticleColor, i);
|
|
1027
|
-
for (let j = 0; j < numParticles; j += 1) {
|
|
1028
|
-
const progress = j / numParticles;
|
|
1029
|
-
const pt = tempPath.getPointAtLength(progress * pathLength);
|
|
1030
|
-
const particle = {
|
|
1031
|
-
x: pt.x,
|
|
1032
|
-
y: pt.y,
|
|
1033
|
-
velocity,
|
|
1034
|
-
radius,
|
|
1035
|
-
color,
|
|
1036
|
-
progress,
|
|
1037
|
-
arcPath,
|
|
1038
|
-
pathLength,
|
|
1039
|
-
id: `${getString(link, config.linkId, i) || i}-${j}`,
|
|
1040
|
-
flowData: undefined,
|
|
1041
|
-
};
|
|
1042
|
-
this._flowParticles.push(particle);
|
|
1043
|
-
}
|
|
1044
|
-
});
|
|
973
|
+
initFlowFeatures(this);
|
|
1045
974
|
}
|
|
1046
975
|
_renderSourcePoints(duration) {
|
|
1047
976
|
const { config } = this;
|
|
@@ -1113,29 +1042,11 @@ class TopoJSONMap extends ComponentCore {
|
|
|
1113
1042
|
}
|
|
1114
1043
|
}
|
|
1115
1044
|
_updateFlowParticles() {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
// Move particle along the arc path using progress
|
|
1121
|
-
particle.progress += particle.velocity * 0.01;
|
|
1122
|
-
if (particle.progress > 1)
|
|
1123
|
-
particle.progress = 0;
|
|
1124
|
-
// Use the stored SVG path and pathLength
|
|
1125
|
-
if (particle.arcPath && typeof particle.pathLength === 'number') {
|
|
1126
|
-
const tempPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
1127
|
-
tempPath.setAttribute('d', particle.arcPath);
|
|
1128
|
-
const pt = tempPath.getPointAtLength(particle.progress * particle.pathLength);
|
|
1129
|
-
particle.x = pt.x;
|
|
1130
|
-
particle.y = pt.y;
|
|
1131
|
-
}
|
|
1045
|
+
updateFlowParticles({
|
|
1046
|
+
_flowParticles: this._flowParticles,
|
|
1047
|
+
_currentZoomLevel: this._currentZoomLevel,
|
|
1048
|
+
_flowParticlesGroup: this._flowParticlesGroup,
|
|
1132
1049
|
});
|
|
1133
|
-
// Update DOM elements directly without data rebinding (for performance)
|
|
1134
|
-
this._flowParticlesGroup
|
|
1135
|
-
.selectAll(`.${flowParticle}`)
|
|
1136
|
-
.attr('cx', (d, i) => { var _a; return ((_a = this._flowParticles[i]) === null || _a === void 0 ? void 0 : _a.x) || 0; })
|
|
1137
|
-
.attr('cy', (d, i) => { var _a; return ((_a = this._flowParticles[i]) === null || _a === void 0 ? void 0 : _a.y) || 0; })
|
|
1138
|
-
.attr('r', (d, i) => { var _a; return (((_a = this._flowParticles[i]) === null || _a === void 0 ? void 0 : _a.radius) || 1) / zoomLevel; });
|
|
1139
1050
|
}
|
|
1140
1051
|
_onPointClick(d, event) {
|
|
1141
1052
|
var _a;
|
|
@@ -1253,6 +1164,7 @@ class TopoJSONMap extends ComponentCore {
|
|
|
1253
1164
|
this._renderPoints(config.duration / 2);
|
|
1254
1165
|
// Re-bind user-defined events to include newly created expanded cluster points
|
|
1255
1166
|
this._setUpComponentEventsThrottled();
|
|
1167
|
+
this._setCustomAttributesThrottled();
|
|
1256
1168
|
// Return the original point data for centroid calculation
|
|
1257
1169
|
return points.map(p => p.properties);
|
|
1258
1170
|
}
|
|
@@ -1314,10 +1226,11 @@ class TopoJSONMap extends ComponentCore {
|
|
|
1314
1226
|
this._collapsedClusterPointIds = expandedPointIds;
|
|
1315
1227
|
// Clean up all references to prevent memory leaks
|
|
1316
1228
|
(_a = this._expandedCluster.points) === null || _a === void 0 ? void 0 : _a.forEach((d) => {
|
|
1317
|
-
|
|
1318
|
-
delete
|
|
1319
|
-
delete
|
|
1320
|
-
delete
|
|
1229
|
+
const expandedPoint = d;
|
|
1230
|
+
delete expandedPoint.expandedClusterPoint;
|
|
1231
|
+
delete expandedPoint.clusterColor;
|
|
1232
|
+
delete expandedPoint.dx;
|
|
1233
|
+
delete expandedPoint.dy;
|
|
1321
1234
|
});
|
|
1322
1235
|
this._expandedCluster = null;
|
|
1323
1236
|
}
|