Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
77d13c7
Component | TopoJSON: Add support for point shapes
lee00678 Dec 8, 2025
f42dfcb
Component | TopoJSON: Update styling for point shape ring
lee00678 Jan 27, 2026
d23e00e
Component | TopoJSON: Add flow animation to topojson map
lee00678 Jan 29, 2026
324e78c
Dev | TopoJSON: Add link and flow example
lee00678 Jan 29, 2026
37b5868
Component | Map: Add area label option to TopoJSON map
lee00678 Dec 3, 2025
c7568fd
Dev | Example: Add example with area labels
lee00678 Dec 8, 2025
425184e
Component | TopoJSON: Add colliding labels to avoid overlap
lee00678 Feb 3, 2026
5f7f0f0
Component | TopoJSON: Remove PointDatum GenericDataRecord type
lee00678 Feb 4, 2026
903ce27
Component | TopoJSON: Refector collideAreaLabels
lee00678 Feb 4, 2026
d530d47
Component | TopoJSON: Deprecate PointLabelPosition, add PointBottomLabel
lee00678 Feb 5, 2026
3e8c165
Dev | TopoJSON: Fix point label scale issue
lee00678 Feb 5, 2026
c899aa2
Component | TopoJSONMap: Add clustering with color map visualization
suryahanumandla Feb 5, 2026
5b1b638
Dev | TopoJSON: Add example with radius animation and flow animation
lee00678 Feb 5, 2026
132b474
Dev | TopoJSON: Add malicious user demo
lee00678 Feb 5, 2026
fac55ec
Dev | Component: Add tooltip to examples
lee00678 Feb 5, 2026
17aa14b
Component | TopoJSON: Expose current zoom level to better support ani…
lee00678 Feb 9, 2026
defe4ee
Component | TopoJSON: Fix map label colors
suryahanumandla Feb 12, 2026
2d51517
Component | TopoJSON: Fix issue with cluster expanding
lee00678 Feb 13, 2026
21d1aa6
Component | TopoJSON: Remove colliding map area labels on manual zoom…
suryahanumandla Feb 13, 2026
982a81a
Component | TopoJSON: Fix zoomExtent with supercluster behavior
lee00678 Feb 18, 2026
f83a959
Component | TopoJSON: Update click to expend behavior, one click will…
lee00678 Feb 18, 2026
28d91eb
Component | TopoJSON: Add zoomToLocation as part of config file, expo…
lee00678 Feb 18, 2026
d6a22c8
Component | TopoJSON: Update cluster behavior, clicking on packed nod…
lee00678 Feb 18, 2026
aa53d87
Component | TopoJSON: Fix click expanded node group issue, add pointS…
lee00678 Feb 19, 2026
b1054b5
Component | TopoJSON: Fixed pointShape in cluster issue
lee00678 Feb 19, 2026
a4a6a86
Component | TopoJSON: Don't render pointbottomLabel on expandedCluster
lee00678 Feb 20, 2026
a231690
Component | TopoJSON: Add collide detection for pointBottomLabel
lee00678 Feb 20, 2026
9db59a7
Component | TopoJSON: Add support for fallback to GeoJson properties …
suryahanumandla Feb 24, 2026
bec79c2
Component | TopoJSONMap: Fix: apply pointCursor config to expanded cl…
suryahanumandla Feb 24, 2026
65f07af
Dev | Examples: Add examples for background and feature click in Topo…
suryahanumandla Feb 24, 2026
26d02c6
Misc: Vulnerability fix
lee00678 Feb 3, 2026
4ad9497
Component | TopoJSON: Fix zoom out behavior via visControl
lee00678 Feb 25, 2026
8af66b4
Component | TopoJSON: Update css for point shape and cluster hover ef…
lee00678 Feb 27, 2026
0c65c90
Component | TopoJSON: Fix clusterbottom label and margin
lee00678 Mar 2, 2026
b54b252
Component | TopoJSON: FitToPoint doesn't handle single point, now it …
lee00678 Mar 2, 2026
ff5d18c
Component | TopoJSON: Fix fitview issue when mapfittopoints is set to…
lee00678 Mar 3, 2026
31b3016
Component | TopoJSON: Fix as any types
lee00678 Mar 3, 2026
5934eaa
Component | TopoJSON: Update style.ts to the new css variabel definit…
lee00678 Mar 3, 2026
4e8afd3
Component | TopoJSON: Move certain functions to modules
lee00678 Mar 3, 2026
3199ffa
Component | TopoJSON: Remove type any
lee00678 Mar 9, 2026
e138629
Component | TopoJSON: Add smartTransition to area label
lee00678 Mar 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"@angular/compiler": "12 - 19",
"@angular/core": "12 - 19",
"rxjs": "^6.6.0 || ^7.5.0",
"d3-array": "3.2.4"
"d3-array": "3.2.4",
"d3-geo": "~3.1.1"
},
"devDependencies": {
"@unovis/ts": "workspace:*",
Expand Down
131 changes: 127 additions & 4 deletions packages/angular/src/components/topojson-map/topojson-map.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
ColorAccessor,
StringAccessor,
MapPointLabelPosition,
TopoJSONMapClusterDatum,
TopoJSONMapPointStyles,
} from '@unovis/ts'
import { GeoProjection } from 'd3-geo'
import { VisCoreComponent } from '../../core'
Expand Down Expand Up @@ -86,6 +88,16 @@ export class VisTopoJSONMapComponent<AreaDatum, PointDatum, LinkDatum> implement
/** Initial zoom level. Default: `undefined` */
@Input() zoomFactor?: number

