@cdc/map 4.25.7 → 4.25.8

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.
Files changed (44) hide show
  1. package/CLAUDE.local.md +0 -0
  2. package/dist/cdcmap.js +22037 -22074
  3. package/examples/private/filter-map.json +909 -0
  4. package/examples/private/rsv-data.json +532 -0
  5. package/examples/private/test.json +222 -640
  6. package/index.html +34 -35
  7. package/package.json +3 -3
  8. package/src/CdcMap.tsx +7 -2
  9. package/src/CdcMapComponent.tsx +26 -8
  10. package/src/_stories/CdcMap.stories.tsx +8 -11
  11. package/src/_stories/_mock/multi-state.json +21389 -0
  12. package/src/components/CityList.tsx +4 -4
  13. package/src/components/DataTable.tsx +8 -4
  14. package/src/components/EditorPanel/components/EditorPanel.tsx +24 -38
  15. package/src/components/Legend/components/Legend.tsx +23 -35
  16. package/src/components/Modal.tsx +2 -8
  17. package/src/components/NavigationMenu.tsx +4 -1
  18. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +21 -15
  19. package/src/components/UsaMap/components/TerritoriesSection.tsx +2 -2
  20. package/src/components/UsaMap/components/UsaMap.County.tsx +6 -1
  21. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +36 -24
  22. package/src/components/UsaMap/helpers/map.ts +16 -8
  23. package/src/components/WorldMap/WorldMap.tsx +17 -0
  24. package/src/context.ts +1 -0
  25. package/src/data/initial-state.js +8 -6
  26. package/src/data/supported-geos.js +185 -2
  27. package/src/helpers/addUIDs.ts +8 -8
  28. package/src/helpers/applyColorToLegend.ts +24 -43
  29. package/src/helpers/applyLegendToRow.ts +5 -7
  30. package/src/helpers/displayGeoName.ts +11 -6
  31. package/src/helpers/formatLegendLocation.ts +1 -3
  32. package/src/helpers/generateRuntimeLegend.ts +149 -333
  33. package/src/helpers/getStatesPicked.ts +11 -0
  34. package/src/helpers/handleMapAriaLabels.ts +2 -2
  35. package/src/hooks/useStateZoom.tsx +116 -86
  36. package/src/index.jsx +6 -1
  37. package/src/scss/main.scss +23 -12
  38. package/src/store/map.actions.ts +2 -2
  39. package/src/store/map.reducer.ts +4 -4
  40. package/src/types/MapConfig.ts +2 -3
  41. package/src/types/MapContext.ts +2 -1
  42. package/src/types/runtimeLegend.ts +1 -15
  43. package/src/_stories/_mock/floating-point.json +0 -427
  44. package/src/helpers/getStatePicked.ts +0 -8
@@ -33,16 +33,6 @@ export type GeneratedLegend = {
33
33
  items: LegendItem[] | []
34
34
  }
35
35
 
