@smartnet360/svelte-components 0.0.93 → 0.0.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -153,6 +153,46 @@
153
153
  />
154
154
  </div>
155
155
  </div>
156
+
157
+ <!-- Grid-Based Outlier Capping -->
158
+ <div class="row align-items-center g-2 mb-3 ps-3">
159
+ <div class="col-4 text-secondary small">Grid Capping</div>
160
+ <div class="col-8">
161
+ <div class="form-check form-switch">
162
+ <input
163
+ class="form-check-input"
164
+ type="checkbox"
165
+ id="gridCappingToggle"
166
+ bind:checked={displayStore.useGridCapping}
167
+ title="Remove outliers using grid-based percentile capping"
168
+ />
169
+ <label class="form-check-label small" for="gridCappingToggle">
170
+ Remove outliers
171
+ </label>
172
+ </div>
173
+ </div>
174
+ </div>
175
+
176
+ {#if displayStore.useGridCapping}
177
+ <!-- Grid Cap Percentile -->
178
+ <div class="row align-items-center g-2 mb-3 ps-3">
179
+ <div class="col-4 text-secondary small">Cap Percentile</div>
180
+ <div class="col-3 text-end">
181
+ <span class="badge bg-white text-muted border">{displayStore.gridCapPercentile}th</span>
182
+ </div>
183
+ <div class="col-5">
184
+ <input
185
+ type="range"
186
+ class="form-range w-100"
187
+ min="70"
188
+ max="95"
189
+ step="5"
190
+ bind:value={displayStore.gridCapPercentile}
191
+ title="Percentile threshold for capping outliers within each grid"
192
+ />
193
+ </div>
194
+ </div>
195
+ {/if}
156
196
  {/if}
157
197
 
158
198
  <div class="border-top my-3"></div>
@@ -136,6 +136,9 @@
136
136
  const _autoSizeMode = displayStore.autoSizeMode;
137
137
  const _autoSizeNeighborCount = displayStore.autoSizeNeighborCount;
138
138
  const _autoSizeMultiplier = displayStore.autoSizeMultiplier;
139
+ const _useGridCapping = displayStore.useGridCapping;
140
+ const _gridCapPercentile = displayStore.gridCapPercentile;
141
+ const _gridSizeKm = displayStore.gridSizeKm;
139
142
 
140
143
  updateLayer();
141
144
  });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Grid-Based Density Calculation