/** Zoom to a specific location. When set, the map will zoom to the specified coordinates at the given zoom level.
* Format: `{ coordinates: [longitude, latitude], zoomLevel: number, expandCluster?: boolean }`
* When `expandCluster` is true, the cluster at or nearest to the coordinates will be expanded.
* Default: `undefined` */
@Input() zoomToLocation?: {
coordinates: [number, number];
zoomLevel: number;
expandCluster?: boolean;
}

/** Disable pan / zoom interactions. Default: `false` */
@Input() disableZoom?: boolean

Expand Down Expand Up @@ -113,6 +125,48 @@ export class VisTopoJSONMapComponent<AreaDatum, PointDatum, LinkDatum> implement
/** Link target accessor function. Default: `d => d.target` */
@Input() linkTarget?: ((l: LinkDatum) => number | string | PointDatum)

/** Flow source point longitude accessor function or value. Default: `f => f.sourceLongitude` */
@Input() sourceLongitude?: NumericAccessor<LinkDatum>

/** Flow source point latitude accessor function or value. Default: `f => f.sourceLatitude` */
@Input() sourceLatitude?: NumericAccessor<LinkDatum>

/** Flow target point longitude accessor function or value. Default: `f => f.targetLongitude` */
@Input() targetLongitude?: NumericAccessor<LinkDatum>

/** Flow target point latitude accessor function or value. Default: `f => f.targetLatitude` */
@Input() targetLatitude?: NumericAccessor<LinkDatum>

/** Flow source point radius accessor function or value. Default: `3` */
@Input() sourcePointRadius?: NumericAccessor<LinkDatum>

/** Source point color accessor function or value. Default: `'#88919f'` */
@Input() sourcePointColor?: ColorAccessor<LinkDatum>

/** Flow particle color accessor function or value. Default: `'#949dad'` */
@Input() flowParticleColor?: ColorAccessor<LinkDatum>

/** Flow particle radius accessor function or value. Default: `1.1` */
@Input() flowParticleRadius?: NumericAccessor<LinkDatum>

/** Flow particle speed accessor function or value. The unit is arbitrary, recommended range is 0 – 0.2. Default: `0.07` */
@Input() flowParticleSpeed?: NumericAccessor<LinkDatum>

/** Flow particle density accessor function or value on the range of [0, 1]. Default: `0.6` */
@Input() flowParticleDensity?: NumericAccessor<LinkDatum>

/** Enable flow animations. When true, shows animated particles along links. Default: `false` */
@Input() enableFlowAnimation?: boolean

/** Flow source point click callback function. Default: `undefined` */
@Input() onSourcePointClick?: (f: LinkDatum, x: number, y: number, event: MouseEvent) => void

/** Flow source point mouse over callback function. Default: `undefined` */
@Input() onSourcePointMouseEnter?: (f: LinkDatum, x: number, y: number, event: MouseEvent) => void

/** Flow source point mouse leave callback function. Default: `undefined` */
@Input() onSourcePointMouseLeave?: (f: LinkDatum, event: MouseEvent) => void

/** Area id accessor function corresponding to the feature id from TopoJSON. Default: `d => d.id ?? ''` */
@Input() areaId?: StringAccessor<AreaDatum>

