Skip to content

Commit 5b71904

Browse files
committed
Component | TopoJSON: Move certain functions to modules
1 parent 5934eaa commit 5b71904

File tree

4 files changed

+270
-160
lines changed

4 files changed

+270
-160
lines changed

packages/ts/src/components/topojson-map/index.ts

Lines changed: 28 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import { getCSSVariableValue, isStringCSSVariable } from 'utils/misc'
1919
import { trimStringMiddle } from 'utils/text'
2020
// Types
2121
import { MapLink } from 'types/map'
22+
import { GenericDataRecord } from 'types/data'
2223
import Supercluster from 'supercluster'
2324

24-
2525
// Local Types
2626
import {
2727
MapData,
@@ -51,10 +51,13 @@ import {
5151
calculateClusterIndex,
5252
getClustersAndPoints,
5353
geoJsonPointToScreenPoint,
54-
getClusterRadius,
5554
getNextZoomLevelOnClusterClick,
55+
getClusterRadius,
5656
} from './utils'
5757
import { 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
6063
import * as s from './style'
@@ -65,9 +68,9 @@ import * as s from './style'
6568
const SUPERCLUSTER_MAX_ZOOM = 22
6669

6770
export 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 {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Selection } from 'd3-selection'
2+
3+
export interface BackgroundContext {
4+
bleed: { left?: number; top?: number };
5+
onClick: () => void;
6+
}
7+
8+
export function renderBackground (
9+
backgroundRect: Selection<SVGRectElement, unknown, null, undefined>,
10+
bgContext: BackgroundContext
11+
): void {
12+
backgroundRect
13+
.attr('width', '100%')
14+
.attr('height', '100%')
15+
.attr('transform', `translate(${-(bgContext.bleed.left ?? 0)}, ${-(bgContext.bleed.top ?? 0)})`)
16+
.style('cursor', 'default')
17+
.on('click', () => {
18+
bgContext.onClick()
19+
})
20+
}
21+

0 commit comments

Comments
 (0)