3
+ *
4
+ * Divides map into fixed-size grids and computes density-based size caps
5
+ * Used to remove outliers and create visual uniformity within local areas
6
+ */
7
+ import type { Cell } from '../types';
8
+ /**
9
+ * Convert geographic coordinates to grid key
10
+ * @param lon Longitude
11
+ * @param lat Latitude
12
+ * @param gridSizeKm Size of grid cell in kilometers (default: 1km)
13
+ * @returns Grid key string "x_y"
14
+ */
15
+ export declare function getGridKey(lon: number, lat: number, gridSizeKm?: number): string;
16
+ /**
17
+ * Group cells by grid
18
+ * @param cells Array of cells
19
+ * @param gridSizeKm Grid size in kilometers
20
+ * @returns Map of gridKey -> cells in that grid
21
+ */
22
+ export declare function groupCellsByGrid(cells: Cell[], gridSizeKm?: number): Map<string, Cell[]>;
23
+ /**
24
+ * Calculate percentile value from array
25
+ * @param values Sorted array of numbers
26
+ * @param percentile Percentile to calculate (0-100, e.g., 80 for 80th percentile)
27
+ * @returns Value at the given percentile
28
+ */
29
+ export declare function calculatePercentile(values: number[], percentile: number): number;
30
+ /**
31
+ * Compute grid-based size caps using percentile
32
+ * @param cells Array of cells
33
+ * @param sizeMap Map of siteId -> calculated size
34
+ * @param percentile Percentile threshold (0-100, e.g., 80)
35
+ * @param gridSizeKm Grid size in kilometers
36
+ * @param minCellsForCapping Minimum cells in grid to apply capping (default: 3)
37
+ * @returns Map of siteId -> capped size
38
+ */
39
+ export declare function computeGridBasedSizeCaps(cells: Cell[], sizeMap: Map<string, number>, percentile: number, gridSizeKm?: number, minCellsForCapping?: number): Map<string, number>;
40
+ /**
41
+ * Get statistics about grid-based capping
42
+ * @param originalSizes Map of original sizes
43
+ * @param cappedSizes Map of capped sizes
44
+ * @returns Object with statistics
45
+ */
46
+ export declare function getGridCappingStats(originalSizes: Map<string, number>, cappedSizes: Map<string, number>): {
47
+ totalSites: number;
48
+ cappedSites: number;
49
+ cappedPercentage: number;
50
+ avgReduction: number;
51
+ };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Grid-Based Density Calculation
3
+ *
4
+ * Divides map into fixed-size grids and computes density-based size caps
5
+ * Used to remove outliers and create visual uniformity within local areas
6
+ */
7
+ /**
8
+ * Convert geographic coordinates to grid key
9
+ * @param lon Longitude
10
+ * @param lat Latitude
11
+ * @param gridSizeKm Size of grid cell in kilometers (default: 1km)
12
+ * @returns Grid key string "x_y"
13
+ */
14
+ export function getGridKey(lon, lat, gridSizeKm = 1) {
15
+ // Approximate: 1 degree ≈ 111km at equator
16
+ // This is rough but sufficient for grouping
17
+ const degreesPerGrid = gridSizeKm / 111;
18
+ const gridX = Math.floor(lon / degreesPerGrid);
19
+ const gridY = Math.floor(lat / degreesPerGrid);
20
+ return `${gridX}_${gridY}`;
21
+ }
22
+ /**
23
+ * Group cells by grid
24
+ * @param cells Array of cells
25
+ * @param gridSizeKm Grid size in kilometers
26
+ * @returns Map of gridKey -> cells in that grid
27
+ */
28
+ export function groupCellsByGrid(cells, gridSizeKm = 1) {
29
+ const grids = new Map();
30
+ for (const cell of cells) {
31
+ const gridKey = getGridKey(cell.longitude, cell.latitude, gridSizeKm);
32
+ if (!grids.has(gridKey)) {
33
+ grids.set(gridKey, []);
34
+ }
35
+ grids.get(gridKey).push(cell);
36
+ }
37
+ return grids;
38
+ }
39
+ /**
40
+ * Calculate percentile value from array
41
+ * @param values Sorted array of numbers
42
+ * @param percentile Percentile to calculate (0-100, e.g., 80 for 80th percentile)
43
+ * @returns Value at the given percentile
44
+ */
45
+ export function calculatePercentile(values, percentile) {
46
+ if (values.length === 0)
47
+ return Infinity;
48
+ if (values.length === 1)
49
+ return values[0];
50
+ const sorted = [...values].sort((a, b) => a - b);
51
+ const index = (percentile / 100) * (sorted.length - 1);
52
+ const lower = Math.floor(index);
53
+ const upper = Math.ceil(index);
54
+ const weight = index % 1;
55
+ if (lower === upper) {
56
+ return sorted[lower];
57
+ }
58
+ return sorted[lower] * (1 - weight) + sorted[upper] * weight;
59
+ }
60
+ /**
61
+ * Compute grid-based size caps using percentile
62
+ * @param cells Array of cells
63
+ * @param sizeMap Map of siteId -> calculated size
64
+ * @param percentile Percentile threshold (0-100, e.g., 80)
65
+ * @param gridSizeKm Grid size in kilometers
66
+ * @param minCellsForCapping Minimum cells in grid to apply capping (default: 3)
67
+ * @returns Map of siteId -> capped size
68
+ */
69
+ export function computeGridBasedSizeCaps(cells, sizeMap, percentile, gridSizeKm = 1, minCellsForCapping = 3) {
70
+ // Group cells by grid
71
+ const grids = groupCellsByGrid(cells, gridSizeKm);
72
+ // Calculate percentile cap for each grid
73
+ const gridCaps = new Map();
74
+ for (const [gridKey, gridCells] of grids) {
75
+ // Get sizes for all sites in this grid
76
+ const siteSizes = new Map();
77
+ for (const cell of gridCells) {
78
+ const size = sizeMap.get(cell.siteId);
79
+ if (size !== undefined) {
80
+ siteSizes.set(cell.siteId, size);
81
+ }
82
+ }
83
+ const uniqueSizes = Array.from(siteSizes.values());
84
+ // Only apply capping if grid has enough cells
85
+ if (uniqueSizes.length >= minCellsForCapping) {
86
+ const cap = calculatePercentile(uniqueSizes, percentile);
87
+ gridCaps.set(gridKey, cap);
88
+ }
89
+ else {
90
+ // For small grids, use max size (no capping)
91
+ gridCaps.set(gridKey, Math.max(...uniqueSizes, 0));
92
+ }
93
+ }
94
+ // Apply caps to each site
95
+ const cappedSizes = new Map();
96
+ const sitesProcessed = new Set();
97
+ for (const cell of cells) {
98
+ // Only process each site once
99
+ if (sitesProcessed.has(cell.siteId))
100
+ continue;
101
+ sitesProcessed.add(cell.siteId);
102
+ const originalSize = sizeMap.get(cell.siteId);
103
+ if (originalSize === undefined)
104
+ continue;
105
+ const gridKey = getGridKey(cell.longitude, cell.latitude, gridSizeKm);
106
+ const cap = gridCaps.get(gridKey) ?? originalSize;
107
+ // Apply cap (take minimum)
108
+ cappedSizes.set(cell.siteId, Math.min(originalSize, cap));
109
+ }
110
+ return cappedSizes;
111
+ }
112
+ /**
113
+ * Get statistics about grid-based capping
114
+ * @param originalSizes Map of original sizes
115
+ * @param cappedSizes Map of capped sizes
116
+ * @returns Object with statistics
117
+ */
118
+ export function getGridCappingStats(originalSizes, cappedSizes) {
119
+ let totalSites = 0;
120
+ let cappedSites = 0;
121
+ let totalReduction = 0;
122
+ for (const [siteId, originalSize] of originalSizes) {
123
+ const cappedSize = cappedSizes.get(siteId);
124
+ if (cappedSize === undefined)
125
+ continue;
126
+ totalSites++;
127
+ if (cappedSize < originalSize) {
128
+ cappedSites++;
129
+ totalReduction += originalSize - cappedSize;
130
+ }
131
+ }
132
+ return {
133
+ totalSites,
134
+ cappedSites,
135
+ cappedPercentage: totalSites > 0 ? (cappedSites / totalSites) * 100 : 0,
136
+ avgReduction: cappedSites > 0 ? totalReduction / cappedSites : 0
137
+ };
138
+ }
@@ -13,9 +13,12 @@ export class CellDataStore {
13
13
  setCells(cells) {
14
14
  this.rawCells = cells;
15
15
  // Automatically update site distances when cells are loaded
16
- // Use neighborCount from displayStore if available
16
+ // Pass auto-size and grid capping settings from displayStore
17
17
  const neighborCount = this.displayStore?.autoSizeNeighborCount ?? 5;
18
- this.siteDistanceStore.updateDistances(cells, neighborCount);
18
+ const useGridCapping = this.displayStore?.useGridCapping ?? true;
19
+ const gridCapPercentile = this.displayStore?.gridCapPercentile ?? 80;
20
+ const gridSizeKm = this.displayStore?.gridSizeKm ?? 1;
21
+ this.siteDistanceStore.updateDistances(cells, neighborCount, useGridCapping, gridCapPercentile, gridSizeKm);
19
22
  }
20
23
  get filteredCells() {
21
24
  if (!this.filterOnAir)
@@ -11,6 +11,9 @@ export declare class CellDisplayStore {
11
11
  autoSizeMode: AutoSizeMode;
12
12
  autoSizeNeighborCount: number;
13
13
  autoSizeMultiplier: number;
14
+ useGridCapping: boolean;
15
+ gridCapPercentile: number;
16
+ gridSizeKm: number;
14
17
  level1: CellGroupingField;
15
18
  level2: CellGroupingField;
16
19
  currentZIndex: Record<string, number>;
@@ -13,6 +13,10 @@ export class CellDisplayStore {
13
13
  autoSizeMode = $state('logarithmic');
14
14
  autoSizeNeighborCount = $state(5); // Number of neighbors to average for density
15
15
  autoSizeMultiplier = $state(1.0); // Scale factor for auto-size results (0.3 - 2.0)
16
+ // Grid-based outlier capping
17
+ useGridCapping = $state(true); // Enable grid-based outlier removal
18
+ gridCapPercentile = $state(80); // Percentile for capping (70-95)
19
+ gridSizeKm = $state(1); // Grid size in kilometers
16
20
  // Grouping
17
21
  level1 = $state('tech');
18
22
  level2 = $state('fband');
@@ -44,6 +48,9 @@ export class CellDisplayStore {
44
48
  this.autoSizeMode = parsed.autoSizeMode ?? 'logarithmic';
45
49
  this.autoSizeNeighborCount = parsed.autoSizeNeighborCount ?? 5;
46
50
  this.autoSizeMultiplier = parsed.autoSizeMultiplier ?? 1.0;
51
+ this.useGridCapping = parsed.useGridCapping ?? true;
52
+ this.gridCapPercentile = parsed.gridCapPercentile ?? 80;
53
+ this.gridSizeKm = parsed.gridSizeKm ?? 1;
47
54
  this.level1 = parsed.level1 ?? 'tech';
48
55
  this.level2 = parsed.level2 ?? 'fband';
49
56
  this.labelPixelDistance = parsed.labelPixelDistance ?? 60;
@@ -70,6 +77,9 @@ export class CellDisplayStore {
70
77
  autoSizeMode: this.autoSizeMode,
71
78
  autoSizeNeighborCount: this.autoSizeNeighborCount,
72
79
  autoSizeMultiplier: this.autoSizeMultiplier,
80
+ useGridCapping: this.useGridCapping,
81
+ gridCapPercentile: this.gridCapPercentile,
82
+ gridSizeKm: this.gridSizeKm,
73
83
  level1: this.level1,
74
84
  level2: this.level2,
75
85
  labelPixelDistance: this.labelPixelDistance,
@@ -8,6 +8,7 @@ import type { Cell } from '../types';
8
8
  export declare class SiteDistanceStore {
9
9
  key: string;
10
10
  distances: Map<string, number>;
11
+ cappedSizes: Map<string, number>;
11
12
  computedSites: Set<string>;
12
13
  lastComputeTime: number;
13
14
  computedCount: number;
@@ -17,14 +18,18 @@ export declare class SiteDistanceStore {
17
18
  * Only computes distances for sites not in cache
18
19
  * @param cells Array of cells to compute distances for
19
20
  * @param neighborCount Number of neighbors to average for density calculation
21
+ * @param useGridCapping Whether to apply grid-based outlier capping
22
+ * @param gridCapPercentile Percentile for grid capping (70-95)
23
+ * @param gridSizeKm Grid size in kilometers
20
24
  */
21
- updateDistances(cells: Cell[], neighborCount?: number): void;
25
+ updateDistances(cells: Cell[], neighborCount?: number, useGridCapping?: boolean, gridCapPercentile?: number, gridSizeKm?: number): void;
22
26
  /**
23
27
  * Update existing sites if needed (when no new sites but data might have changed)
24
28
  */
25
29
  private updateExistingSitesIfNeeded;
26
30
  /**
27
31
  * Get distance for a site, with fallback
32
+ * Returns capped size if grid capping was applied, otherwise original
28
33
  */
29
34
  getDistance(siteId: string, fallback?: number): number;
30
35
  /**
@@ -6,10 +6,13 @@
6
6
  */
7
7
  import { browser } from '$app/environment';
8
8
  import { extractSiteLocations, computeNearestNeighbor, haversineDistance } from '../logic/site-distance';
9
+ import { computeGridBasedSizeCaps, getGridCappingStats } from '../logic/grid-density';
9
10
  export class SiteDistanceStore {
10
11
  key = 'map-v3-site-distances';
11
- // Cached nearest neighbor distances
12
+ // Cached nearest neighbor distances (original calculated sizes)
12
13
  distances = $state(new Map());
14
+ // Grid-capped sizes (after outlier removal)
15
+ cappedSizes = $state(new Map());
13
16
  // Track which sites have been computed
14
17
  computedSites = $state(new Set());
15
18
  // Performance tracking
@@ -25,8 +28,11 @@ export class SiteDistanceStore {
25
28
  * Only computes distances for sites not in cache
26
29
  * @param cells Array of cells to compute distances for
27
30
  * @param neighborCount Number of neighbors to average for density calculation
31
+ * @param useGridCapping Whether to apply grid-based outlier capping
32
+ * @param gridCapPercentile Percentile for grid capping (70-95)
33
+ * @param gridSizeKm Grid size in kilometers
28
34
  */
29
- updateDistances(cells, neighborCount = 5) {
35
+ updateDistances(cells, neighborCount = 5, useGridCapping = true, gridCapPercentile = 80, gridSizeKm = 1) {
30
36
  if (!cells || cells.length === 0)
31
37
  return;
32
38
  const startTime = performance.now();
@@ -55,6 +61,16 @@ export class SiteDistanceStore {
55
61
  const updatedDist = computeNearestNeighbor(existingId, existingLoc, siteLocations, neighborCount);
56
62
  this.distances.set(existingId, updatedDist);
57
63
  }
64
+ // Apply grid-based capping if enabled
65
+ if (useGridCapping) {
66
+ this.cappedSizes = computeGridBasedSizeCaps(cells, this.distances, gridCapPercentile, gridSizeKm);
67
+ const stats = getGridCappingStats(this.distances, this.cappedSizes);
68
+ console.log(`[SiteDistance] Grid capping: ${stats.cappedSites}/${stats.totalSites} sites capped (${stats.cappedPercentage.toFixed(1)}%), avg reduction: ${stats.avgReduction.toFixed(1)}m`);
69
+ }
70
+ else {
71
+ // No capping - capped sizes = original sizes
72
+ this.cappedSizes = new Map(this.distances);
73
+ }
58
74
  const endTime = performance.now();
59
75
  this.lastComputeTime = endTime - startTime;
60
76
  this.computedCount = this.distances.size;
@@ -82,15 +98,17 @@ export class SiteDistanceStore {
82
98
  }
83
99
  /**
84
100
  * Get distance for a site, with fallback
101
+ * Returns capped size if grid capping was applied, otherwise original
85
102
  */
86
103
  getDistance(siteId, fallback = 500) {
87
- return this.distances.get(siteId) ?? fallback;
104
+ return this.cappedSizes.get(siteId) ?? this.distances.get(siteId) ?? fallback;
88
105
  }
89
106
  /**
90
107
  * Clear all cached distances (useful for testing or data refresh)
91
108
  */
92
109
  clear() {
93
110
  this.distances.clear();
111
+ this.cappedSizes.clear();
94
112
  this.computedSites.clear();
95
113
  this.lastComputeTime = 0;
96
114
  this.computedCount = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.93",
3
+ "version": "0.0.94",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",