@@ -19,8 +19,8 @@ import { getCSSVariableValue, isStringCSSVariable } from 'utils/misc'
1919import { trimStringMiddle } from 'utils/text'
2020// Types
2121import { MapLink } from 'types/map'
22- import Supercluster from 'supercluster '
23-
22+ import { GenericDataRecord } from 'types/data '
23+ import Supercluster , { PointFeature , ClusterFeature } from 'supercluster'
2424
2525// Local Types
2626import {
@@ -51,10 +51,14 @@ import {
5151 calculateClusterIndex ,
5252 getClustersAndPoints ,
5353 geoJsonPointToScreenPoint ,
54- getClusterRadius ,
5554 getNextZoomLevelOnClusterClick ,
55+ getClusterRadius ,
56+ PackedPoint ,
5657} from './utils'
5758import { updateDonut } from './modules/donut'
59+ import { renderBackground } from './modules/background'
60+ import { updateSelectionRing } from './modules/selectionRing'
61+ import { initFlowFeatures , updateFlowParticles , FlowInitContext , FlowUpdateContext } from './modules/flow'
5862
5963// Styles
6064import * as s from './style'
@@ -66,8 +70,8 @@ const SUPERCLUSTER_MAX_ZOOM = 22
6670
6771export class TopoJSONMap <
6872 AreaDatum ,
69- PointDatum = unknown ,
70- LinkDatum = unknown ,
73+ PointDatum = GenericDataRecord ,
74+ LinkDatum = GenericDataRecord ,
7175> extends ComponentCore <
7276 MapData < AreaDatum , PointDatum , LinkDatum > ,
7377 TopoJSONMapConfigInterface < AreaDatum , PointDatum , LinkDatum >
@@ -165,7 +169,7 @@ export class TopoJSONMap<
165169 } )
166170 const zoomExtent = this . config . zoomExtent
167171 const maxClusterZoomLevel = Array . isArray ( zoomExtent ) ? Math . min ( zoomExtent [ 1 ] , SUPERCLUSTER_MAX_ZOOM ) : 16
168- this . _clusterIndex = calculateClusterIndex ( dataValid as any , this . config as any , maxClusterZoomLevel ) as any
172+ this . _clusterIndex = calculateClusterIndex ( dataValid , this . config , maxClusterZoomLevel )
169173 } else {
170174 this . _clusterIndex = null
171175 }
@@ -251,15 +255,10 @@ export class TopoJSONMap<
251255 }
252256
253257 _renderBackground ( ) : void {
254- this . _backgroundRect
255- . attr ( 'width' , '100%' )
256- . attr ( 'height' , '100%' )
257- . attr ( 'transform' , `translate(${ - this . bleed . left } , ${ - this . bleed . top } )` )
258- . style ( 'cursor' , 'default' )
259- . on ( 'click' , ( ) => {
260- // Collapse expanded cluster when clicking on background
261- this . _collapseExpandedCluster ( )
262- } )
258+ renderBackground ( this . _backgroundRect , {
259+ bleed : { left : this . bleed . left , top : this . bleed . top } ,
260+ onClick : ( ) => this . _collapseExpandedCluster ( ) ,
261+ } )
263262 }
264263
265264 _renderGroups ( duration : number ) : void {
@@ -452,7 +451,7 @@ export class TopoJSONMap<
452451 const cluster = this . _expandedCluster . cluster
453452 const pos = this . _projection ( cluster . geometry . coordinates as [ number , number ] )
454453
455- const backgroundRadius = getClusterRadius ( this . _expandedCluster as any )
454+ const backgroundRadius = getClusterRadius ( this . _expandedCluster as { points : PackedPoint [ ] ; cluster : TopoJSONMapPoint < PointDatum > } )
456455 // Divide by zoom level since the group transform will scale it back up
457456 const adjustedRadius = backgroundRadius / currentZoomLevel
458457
@@ -551,7 +550,7 @@ export class TopoJSONMap<
551550 // Supercluster expects zoom 0-22; beyond that, all points are unclustered anyway.
552551 const mapZoom = this . _currentZoomLevel || 1
553552 const zoom = Math . max ( 0 , Math . min ( Math . round ( mapZoom ) , SUPERCLUSTER_MAX_ZOOM ) )
554- let geoJsonPoints = getClustersAndPoints ( this . _clusterIndex as any , bounds , zoom )
553+ let geoJsonPoints = getClustersAndPoints ( this . _clusterIndex ! , bounds , zoom )
555554
556555 // Handle expanded cluster points - replace the expanded cluster with individual points
557556 if ( this . _expandedCluster ) {
@@ -594,8 +593,14 @@ export class TopoJSONMap<
594593 }
595594
596595 return geoJsonPoints . map ( ( geoPoint , i ) =>
597- geoJsonPointToScreenPoint ( geoPoint as any , i , this . _projection , this . config as any , this . _currentZoomLevel || 1 )
598- ) as any
596+ geoJsonPointToScreenPoint (
597+ geoPoint as ClusterFeature < TopoJSONMapClusterDatum < PointDatum > > | PointFeature < TopoJSONMapPointDatum < PointDatum > > ,
598+ i ,
599+ this . _projection ,
600+ this . config ,
601+ this . _currentZoomLevel || 1
602+ )
603+ )
599604 }
600605
601606 _renderPoints ( duration : number ) : void {
@@ -905,30 +910,14 @@ export class TopoJSONMap<
905910 this . _pointsGroup . selectAll ( `.${ s . pointBottomLabel } ` ) . style ( 'display' , ( config . heatmapMode && ( this . _currentZoomLevel < config . heatmapModeZoomLevelThreshold ) ) ? 'none' : null )
906911
907912 // Update selection ring
908- const pointSelection = this . _pointSelectionRing . select ( `.${ s . pointSelection } ` )
909- if ( this . _selectedPoint ) {
910- const selectedPointId = getString ( this . _selectedPoint . properties as PointDatum , config . pointId )
911- const foundPoint = pointData . find ( d =>
912- this . _selectedPoint . isCluster
913- ? ( d . id === this . _selectedPoint . id )
914- : ( selectedPointId && getString ( d . properties as PointDatum , config . pointId ) === selectedPointId )
915- )
916- const pos = this . _projection ( ( foundPoint ?? this . _selectedPoint ) . geometry . coordinates as [ number , number ] )
917- if ( pos ) {
918- const dx = ( ( foundPoint as any ) ?. dx || 0 ) / currentZoomLevel
919- const dy = ( ( foundPoint as any ) ?. dy || 0 ) / currentZoomLevel
920- this . _pointSelectionRing . attr ( 'transform' , `translate(${ pos [ 0 ] + dx } ,${ pos [ 1 ] + dy } )` )
921- }
922- pointSelection
923- . classed ( 'active' , Boolean ( foundPoint ) )
924- . attr ( 'd' , foundPoint ?. path || null )
925- . style ( 'fill' , 'transparent' )
926- . style ( 'stroke-width' , 1 )
927- . style ( 'stroke' , ( foundPoint || this . _selectedPoint ) ?. color )
928- . style ( 'transform' , `scale(${ 1.25 / currentZoomLevel } )` )
929- } else {
930- pointSelection . classed ( 'active' , false )
931- }
913+ updateSelectionRing < PointDatum > (
914+ this . _pointSelectionRing ,
915+ this . _selectedPoint ,
916+ pointData ,
917+ this . config ,
918+ this . _projection ,
919+ currentZoomLevel
920+ )
932921 }
933922
934923 _fitToPoints ( points ?: PointDatum [ ] , pad = 0.1 ) : void {
@@ -1150,103 +1139,9 @@ export class TopoJSONMap<
11501139 }
11511140
11521141 private _initFlowFeatures ( ) : void {
1153- const { config, datamodel } = this
1154- // Use raw links data instead of processed links to avoid point lookup issues for flows
1155- const rawLinks = datamodel . data ?. links || [ ]
1156-
1157- // Clear existing flow data
1158- this . _flowParticles = [ ]
1159- this . _sourcePoints = [ ]
1160-
1161- if ( ! rawLinks || rawLinks . length === 0 ) return
1162-
1163- // Create source points and flow particles for each link
1164- rawLinks . forEach ( ( link , i ) => {
1165- // Try to get coordinates from flow-specific accessors first, then fall back to link endpoints
1166- let sourceLon : number , sourceLat : number , targetLon : number , targetLat : number
1167-
1168- if ( config . sourceLongitude && config . sourceLatitude ) {
1169- sourceLon = getNumber ( link , config . sourceLongitude )
1170- sourceLat = getNumber ( link , config . sourceLatitude )
1171- } else {
1172- // Fall back to using linkSource point coordinates
1173- const sourcePoint = config . linkSource ?.( link )
1174- if ( typeof sourcePoint === 'object' && sourcePoint !== null ) {
1175- sourceLon = getNumber ( sourcePoint as PointDatum , config . longitude )
1176- sourceLat = getNumber ( sourcePoint as PointDatum , config . latitude )
1177- } else {
1178- return // Skip if can't resolve source coordinates
1179- }
1180- }
1181-
1182- if ( config . targetLongitude && config . targetLatitude ) {
1183- targetLon = getNumber ( link , config . targetLongitude )
1184- targetLat = getNumber ( link , config . targetLatitude )
1185- } else {
1186- // Fall back to using linkTarget point coordinates
1187- const targetPoint = config . linkTarget ?.( link )
1188- if ( typeof targetPoint === 'object' && targetPoint !== null ) {
1189- targetLon = getNumber ( targetPoint as PointDatum , config . longitude )
1190- targetLat = getNumber ( targetPoint as PointDatum , config . latitude )
1191- } else {
1192- return // Skip if can't resolve target coordinates
1193- }
1194- }
1195-
1196- if ( ! isNumber ( sourceLon ) || ! isNumber ( sourceLat ) || ! isNumber ( targetLon ) || ! isNumber ( targetLat ) ) {
1197- return
1198- }
1199- // Create source point
1200- const sourcePos = this . _projection ( [ sourceLon , sourceLat ] )
1201- if ( sourcePos ) {
1202- const sourcePoint = {
1203- lat : sourceLat ,
1204- lon : sourceLon ,
1205- x : sourcePos [ 0 ] ,
1206- y : sourcePos [ 1 ] ,
1207- radius : getNumber ( link , config . sourcePointRadius ) ,
1208- color : getColor ( link , config . sourcePointColor , i ) ,
1209- flowData : link ,
1210- }
1211- this . _sourcePoints . push ( sourcePoint )
1212- }
1213-
1214- // Use the same arc as _renderLinks for flow animation
1215- const sourceProj = this . _projection ( [ sourceLon , sourceLat ] )
1216- const targetProj = this . _projection ( [ targetLon , targetLat ] )
1217- if ( ! sourceProj || ! targetProj ) return
1218-
1219- // Generate SVG arc path string using the same arc() function
1220- const arcPath = arc ( sourceProj , targetProj )
1221- // Create a temporary SVG path element for sampling
1222- const tempPath = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'path' )
1223- tempPath . setAttribute ( 'd' , arcPath )
1224- const pathLength = tempPath . getTotalLength ( )
1225-
1226- const dist = Math . sqrt ( ( targetLat - sourceLat ) ** 2 + ( targetLon - sourceLon ) ** 2 )
1227- const numParticles = Math . max ( 1 , Math . round ( dist * getNumber ( link , config . flowParticleDensity ) ) )
1228- const velocity = getNumber ( link , config . flowParticleSpeed )
1229- const radius = getNumber ( link , config . flowParticleRadius )
1230- const color = getColor ( link , config . flowParticleColor , i )
1231-
1232- for ( let j = 0 ; j < numParticles ; j += 1 ) {
1233- const progress = j / numParticles
1234- const pt = tempPath . getPointAtLength ( progress * pathLength )
1235- const particle : FlowParticle = {
1236- x : pt . x ,
1237- y : pt . y ,
1238- velocity,
1239- radius,
1240- color,
1241- progress,
1242- arcPath,
1243- pathLength,
1244- id : `${ getString ( link , config . linkId , i ) || i } -${ j } ` ,
1245- flowData : undefined ,
1246- }
1247- this . _flowParticles . push ( particle )
1248- }
1249- } )
1142+ initFlowFeatures < PointDatum , LinkDatum > (
1143+ this as unknown as FlowInitContext < PointDatum , LinkDatum >
1144+ )
12501145 }
12511146
12521147 private _renderSourcePoints ( duration : number ) : void {
@@ -1328,31 +1223,11 @@ export class TopoJSONMap<
13281223 }
13291224
13301225 private _updateFlowParticles ( ) : void {
1331- if ( this . _flowParticles . length === 0 ) return
1332-
1333- const zoomLevel = this . _currentZoomLevel || 1
1334-
1335- this . _flowParticles . forEach ( particle => {
1336- // Move particle along the arc path using progress
1337- particle . progress += particle . velocity * 0.01
1338- if ( particle . progress > 1 ) particle . progress = 0
1339-
1340- // Use the stored SVG path and pathLength
1341- if ( particle . arcPath && typeof particle . pathLength === 'number' ) {
1342- const tempPath = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'path' )
1343- tempPath . setAttribute ( 'd' , particle . arcPath )
1344- const pt = tempPath . getPointAtLength ( particle . progress * particle . pathLength )
1345- particle . x = pt . x
1346- particle . y = pt . y
1347- }
1348- } )
1349-
1350- // Update DOM elements directly without data rebinding (for performance)
1351- this . _flowParticlesGroup
1352- . selectAll < SVGCircleElement , any > ( `.${ s . flowParticle } ` )
1353- . attr ( 'cx' , ( d , i ) => this . _flowParticles [ i ] ?. x || 0 )
1354- . attr ( 'cy' , ( d , i ) => this . _flowParticles [ i ] ?. y || 0 )
1355- . attr ( 'r' , ( d , i ) => ( this . _flowParticles [ i ] ?. radius || 1 ) / zoomLevel )
1226+ updateFlowParticles ( {
1227+ _flowParticles : this . _flowParticles as FlowParticle [ ] ,
1228+ _currentZoomLevel : this . _currentZoomLevel ,
1229+ _flowParticlesGroup : this . _flowParticlesGroup ,
1230+ } as FlowUpdateContext )
13561231 }
13571232
13581233 private _onPointClick ( d : TopoJSONMapPoint < PointDatum > , event : MouseEvent ) : void {
@@ -1437,7 +1312,7 @@ export class TopoJSONMap<
14371312 return {
14381313 x : null as number | null ,
14391314 y : null as number | null ,
1440- r : getPointRadius ( point as any , config . pointRadius as any , packingZoomLevel ) + padding ,
1315+ r : getPointRadius ( point as PointFeature < TopoJSONMapPointDatum < PointDatum > > , config . pointRadius , packingZoomLevel ) + padding ,
14411316 }
14421317 } )
14431318 packSiblings ( packPoints )
@@ -1450,7 +1325,7 @@ export class TopoJSONMap<
14501325 // Don't show pie charts for expanded cluster points (similar to Leaflet map)
14511326 const donutData : TopoJSONMapPieDatum [ ] = [ ]
14521327 // Use each point's own color (from colorMap or pointColor) so shapes keep their correct color when expanded
1453- const explicitPointColor = getColor ( originalData , config . pointColor as any )
1328+ const explicitPointColor = getColor ( originalData , config . pointColor )
14541329 const pointDonutData = getDonutData ( originalData , config . colorMap )
14551330 const maxVal = pointDonutData . length ? Math . max ( ...pointDonutData . map ( d => d . value ) ) : 0
14561331 const biggestDatum = pointDonutData . find ( d => d . value === maxVal ) || pointDonutData [ 0 ]
0 commit comments