@@ -19,9 +19,9 @@ import { getCSSVariableValue, isStringCSSVariable } from 'utils/misc'
1919import { trimStringMiddle } from 'utils/text'
2020// Types
2121import { MapLink } from 'types/map'
22+ import { GenericDataRecord } from 'types/data'
2223import Supercluster from 'supercluster'
2324
24-
2525// Local Types
2626import {
2727 MapData ,
@@ -51,10 +51,13 @@ import {
5151 calculateClusterIndex ,
5252 getClustersAndPoints ,
5353 geoJsonPointToScreenPoint ,
54- getClusterRadius ,
5554 getNextZoomLevelOnClusterClick ,
55+ getClusterRadius ,
5656} from './utils'
5757import { updateDonut } from './modules/donut'
58+ import { renderBackground } from './modules/background'
59+ import { updateSelectionRing } from './modules/selectionRing'
60+ import { initFlowFeatures , updateFlowParticles , FlowInitContext , FlowUpdateContext } from './modules/flow'
5861
5962// Styles
6063import * as s from './style'
@@ -65,9 +68,9 @@ import * as s from './style'
6568const SUPERCLUSTER_MAX_ZOOM = 22
6669
6770export class TopoJSONMap <
68- AreaDatum ,
69- PointDatum = unknown ,
70- LinkDatum = unknown ,
71+ AreaDatum extends GenericDataRecord ,
72+ PointDatum extends GenericDataRecord = GenericDataRecord ,
73+ LinkDatum extends GenericDataRecord = GenericDataRecord ,
7174> extends ComponentCore <
7275 MapData < AreaDatum , PointDatum , LinkDatum > ,
7376 TopoJSONMapConfigInterface < AreaDatum , PointDatum , LinkDatum >
@@ -251,15 +254,10 @@ export class TopoJSONMap<
251254 }
252255
253256 _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- } )
257+ renderBackground ( this . _backgroundRect , {
258+ bleed : { left : this . bleed . left , top : this . bleed . top } ,
259+ onClick : ( ) => this . _collapseExpandedCluster ( ) ,
260+ } )
263261 }
264262
265263 _renderGroups ( duration : number ) : void {
@@ -905,30 +903,14 @@ export class TopoJSONMap<
905903 this . _pointsGroup . selectAll ( `.${ s . pointBottomLabel } ` ) . style ( 'display' , ( config . heatmapMode && ( this . _currentZoomLevel < config . heatmapModeZoomLevelThreshold ) ) ? 'none' : null )
906904
907905 // 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- }
906+ updateSelectionRing < PointDatum > (
907+ this . _pointSelectionRing ,
908+ this . _selectedPoint ,
909+ pointData ,
910+ this . config ,
911+ this . _projection ,
912+ currentZoomLevel
913+ )
932914 }
933915
934916 _fitToPoints ( points ?: PointDatum [ ] , pad = 0.1 ) : void {
@@ -1150,103 +1132,9 @@ export class TopoJSONMap<
11501132 }
11511133
11521134 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- } )
1135+ initFlowFeatures < AreaDatum , PointDatum , LinkDatum > (
1136+ this as unknown as FlowInitContext < AreaDatum , PointDatum , LinkDatum >
1137+ )
12501138 }
12511139
12521140 private _renderSourcePoints ( duration : number ) : void {
@@ -1328,31 +1216,11 @@ export class TopoJSONMap<
13281216 }
13291217
13301218 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 )
1219+ updateFlowParticles ( {
1220+ _flowParticles : this . _flowParticles as FlowParticle [ ] ,
1221+ _currentZoomLevel : this . _currentZoomLevel ,
1222+ _flowParticlesGroup : this . _flowParticlesGroup as any ,
1223+ } as FlowUpdateContext )
13561224 }
13571225
13581226 private _onPointClick ( d : TopoJSONMapPoint < PointDatum > , event : MouseEvent ) : void {
0 commit comments