import { describe, it, expect } from 'vitest'; import { clusterMarkers } from './map-cluster'; import type { MapMarkerData } from './map-marker'; const at = (lat: number, lng: number, id: string): MapMarkerData => ({ id, position: { lat, lng }, }); describe('clusterMarkers', () => { it('passes markers through untouched when nothing is configured', () => { const markers = [at(-30, -51, 'a'), at(-29, -50, 'b')]; const result = clusterMarkers(markers, undefined); expect(result).toHaveLength(2); expect(result[0].marker).toBe(markers[0]); expect(result[0].memberIndices).toEqual([0]); }); it('groups by the supplied key and counts the members', () => { const markers = [ { ...at(-30, -51, 'a'), data: { city: 'POA' } }, { ...at(-30.1, -51.1, 'b'), data: { city: 'POA' } }, { ...at(-29, -50, 'c'), data: { city: 'Canoas' } }, ]; const result = clusterMarkers(markers, { by: marker => (marker.data as { city: string }).city, }); expect(result).toHaveLength(2); const poa = result.find(entry => entry.memberIndices.length === 2)!; expect(poa.marker.badge).toBe(2); expect(poa.marker.id).toBe('cluster:POA'); }); it('averages the position of a cluster', () => { const markers = [ { ...at(-30, -52, 'a'), data: { k: 'x' } }, { ...at(-32, -50, 'b'), data: { k: 'x' } }, ]; const [cluster] = clusterMarkers(markers, { by: m => (m.data as { k: string }).k }); expect(cluster.marker.position).toEqual({ lat: -31, lng: -51 }); }); it('marks a cluster position as a centroid, never as exact', () => { const markers = [ { ...at(-30, -52, 'a'), data: { k: 'x' }, precision: 'exact' as const }, { ...at(-32, -50, 'b'), data: { k: 'x' }, precision: 'exact' as const }, ]; const [cluster] = clusterMarkers(markers, { by: m => (m.data as { k: string }).k }); expect(cluster.marker.precision).toBe('centroid'); }); it('collapses stacked markers even with no key function', () => { const markers = [at(-30, -51, 'a'), at(-30, -51, 'b'), at(-29, -50, 'c')]; const result = clusterMarkers(markers, {}, 0); // Without `by` and without minAggregation this is a pass-through. expect(result).toHaveLength(3); }); describe('minAggregation', () => { const markers = [ { ...at(-30.0, -51.0, 'a'), data: { g: 'big' } }, { ...at(-30.1, -51.1, 'b'), data: { g: 'big' } }, { ...at(-30.2, -51.2, 'c'), data: { g: 'big' } }, { ...at(-30.3, -51.3, 'd'), data: { g: 'tiny' } }, ]; const by = (m: MapMarkerData) => (m.data as { g: string }).g; it('folds an under-sized group into the nearest larger one', () => { const result = clusterMarkers(markers, { by }, 3); // 'tiny' has one record and must not be plotted on its own. expect(result).toHaveLength(1); expect(result[0].memberIndices).toHaveLength(4); }); it('never drops records while anonymizing', () => { const result = clusterMarkers(markers, { by }, 3); const total = result.reduce((sum, entry) => sum + entry.memberIndices.length, 0); // Hiding records would silently misreport the totals. expect(total).toBe(markers.length); }); it('merges small groups together when none is large enough', () => { const scattered = [ { ...at(-30, -51, 'a'), data: { g: 'x' } }, { ...at(-29, -50, 'b'), data: { g: 'y' } }, ]; const result = clusterMarkers(scattered, { by }, 5); expect(result).toHaveLength(1); expect(result[0].marker.badge).toBe(2); }); it('leaves groups at or above the threshold alone', () => { const result = clusterMarkers(markers, { by }, 1); expect(result).toHaveLength(2); }); }); });