36
- // Helper function to convert and round values consistently
37
- const convertAndRoundValue = (value: any, roundToPlace: number): number => {
38
- const num = Number(value)
39
- if (isNaN(num)) return NaN
40
-
41
- // Apply rounding to handle floating-point precision issues
42
- const factor = Math.pow(10, roundToPlace)
43
- return Math.round(num * factor) / factor
44
- }
45
-
46
36
  export const generateRuntimeLegend = (
47
37
  configObj,
48
38
  runtimeData: object[],
@@ -61,8 +51,8 @@ export const generateRuntimeLegend = (
61
51
  if (!legendSpecialClassLastMemo) Error('No legend special class last memo provided')
62
52
 
63
53
  // Define variables..
64
- const newLegendMemo = new Map<string, number>() // Reset memoization
65
- const newLegendSpecialClassLastMemo = new Map<string, number>() // Reset bin memoization
54
+ const newLegendMemo = new Map() // Reset memoization
55
+ const newLegendSpecialClassLastMemo = new Map() // Reset bin memoization
66
56
  const countryKeys = Object.keys(supportedCountries)
67
57
  const { legend, columns, general } = configObj
68
58
  const primaryColName = columns.primary.name
@@ -89,12 +79,9 @@ export const generateRuntimeLegend = (
89
79
  // Unified will base the legend off ALL the data maps received. Otherwise, it will use
90
80
  let dataSet = legend.unified ? data : Object?.values(runtimeData)
91
81
 
92
- const roundToPlace = Number(columns?.primary?.roundToPlace) || 1
93
- let domainNums = Array.from(
94
- new Set(dataSet?.map(item => convertAndRoundValue(item[configObj.columns.primary.name], roundToPlace)))
95
- )
82
+ let domainNums = Array.from(new Set(dataSet?.map(item => item[configObj.columns.primary.name])))
96
83
  .filter(d => typeof d === 'number' && !isNaN(d))
97
- .sort((a, b) => (a as number) - (b as number))
84
+ .sort((a, b) => a - b)
98
85
 
99
86
  let specialClasses = 0
100
87
  let specialClassesHash = {}
@@ -116,6 +103,12 @@ export const generateRuntimeLegend = (
116
103
  label: specialClass.label
117
104
  })
118
105
 
106
+ result.items[result.items.length - 1].color = applyColorToLegend(
107
+ result.items.length - 1,
108
+ configObj,
109
+ result.items
110
+ )
111
+
119
112
  specialClasses += 1
120
113
  }
121
114
 
@@ -124,7 +117,7 @@ export const generateRuntimeLegend = (
124
117
  // color the configObj if val is in row
125
118
  specialColor = result.items.findIndex(p => p.value === val)
126
119
 
127
- newLegendMemo.set(String(hashObj(row)), specialColor)
120
+ newLegendMemo.set(hashObj(row), specialColor)
128
121
 
129
122
  return false
130
123
  }
@@ -146,14 +139,14 @@ export const generateRuntimeLegend = (
146
139
  if (undefined === value) continue
147
140
 
148
141
  if (false === uniqueValues.has(value)) {
149
- uniqueValues.set(value, [String(hashObj(row))])
142
+ uniqueValues.set(value, [hashObj(row)])
150
143
  count++
151
144
  } else {
152
- uniqueValues.get(value).push(String(hashObj(row)))
145
+ uniqueValues.get(value).push(hashObj(row))
153
146
  }
154
147
  }
155
148
 
156
- let sorted = Array.from(uniqueValues.keys())
149
+ let sorted = [...uniqueValues.keys()]
157
150
 
158
151
  if (legend.additionalCategories) {
159
152
  legend.additionalCategories.forEach(additionalCategory => {
@@ -191,103 +184,54 @@ export const generateRuntimeLegend = (
191
184
  let arr = uniqueValues.get(val)
192
185
 
193
186
  if (arr) {
194
- arr.forEach(hashedRow => newLegendMemo.set(String(hashedRow), lastIdx))
187
+ arr.forEach(hashedRow => newLegendMemo.set(hashedRow, lastIdx))
195
188
  }
196
189
  })
197
190
 
198
- // before returning the legend result
199
- // add property for bin number and set to index location
200
- setBinNumbers(result)
201
-
202
- // Store original legend items with their indices before sorting
203
- const originalCategoricalItems = result.items.map((item, index) => ({
204
- item: { ...item }, // Create a copy to avoid reference issues
205
- originalIndex: index
206
- }))
207
-
208
- // Move all special legend items from "Special Classes" to the end of the legend
209
- sortSpecialClassesLast(result, legend)
210
-
211
- // Update legend memo to reflect new positions after sorting for categorical legends
212
- if (legend.showSpecialClassesLast) {
213
- const updatedLegendMemo = new Map()
191
+ // Add color to new legend item (normal items only, not special classes)
192
+ for (let i = 0; i < result.items.length; i++) {
193
+ if (!result.items[i].special) {
194
+ result.items[i].color = applyColorToLegend(i, configObj, result.items)
195
+ }
196
+ }
214
197
 
215
- // Create a mapping from old index to new index
216
- const indexMapping = new Map()
198
+ // Now apply special class colors last, to overwrite if needed
199
+ for (let i = 0; i < result.items.length; i++) {
200
+ if (result.items[i].special) {
201
+ result.items[i].color = applyColorToLegend(i, configObj, result.items)
202
+ }
203
+ }
217
204
 
218
- // For each item in the new sorted order, find its original position
219
- result.items.forEach((newItem, newIndex) => {
220
- const originalData = originalCategoricalItems.find(({ item }) => {
221
- if (newItem.special) {
222
- return item.special && item.value === newItem.value
223
- } else {
224
- return !item.special && item.value === newItem.value
225
- }
205
+ // Overwrite legendMemo for special class rows to ensure correct color lookup
206
+ result.items.forEach((item, idx) => {
207
+ if (item.special) {
208
+ // Find all rows in the data that match this special class value
209
+ let specialRows = data.filter(row => {
210
+ // If special class has a key, use it, otherwise use primaryColName
211
+ const key = legend.specialClasses.find(sc => String(sc.value) === String(item.value))?.key || primaryColName
212
+ return String(row[key]) === String(item.value)
226
213
  })
214
+ specialRows.forEach(row => {
215
+ newLegendMemo.set(hashObj(row), idx)
216
+ })
217
+ }
218
+ })
227
219
 
228
- if (originalData) {
229
- indexMapping.set(originalData.originalIndex, newIndex)
230
- }
231
- })
232
-
233
- // Update all memo entries using the index mapping
234
- newLegendMemo.forEach((originalIndex, rowHash) => {
235
- const newIndex = indexMapping.get(originalIndex)
236
- if (newIndex !== undefined) {
237
- updatedLegendMemo.set(rowHash, newIndex)
238
- } else {
239
- // Fallback: clamp to valid range
240
- const clampedIndex = Math.min(originalIndex, result.items.length - 1)
241
- updatedLegendMemo.set(rowHash, clampedIndex)
242
- }
243
- })
244
-
245
- legendMemo.current = updatedLegendMemo
246
-
247
- // Apply colors based on original positions before sorting for categorical legends
248
- for (let i = 0; i < result.items.length; i++) {
249
- const currentItem = result.items[i]
220
+ legendMemo.current = newLegendMemo
250
221
 
251
- // Find the original position of this item
252
- const originalData = originalCategoricalItems.find(({ item }) => {
253
- if (currentItem.special) {
254
- return item.special && item.value === currentItem.value
255
- } else {
256
- return !item.special && item.value === currentItem.value
257
- }
258
- })
222
+ // before returning the legend result
223
+ // add property for bin number and set to index location
224
+ setBinNumbers(result)
259
225
 
260
- if (originalData) {
261
- // Use the original index for color calculation to maintain proper color sequence
262
- const contextArray = originalCategoricalItems.slice(0, originalData.originalIndex + 1).map(o => o.item)
263
- const appliedColor = applyColorToLegend(originalData.originalIndex, configObj, contextArray)
264
- result.items[i].color = appliedColor
265
- } else {
266
- // Fallback: apply color normally
267
- const contextArray = result.items.slice(0, i + 1)
268
- const appliedColor = applyColorToLegend(i, configObj, contextArray)
269
- result.items[i].color = appliedColor
270
- }
271
- }
272
- } else {
273
- // Simple case: no special sorting, just apply colors normally
274
- legendMemo.current = newLegendMemo
275
-
276
- for (let i = 0; i < result.items.length; i++) {
277
- // Create a context array that simulates the original incremental state
278
- const contextArray = result.items.slice(0, i + 1)
279
- const appliedColor = applyColorToLegend(i, configObj, contextArray)
280
- result.items[i].color = appliedColor
281
- }
282
- }
226
+ // Move all special legend items from "Special Classes" to the end of the legend
227
+ sortSpecialClassesLast(result, legend)
283
228
 
284
229
  const assignSpecialClassLastIndex = (value, key) => {
285
230
  const newIndex = result.items.findIndex(d => d.bin === value)
286
231
  newLegendSpecialClassLastMemo.set(key, newIndex)
287
232
  }
288
233
 
289
- // Use the current legend memo (which might have been updated after sorting)
290
- legendMemo.current.forEach(assignSpecialClassLastIndex)
234
+ newLegendMemo.forEach(assignSpecialClassLastIndex)
291
235
  legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
292
236
 
293
237
  return result
@@ -304,26 +248,30 @@ export const generateRuntimeLegend = (
304
248
  if (true === legend.separateZero && !general.equalNumberOptIn) {
305
249
  let addLegendItem = false
306
250
 
307
- // First, add the zero bucket
308
- result.items.push({
309
- min: 0,
310
- max: 0
311
- })
312
-
313
- // Then process zero values and assign them to the zero bucket (index 0)
314
251
  for (let i = 0; i < dataSet.length; i++) {
315
252
  if (dataSet[i][primaryColName] === 0) {
316
253
  addLegendItem = true
317
254
 
318
255
  let row = dataSet.splice(i, 1)[0]
319
256
 
320
- newLegendMemo.set(String(hashObj(row)), 0) // Assign to the zero bucket at index 0
257
+ newLegendMemo.set(hashObj(row), result.items.length)
321
258
  i--
322
259
  }
323
260
  }
324
261
 
325
262
  if (addLegendItem) {
326
263
  legendNumber -= 1 // This zero takes up one legend item
264
+
265
+ // Add new legend item
266
+ result.items.push({
267
+ min: 0,
268
+ max: 0
269
+ })
270
+
271
+ let lastIdx = result.items.length - 1
272
+
273
+ // Add color to new legend item
274
+ result.items[lastIdx].color = applyColorToLegend(lastIdx, configObj, result.items)
327
275
  }
328
276
  }
329
277
 
@@ -365,7 +313,7 @@ export const generateRuntimeLegend = (
365
313
 
366
314
  // eslint-disable-next-line
367
315
  removedRows.forEach(row => {
368
- newLegendMemo.set(String(hashObj(row)), result.items.length)
316
+ newLegendMemo.set(hashObj(row), result.items.length)
369
317
  })
370
318
 
371
319
  result.items.push({
@@ -373,25 +321,35 @@ export const generateRuntimeLegend = (
373
321
  max
374
322
  })
375
323
 
324
+ result.items[result.items.length - 1].color = applyColorToLegend(
325
+ result.items.length - 1,
326
+ configObj,
327
+ result.items
328
+ )
329
+
376
330
  changingNumber -= 1
377
331
  numberOfRows -= chunkAmt
378
332
  }
379
333
  } else {
380
- // Use the appropriate rounding precision
381
- const roundingPrecision =
382
- general?.equalNumberOptIn && columns?.primary?.roundToPlace !== undefined
383
- ? Number(columns.primary.roundToPlace)
384
- : roundToPlace
385
-
386
334
  let colors = colorPalettes[configObj.color]
387
335
  let colorRange = colors.slice(0, legend.numberOfItems)
388
336
 
389
337
  const getDomain = () => {
390
- return _.uniq(dataSet.map(item => convertAndRoundValue(item[columns.primary.name], roundingPrecision)))
338
+ // backwards compatibility
339
+ if (columns?.primary?.roundToPlace !== undefined && general?.equalNumberOptIn) {
340
+ return _.uniq(
341
+ dataSet.map(item => Number(item[columns.primary.name]).toFixed(Number(columns?.primary?.roundToPlace)))
342
+ )
343
+ }
344
+ return _.uniq(dataSet.map(item => Math.round(Number(item[columns.primary.name]))))
391
345
  }
392
346
 
393
347
  const getBreaks = scale => {
394
- return scale.quantiles().map(b => convertAndRoundValue(b, roundingPrecision))
348
+ // backwards compatibility
349
+ if (columns?.primary?.roundToPlace !== undefined && general?.equalNumberOptIn) {
350
+ return scale.quantiles().map(b => Number(b)?.toFixed(Number(columns?.primary?.roundToPlace)))
351
+ }
352
+ return scale.quantiles().map(item => Number(Math.round(item)))
395
353
  }
396
354
 
397
355
  let scale = d3
@@ -405,125 +363,85 @@ export const generateRuntimeLegend = (
405
363
  cachedBreaks = breaks
406
364
  }
407
365
 
408
- // Handle separateZero logic: if separating zero and it's not already included, add it
409
- if (legend.separateZero) {
410
- // Add zero bucket first if separating zero
411
- result.items.push({
412
- min: 0,
413
- max: 0
414
- })
415
-
416
- // Assign all zero values to this bucket
417
- dataSet.forEach(row => {
418
- let number = convertAndRoundValue(row[columns.primary.name], roundingPrecision)
419
- if (number === 0) {
420
- newLegendMemo.set(String(hashObj(row)), 0)
421
- }
422
- })
366
+ // if separating zero force it into breaks
367
+ if (cachedBreaks[0] !== 0) {
368
+ cachedBreaks.unshift(0)
423
369
  }
424
370
 
425
- // Create quantile breaks for non-zero values (or all values if not separating zero)
426
- const dataForBreaks = legend.separateZero
427
- ? dataSet.filter(row => convertAndRoundValue(row[columns.primary.name], roundingPrecision) !== 0)
428
- : dataSet
429
-
430
- if (dataForBreaks.length > 0) {
431
- // Recalculate scale and breaks for non-zero data
432
- const getNonZeroDomain = () => {
433
- return _.uniq(
434
- dataForBreaks.map(item => convertAndRoundValue(item[columns.primary.name], roundingPrecision))
435
- ).sort((a: number, b: number) => a - b)
436
- }
437
-
438
- const nonZeroDomain = getNonZeroDomain()
439
- const numberOfBuckets = legend.separateZero ? legend.numberOfItems - 1 : legend.numberOfItems
371
+ // eslint-disable-next-line array-callback-return
372
+ cachedBreaks.map((item, index) => {
373
+ const setMin = index => {
374
+ let min = cachedBreaks[index]
440
375
 
441
- if (nonZeroDomain.length > 0 && numberOfBuckets > 0) {
442
- let nonZeroScale = d3
443
- .scaleQuantile()
444
- .domain(nonZeroDomain)
445
- .range(colorPalettes[configObj.color].slice(0, numberOfBuckets))
376
+ // if first break is a seperated zero, min is zero
377
+ if (index === 0 && legend.separateZero) {
378
+ min = 0
379
+ }
446
380
 
447
- const quantileBreaks = nonZeroScale.quantiles().map(b => convertAndRoundValue(b, roundingPrecision))
381
+ // if we're on the second break, and separating out zero, increment min to 1.
382
+ if (index === 1 && legend.separateZero) {
383
+ min = 1
384
+ }
448
385
 
449
- // Create buckets based on quantile breaks
450
- const createBuckets = () => {
451
- const buckets = []
452
- const sortedDomain = nonZeroDomain.sort((a: number, b: number) => a - b)
386
+ // For non-first ranges, add small increment to prevent overlap
387
+ if (index > 0 && !legend.separateZero) {
388
+ const decimalPlace = Number(configObj?.columns?.primary?.roundToPlace) || 1
389
+ min = Number(cachedBreaks[index]) + Math.pow(10, -decimalPlace)
390
+ }
453
391
 
454
- if (quantileBreaks.length === 0) {
455
- // Single bucket case
456
- buckets.push({
457
- min: sortedDomain[0],
458
- max: sortedDomain[sortedDomain.length - 1]
459
- })
460
- } else {
461
- // First bucket: min value to first break
462
- buckets.push({
463
- min: sortedDomain[0],
464
- max: quantileBreaks[0]
465
- })
392
+ return min
393
+ }
466
394
 
467
- // Middle buckets: previous break + increment to current break
468
- for (let i = 1; i < quantileBreaks.length; i++) {
469
- const increment = Math.pow(10, -roundingPrecision)
470
- buckets.push({
471
- min: convertAndRoundValue(quantileBreaks[i - 1] + increment, roundingPrecision),
472
- max: quantileBreaks[i]
473
- })
474
- }
395
+ const getDecimalPlace = n => {
396
+ return Math.pow(10, -n)
397
+ }
475
398
 
476
- // Last bucket: last break + increment to max value
477
- if (quantileBreaks.length > 0) {
478
- const increment = Math.pow(10, -roundingPrecision)
479
- buckets.push({
480
- min: convertAndRoundValue(quantileBreaks[quantileBreaks.length - 1] + increment, roundingPrecision),
481
- max: sortedDomain[sortedDomain.length - 1]
482
- })
483
- }
484
- }
399
+ const setMax = index => {
400
+ let max = Number(breaks[index + 1])
485
401
 
486
- return buckets
402
+ if (index === 0 && legend.separateZero) {
403
+ max = 0
487
404
  }
488
405
 
489
- const buckets = createBuckets()
406
+ if (index + 1 === breaks.length) {
407
+ max = Number(domainNums[domainNums.length - 1])
408
+ }
490
409
 
491
- // Add buckets to result
492
- buckets.forEach(bucket => {
493
- result.items.push(bucket)
494
- })
410
+ return max
411
+ }
495
412
 
496
- // Assign non-zero values to appropriate buckets
497
- dataForBreaks.forEach(row => {
498
- let number = convertAndRoundValue(row[columns.primary.name], roundingPrecision)
499
- let assigned = false
413
+ let min = setMin(index)
414
+ let max = setMax(index)
500
415
 
501
- for (let itemIndex = legend.separateZero ? 1 : 0; itemIndex < result.items.length; itemIndex++) {
502
- const item = result.items[itemIndex]
416
+ result.items.push({
417
+ min,
418
+ max
419
+ })
420
+ result.items[result.items.length - 1].color = applyColorToLegend(
421
+ result.items.length - 1,
422
+ configObj,
423
+ result.items
424
+ )
503
425
 
504
- if (item.min === undefined || item.max === undefined) continue
426
+ dataSet.forEach(row => {
427
+ let number = row[columns.primary.name]
428
+ let updated = result.items.length - 1
505
429
 
506
- if (number >= item.min && number <= item.max) {
507
- newLegendMemo.set(String(hashObj(row)), itemIndex)
508
- assigned = true
509
- break
510
- }
511
- }
430
+ if (result.items?.[updated]?.min === undefined || result.items?.[updated]?.max === undefined) return
512
431
 
513
- // Fallback assignment if not assigned
514
- if (!assigned) {
515
- console.warn('Non-zero value not assigned to any range:', number)
516
- const fallbackIndex = legend.separateZero ? 1 : 0
517
- newLegendMemo.set(String(hashObj(row)), fallbackIndex)
432
+ // Check if this row hasn't been assigned yet to prevent double assignment
433
+ if (!newLegendMemo.has(hashObj(row))) {
434
+ if (number >= result.items[updated].min && number <= result.items[updated].max) {
435
+ newLegendMemo.set(hashObj(row), updated)
518
436
  }
519
- })
520
- }
521
- }
437
+ }
438
+ })
439
+ })
522
440
 
523
441
  // Final pass: handle any unassigned rows
524
442
  dataSet.forEach(row => {
525
- if (!newLegendMemo.has(String(hashObj(row)))) {
526
- let number = convertAndRoundValue(row[columns.primary.name], roundingPrecision)
443
+ if (!newLegendMemo.has(hashObj(row))) {
444
+ let number = row[columns.primary.name]
527
445
  let assigned = false
528
446
 
529
447
  // Find the correct range for this value - check both boundaries
@@ -534,7 +452,7 @@ export const generateRuntimeLegend = (
534
452
 
535
453
  // Check if value falls within range (inclusive of both min and max)
536
454
  if (number >= item.min && number <= item.max) {
537
- newLegendMemo.set(String(hashObj(row)), itemIndex)
455
+ newLegendMemo.set(hashObj(row), itemIndex)
538
456
  assigned = true
539
457
  break
540
458
  }
@@ -555,7 +473,7 @@ export const generateRuntimeLegend = (
555
473
  }
556
474
  }
557
475
 
558
- newLegendMemo.set(String(hashObj(row)), closestIndex)
476
+ newLegendMemo.set(hashObj(row), closestIndex)
559
477
  }
560
478
  }
561
479
  })
@@ -591,7 +509,7 @@ export const generateRuntimeLegend = (
591
509
 
592
510
  // Add rows in dataSet that belong to this new legend item since we've got the data sorted
593
511
  while (pointer < dataSet.length && dataSet[pointer][primaryColName] <= max) {
594
- newLegendMemo.set(String(hashObj(dataSet[pointer])), result.items.length)
512
+ newLegendMemo.set(hashObj(dataSet[pointer]), result.items.length)
595
513
  pointer += 1
596
514
  }
597
515
 
@@ -601,11 +519,19 @@ export const generateRuntimeLegend = (
601
519
  }
602
520
 
603
521
  result.items.push(range)
522
+
523
+ result.items[result.items.length - 1].color = applyColorToLegend(
524
+ result.items.length - 1,
525
+ configObj,
526
+ result.items
527
+ )
604
528
  }
605
529
  }
606
530
 
607
531
  setBinNumbers(result)
608
532
 
533
+ legendMemo.current = newLegendMemo
534
+
609
535
  if (general.geoType === 'world') {
610
536
  const runtimeDataKeys = Object.keys(runtimeData)
611
537
  const isCountriesWithNoDataState =
@@ -621,123 +547,13 @@ export const generateRuntimeLegend = (
621
547
  }
622
548
 
623
549
  setBinNumbers(result)
624
-
625
- // Only do complex sorting and color mapping if showSpecialClassesLast is enabled
626
- if (legend.showSpecialClassesLast) {
627
- // Store original legend items with their indices before sorting
628
- const originalItems = result.items.map((item, index) => ({
629
- item: { ...item }, // Create a copy to avoid reference issues
630
- originalIndex: index
631
- }))
632
-
633
- sortSpecialClassesLast(result, legend)
634
-
635
- // Update legend memo to reflect new positions after sorting
636
- const updatedLegendMemo = new Map()
637
-
638
- // Create a mapping from old index to new index
639
- const indexMapping = new Map()
640
-
641
- // For each item in the new sorted order, find its original position
642
- result.items.forEach((newItem, newIndex) => {
643
- const originalData = originalItems.find(({ item }) => {
644
- if (newItem.special) {
645
- return item.special && item.value === newItem.value
646
- } else {
647
- return !item.special && item.min === newItem.min && item.max === newItem.max
648
- }
649
- })
650
-
651
- if (originalData) {
652
- indexMapping.set(originalData.originalIndex, newIndex)
653
- }
654
- })
655
-
656
- // Update all memo entries using the index mapping
657
- newLegendMemo.forEach((originalIndex, rowHash) => {
658
- const newIndex = indexMapping.get(originalIndex)
659
- if (newIndex !== undefined) {
660
- updatedLegendMemo.set(rowHash, newIndex)
661
- } else {
662
- // For unmapped entries, check if it was originally a special class
663
- const originalItem = originalItems[originalIndex]?.item
664
- if (originalItem?.special) {
665
- // Find the special class in its new position
666
- const specialIndex = result.items.findIndex(item => item.special && item.value === originalItem.value)
667
- if (specialIndex !== -1) {
668
- updatedLegendMemo.set(rowHash, specialIndex)
669
- } else {
670
- // Fallback: clamp to valid range
671
- const clampedIndex = Math.min(originalIndex, result.items.length - 1)
672
- updatedLegendMemo.set(rowHash, clampedIndex)
673
- }
674
- } else {
675
- // Fallback: clamp to valid range
676
- const clampedIndex = Math.min(originalIndex, result.items.length - 1)
677
- updatedLegendMemo.set(rowHash, clampedIndex)
678
- }
679
- }
680
- })
681
-
682
- legendMemo.current = updatedLegendMemo
683
-
684
- // Apply colors based on original positions to maintain proper color sequence
685
- for (let i = 0; i < result.items.length; i++) {
686
- const currentItem = result.items[i]
687
-
688
- // Find the original position of this item
689
- const originalData = originalItems.find(({ item }) => {
690
- if (currentItem.special) {
691
- return item.special && item.value === currentItem.value
692
- } else {
693
- return !item.special && item.min === currentItem.min && item.max === currentItem.max
694
- }
695
- })
696
-
697
- if (originalData) {
698
- // Use the original index for color calculation to maintain proper color sequence
699
- const contextArray = originalItems.slice(0, originalData.originalIndex + 1).map(o => o.item)
700
- const appliedColor = applyColorToLegend(originalData.originalIndex, configObj, contextArray)
701
- result.items[i].color = appliedColor
702
- } else {
703
- // Fallback: apply color normally
704
- const contextArray = result.items.slice(0, i + 1)
705
- const appliedColor = applyColorToLegend(i, configObj, contextArray)
706
- result.items[i].color = appliedColor
707
- }
708
- }
709
-
710
- // Final step: Ensure special class rows are correctly mapped to their legend items
711
- result.items.forEach((item, idx) => {
712
- if (item.special) {
713
- // Find all rows in the original data that match this special class value
714
- let specialRows = data.filter(row => {
715
- // If special class has a key, use it, otherwise use primaryColName
716
- const key = legend.specialClasses.find(sc => String(sc.value) === String(item.value))?.key || primaryColName
717
- return String(row[key]) === String(item.value)
718
- })
719
- specialRows.forEach(row => {
720
- legendMemo.current.set(String(hashObj(row)), idx)
721
- })
722
- }
723
- })
724
- } else {
725
- legendMemo.current = newLegendMemo
726
-
727
- for (let i = 0; i < result.items.length; i++) {
728
- const contextArray = result.items.slice(0, i + 1)
729
- const appliedColor = applyColorToLegend(i, configObj, contextArray)
730
- result.items[i].color = appliedColor
731
- }
732
- }
550
+ sortSpecialClassesLast(result, legend)
733
551
 
734
552
  const assignSpecialClassLastIndex = (value, key) => {
735
553
  const newIndex = result.items.findIndex(d => d.bin === value)
736
554
  newLegendSpecialClassLastMemo.set(key, newIndex)
737
555
  }
738
-
739
- // Use the current legend memo (which might have been updated after sorting)
740
- legendMemo.current.forEach(assignSpecialClassLastIndex)
556
+ newLegendMemo.forEach(assignSpecialClassLastIndex)
741
557
  legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
742
558
 
743
559
  return result
@@ -0,0 +1,11 @@
1
+ import { getFilterControllingStatesPicked } from '../components/UsaMap/helpers/map'
2
+ import { supportedStatesFipsCodes } from '../data/supported-geos'
3
+
4
+ export const getStatesPicked = (config, runtimeData) => {
5
+ const stateNames = getFilterControllingStatesPicked(config, runtimeData)
6
+
7
+ return stateNames.map(stateName => ({
8
+ fipsCode: Object.keys(supportedStatesFipsCodes).find(key => supportedStatesFipsCodes[key] === stateName),
9
+ stateName
10
+ }))
11
+ }