@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.
@@ -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 = unknown, LinkDatum = unknown> extends ComponentCore<MapData<AreaDatum, PointDatum, LinkDatum>, TopoJSONMapConfigInterface<AreaDatum, PointDatum, LinkDatum>> {
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<any, any>): void;
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.setZoom(config.zoomFactor || 1);
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(--vis-map-point-label-font-size) / ${this._currentZoomLevel})`)
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', 'var(--vis-map-cluster-expanded-background-fill-color)')
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 isExpandedCluster = c.properties.cluster && c.properties.clusterId === expandedClusterId;
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((p) => p.id));
431
- geoJsonPoints = geoJsonPoints.filter((c) => !this._shouldFilterPointOrCluster(c, expandedPointIds));
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((c) => c.properties.cluster && c.properties.clusterId === collapsedClusterId);
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((c) => !this._shouldFilterPointOrCluster(c, this._collapsedClusterPointIds));
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
- d._zIndex = d.expandedClusterPoint ? 2 : 0;
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 || 0) / currentZoomLevel;
475
- const dy = (expandedPoint.dy || 0) / currentZoomLevel;
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 || 0) / currentZoomLevel;
535
- const dy = (expandedPoint.dy || 0) / currentZoomLevel;
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(--vis-map-point-label-font-size) / ${currentZoomLevel})`;
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 ? 'var(--vis-map-point-label-text-color-dark)' : 'var(--vis-map-point-label-text-color-light)';
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 ? 'var(--vis-map-point-label-text-color-dark)' : 'var(--vis-map-point-label-text-color-light)';
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(--vis-map-point-bottom-label-font-size, 10px) / ${this._currentZoomLevel})`);
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
- const pointSelection$1 = this._pointSelectionRing.select(`.${pointSelection}`);
742
- if (this._selectedPoint) {
743
- const selectedPointId = getString(this._selectedPoint.properties, config.pointId);
744
- const foundPoint = pointData.find(d => this._selectedPoint.isCluster
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
- this._center = [
823
- this._projection.translate()[0] * this._center[0] / prevTranslate[0],
824
- this._projection.translate()[1] * this._center[1] / prevTranslate[1],
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 !== undefined;
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
- this._projection.fitExtent([[0, 0], [this._width, this._height]], this._featureCollection);
940
- this._currentZoomLevel = (((_a = this._projection) === null || _a === void 0 ? void 0 : _a.scale()) / this._initialScale) || 1;
941
- // Set CSS custom property for zoom level
942
- (_c = (_b = this.g.node()) === null || _b === void 0 ? void 0 : _b.parentElement) === null || _c === void 0 ? void 0 : _c.style.setProperty('--vis-map-current-zoom-level', String(this._currentZoomLevel));
943
- this._center = this._projection.translate();
944
- // We are using this._applyZoom() instead of directly calling this._render(config.zoomDuration) because
945
- // we've to "attach" new transform to the map group element. Otherwise zoomBehavior will not know
946
- // that the zoom state has changed
947
- this._applyZoom();
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
- var _a;
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
- if (this._flowParticles.length === 0)
1117
- return;
1118
- const zoomLevel = this._currentZoomLevel || 1;
1119
- this._flowParticles.forEach(particle => {
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
- delete d.expandedClusterPoint;
1318
- delete d.clusterColor;
1319
- delete d.dx;
1320
- delete d.dy;
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
  }