Expand All @@ -122,6 +176,9 @@ export class VisTopoJSONMapComponent<AreaDatum, PointDatum, LinkDatum> implement
/** Area cursor value or accessor function. Default: `null` */
@Input() areaCursor?: StringAccessor<AreaDatum>

/** Area label accessor function. Default: `undefined` */
@Input() areaLabel?: StringAccessor<AreaDatum>

/** Point color accessor. Default: `d => d.color ?? null` */
@Input() pointColor?: ColorAccessor<PointDatum>

Expand All @@ -131,6 +188,12 @@ export class VisTopoJSONMapComponent<AreaDatum, PointDatum, LinkDatum> implement
/** Point stroke width accessor. Default: `d => d.strokeWidth ?? null` */
@Input() pointStrokeWidth?: NumericAccessor<PointDatum>

/** Point shape accessor. Default: `TopoJSONMapPointShape.Circle` */
@Input() pointShape?: StringAccessor<PointDatum>

/** Point ring width for ring-shaped points. Default: `2` */
@Input() pointRingWidth?: NumericAccessor<PointDatum>

/** Point cursor constant value or accessor function. Default: `null` */
@Input() pointCursor?: StringAccessor<PointDatum>

Expand All @@ -140,18 +203,57 @@ export class VisTopoJSONMapComponent<AreaDatum, PointDatum, LinkDatum> implement
/** Point latitude accessor function. Default: `d => d.latitude ?? null` */
@Input() latitude?: NumericAccessor<PointDatum>

/** Point label accessor function. Default: `undefined` */
/** Point inner label accessor function. Default: `undefined` */
@Input() pointLabel?: StringAccessor<PointDatum>

/** Point label position. Default: `Position.Bottom` */
/** Point inner label color accessor function or constant value.
* By default, the label color will be set, depending on the point brightness, either to
* `--vis-map-point-label-text-color-dark` or to `--vis-map-point-label-text-color-light` CSS variable.
* Default: `undefined` */
@Input() pointLabelColor?: ColorAccessor<PointDatum>

/** Point label position. Default: `MapPointLabelPosition.Center` */
@Input() pointLabelPosition?: MapPointLabelPosition

/** Point bottom label accessor function. Default: `undefined` */
@Input() pointBottomLabel?: StringAccessor<PointDatum>

/** Point color brightness ratio for switching between dark and light text label color. Default: `0.65` */
@Input() pointLabelTextBrightnessRatio?: number

/** Point id accessor function. Default: `d => d.id` */
@Input() pointId?: ((d: PointDatum, i: number) => string)

/** Cluster color accessor function or constant value. Default: `undefined` */
@Input() clusterColor?: ColorAccessor<TopoJSONMapClusterDatum<PointDatum>>

/** Cluster radius accessor function or constant value. Default: `undefined` */
@Input() clusterRadius?: NumericAccessor<TopoJSONMapClusterDatum<PointDatum>>

/** Cluster inner label accessor function. Default: `d => d.pointCount` */
@Input() clusterLabel?: StringAccessor<TopoJSONMapClusterDatum<PointDatum>>

/** Cluster inner label color accessor function or constant value. Default: `undefined` */
@Input() clusterLabelColor?: StringAccessor<TopoJSONMapClusterDatum<PointDatum>>

/** Cluster bottom label accessor function. Default: `''` */
@Input() clusterBottomLabel?: StringAccessor<TopoJSONMapClusterDatum<PointDatum>>

/** The width of the cluster point ring. Default: `2` */
@Input() clusterRingWidth?: number

/** When cluster is expanded, show a background circle to better separate points from the base map. Default: `true` */
@Input() clusterBackground?: boolean

/** Defines whether the cluster should expand on click or not. Default: `true` */
@Input() clusterExpandOnClick?: boolean

/** Clustering distance in pixels. Default: `55` */
@Input() clusteringDistance?: number

/** Enable point clustering. Default: `false` */
@Input() clustering?: boolean

/** Enables blur and blending between neighbouring points. Default: `false` */
@Input() heatmapMode?: boolean

Expand All @@ -160,6 +262,27 @@ export class VisTopoJSONMapComponent<AreaDatum, PointDatum, LinkDatum> implement

/** Zoom level at which the heatmap mode will be disabled. Default: `2.5` */
@Input() heatmapModeZoomLevelThreshold?: number

/** A single map point can have multiple properties displayed as a small pie chart.
* By setting the colorMap configuration you can specify data properties that should be mapped to various pie / donut segments.
*
* ```
* {
* [key in keyof PointDatum]?: { color: string, className?: string }
* }
* ```
* e.g.:
* ```
* {
* healthy: { color: 'green' },
* warning: { color: 'orange' },
* critical: { color: 'red' }
* }
* ```
* where every data point has the `healthy`, `warning` and `critical` numerical or boolean property.
* Note: Properties with 0 or falsy values will not be displayed in the pie chart.
* Default: `{}` */
@Input() colorMap?: TopoJSONMapPointStyles<PointDatum>
@Input() data: {areas?: AreaDatum[]; points?: PointDatum[]; links?: LinkDatum[]}

component: TopoJSONMap<AreaDatum, PointDatum, LinkDatum> | undefined
Expand All @@ -181,8 +304,8 @@ export class VisTopoJSONMapComponent<AreaDatum, PointDatum, LinkDatum> implement
}

private getConfig (): TopoJSONMapConfigInterface<AreaDatum, PointDatum, LinkDatum> {
const { duration, events, attributes, projection, topojson, mapFeatureName, mapFitToPoints, zoomFactor, disableZoom, zoomExtent, zoomDuration, linkWidth, linkColor, linkCursor, linkId, linkSource, linkTarget, areaId, areaColor, areaCursor, pointColor, pointRadius, pointStrokeWidth, pointCursor, longitude, latitude, pointLabel, pointLabelPosition, pointLabelTextBrightnessRatio, pointId, heatmapMode, heatmapModeBlurStdDeviation, heatmapModeZoomLevelThreshold } = this
const config = { duration, events, attributes, projection, topojson, mapFeatureName, mapFitToPoints, zoomFactor, disableZoom, zoomExtent, zoomDuration, linkWidth, linkColor, linkCursor, linkId, linkSource, linkTarget, areaId, areaColor, areaCursor, pointColor, pointRadius, pointStrokeWidth, pointCursor, longitude, latitude, pointLabel, pointLabelPosition, pointLabelTextBrightnessRatio, pointId, heatmapMode, heatmapModeBlurStdDeviation, heatmapModeZoomLevelThreshold }
const { duration, events, attributes, projection, topojson, mapFeatureName, mapFitToPoints, zoomFactor, zoomToLocation, disableZoom, zoomExtent, zoomDuration, linkWidth, linkColor, linkCursor, linkId, linkSource, linkTarget, sourceLongitude, sourceLatitude, targetLongitude, targetLatitude, sourcePointRadius, sourcePointColor, flowParticleColor, flowParticleRadius, flowParticleSpeed, flowParticleDensity, enableFlowAnimation, onSourcePointClick, onSourcePointMouseEnter, onSourcePointMouseLeave, areaId, areaColor, areaCursor, areaLabel, pointColor, pointRadius, pointStrokeWidth, pointShape, pointRingWidth, pointCursor, longitude, latitude, pointLabel, pointLabelColor, pointLabelPosition, pointBottomLabel, pointLabelTextBrightnessRatio, pointId, clusterColor, clusterRadius, clusterLabel, clusterLabelColor, clusterBottomLabel, clusterRingWidth, clusterBackground, clusterExpandOnClick, clusteringDistance, clustering, heatmapMode, heatmapModeBlurStdDeviation, heatmapModeZoomLevelThreshold, colorMap } = this
const config = { duration, events, attributes, projection, topojson, mapFeatureName, mapFitToPoints, zoomFactor, zoomToLocation, disableZoom, zoomExtent, zoomDuration, linkWidth, linkColor, linkCursor, linkId, linkSource, linkTarget, sourceLongitude, sourceLatitude, targetLongitude, targetLatitude, sourcePointRadius, sourcePointColor, flowParticleColor, flowParticleRadius, flowParticleSpeed, flowParticleDensity, enableFlowAnimation, onSourcePointClick, onSourcePointMouseEnter, onSourcePointMouseLeave, areaId, areaColor, areaCursor, areaLabel, pointColor, pointRadius, pointStrokeWidth, pointShape, pointRingWidth, pointCursor, longitude, latitude, pointLabel, pointLabelColor, pointLabelPosition, pointBottomLabel, pointLabelTextBrightnessRatio, pointId, clusterColor, clusterRadius, clusterLabel, clusterLabelColor, clusterBottomLabel, clusterRingWidth, clusterBackground, clusterExpandOnClick, clusteringDistance, clustering, heatmapMode, heatmapModeBlurStdDeviation, heatmapModeZoomLevelThreshold, colorMap }
const keys = Object.keys(config) as (keyof TopoJSONMapConfigInterface<AreaDatum, PointDatum, LinkDatum>)[]
keys.forEach(key => { if (config[key] === undefined) delete config[key] })

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react'
import { VisSingleContainer, VisTopoJSONMap } from '@unovis/react'
import { WorldMapTopoJSON } from '@unovis/ts/maps'

export const title = 'Clustered Color Map with Shapes'
export const subTitle = 'Points with clustering, color map, and different shapes'

export type DataRecord = {
id: string;
latitude: number;
longitude: number;
healthy: number;
warning: number;
critical: number;
shape?: string;
pointColor?: string;
}

// Generate more data points for better clustering demonstration
export const data: { points: DataRecord[] } = {
points: [
// New York area cluster
{ id: 'New York', latitude: 40.7128, longitude: -74.0060, healthy: 80, warning: 15, critical: 5, shape: 'circle', pointColor: '#1f77b4' },
{ id: 'Newark', latitude: 40.7357, longitude: -74.1724, healthy: 75, warning: 20, critical: 5, shape: 'circle', pointColor: '#1f77b4' },
{ id: 'Jersey City', latitude: 40.7282, longitude: -74.0776, healthy: 70, warning: 25, critical: 5, shape: 'circle', pointColor: '#1f77b4' },

// London area cluster
{ id: 'London', latitude: 51.5074, longitude: -0.1278, healthy: 70, warning: 20, critical: 10, shape: 'ring', pointColor: '#ff7f0e' },
{ id: 'Westminster', latitude: 51.3994, longitude: -0.2269, healthy: 65, warning: 25, critical: 10, shape: 'ring', pointColor: '#ff7f0e' },
{ id: 'Camden', latitude: 51.6290, longitude: -0.0255, healthy: 72, warning: 18, critical: 10, shape: 'ring', pointColor: '#ff7f0e' },

// Tokyo area cluster (spread out for visual distinction)
{ id: 'Tokyo', latitude: 35.6762, longitude: 139.6503, healthy: 90, warning: 8, critical: 2, shape: 'square', pointColor: '#2ca02c' },
{ id: 'Shibuya', latitude: 35.1598, longitude: 140.2006, healthy: 85, warning: 12, critical: 3, shape: 'square', pointColor: '#2ca02c' },
{ id: 'Shinjuku', latitude: 36.1895, longitude: 139.1917, healthy: 88, warning: 10, critical: 2, shape: 'square', pointColor: '#2ca02c' },

// Sydney area cluster (spread out for visual distinction)
{ id: 'Sydney', latitude: -33.8688, longitude: 151.2093, healthy: 65, warning: 25, critical: 10, shape: 'triangle', pointColor: '#d62728' },
{ id: 'Bondi', latitude: -34.5906, longitude: 151.8767, healthy: 60, warning: 30, critical: 10, shape: 'triangle', pointColor: '#d62728' },

// Isolated points
{ id: 'Paris', latitude: 48.8566, longitude: 2.3522, healthy: 75, warning: 18, critical: 7, shape: 'circle', pointColor: '#9467bd' },
{ id: 'Berlin', latitude: 52.5200, longitude: 13.4050, healthy: 82, warning: 15, critical: 3, shape: 'square', pointColor: '#8c564b' },
{ id: 'Cairo', latitude: 30.0444, longitude: 31.2357, healthy: 60, warning: 30, critical: 10, shape: 'triangle', pointColor: '#e377c2' },
],
}

export const component = (): React.ReactNode => {
const colorMap = {
healthy: { color: '#4CAF50', className: 'healthy' },
warning: { color: '#FF9800', className: 'warning' },
critical: { color: '#F44336', className: 'critical' },
}

return (
<VisSingleContainer data={data} height={'90vh'}>
<VisTopoJSONMap<any, DataRecord, any>
topojson={WorldMapTopoJSON}
pointRadius={15}
pointLabel={d => d.id}
pointColor={d => d.pointColor}
pointShape={d => d.shape}
pointRingWidth={4}
clustering={true}
clusteringDistance={40}
clusterColor={d => d?.clusterPoints?.[0]?.pointColor || '#2196F3'}
clusterRadius={15}
clusterExpandOnClick={true}
zoomExtent={[0.5, 12]}
/>
</VisSingleContainer>
)
}
Loading
Loading