@cdc/map 4.25.6 → 4.25.7

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.
@@ -33,6 +33,16 @@ 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
+
36
46
  export const generateRuntimeLegend = (
37
47
  configObj,
38
48
  runtimeData: object[],
@@ -51,8 +61,8 @@ export const generateRuntimeLegend = (
51
61
  if (!legendSpecialClassLastMemo) Error('No legend special class last memo provided')
52
62
 
53
63
  // Define variables..
54
- const newLegendMemo = new Map() // Reset memoization
55
- const newLegendSpecialClassLastMemo = new Map() // Reset bin memoization
64
+ const newLegendMemo = new Map<string, number>() // Reset memoization
65
+ const newLegendSpecialClassLastMemo = new Map<string, number>() // Reset bin memoization
56
66
  const countryKeys = Object.keys(supportedCountries)
57
67
  const { legend, columns, general } = configObj
58
68
  const primaryColName = columns.primary.name
@@ -79,9 +89,12 @@ export const generateRuntimeLegend = (
79
89
  // Unified will base the legend off ALL the data maps received. Otherwise, it will use
80
90
  let dataSet = legend.unified ? data : Object?.values(runtimeData)
81
91
 
82
- let domainNums = Array.from(new Set(dataSet?.map(item => item[configObj.columns.primary.name])))
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
+ )
83
96
  .filter(d => typeof d === 'number' && !isNaN(d))
84
- .sort((a, b) => a - b)
97
+ .sort((a, b) => (a as number) - (b as number))
85
98
 
86
99
  let specialClasses = 0
87
100
  let specialClassesHash = {}
@@ -103,12 +116,6 @@ export const generateRuntimeLegend = (
103
116
  label: specialClass.label
104
117
  })
105
118
 
106
- result.items[result.items.length - 1].color = applyColorToLegend(
107
- result.items.length - 1,
108
- configObj,
109
- result.items
110
- )
111
-
112
119
  specialClasses += 1
113
120
  }
114
121
 
@@ -117,7 +124,7 @@ export const generateRuntimeLegend = (
117
124
  // color the configObj if val is in row
118
125
  specialColor = result.items.findIndex(p => p.value === val)
119
126
 
120
- newLegendMemo.set(hashObj(row), specialColor)
127
+ newLegendMemo.set(String(hashObj(row)), specialColor)
121
128
 
122
129
  return false
123
130
  }
@@ -139,14 +146,14 @@ export const generateRuntimeLegend = (
139
146
  if (undefined === value) continue
140
147
 
141
148
  if (false === uniqueValues.has(value)) {
142
- uniqueValues.set(value, [hashObj(row)])
149
+ uniqueValues.set(value, [String(hashObj(row))])
143
150
  count++
144
151
  } else {
145
- uniqueValues.get(value).push(hashObj(row))
152
+ uniqueValues.get(value).push(String(hashObj(row)))
146
153
  }
147
154
  }
148
155
 
149
- let sorted = [...uniqueValues.keys()]
156
+ let sorted = Array.from(uniqueValues.keys())
150
157
 
151
158
  if (legend.additionalCategories) {
152
159
  legend.additionalCategories.forEach(additionalCategory => {
@@ -184,54 +191,103 @@ export const generateRuntimeLegend = (
184
191
  let arr = uniqueValues.get(val)
185
192
 
186
193
  if (arr) {
187
- arr.forEach(hashedRow => newLegendMemo.set(hashedRow, lastIdx))
188
- }
189
- })
190
-
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
- }
197
-
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
- }
204
-
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)
213
- })
214
- specialRows.forEach(row => {
215
- newLegendMemo.set(hashObj(row), idx)
216
- })
194
+ arr.forEach(hashedRow => newLegendMemo.set(String(hashedRow), lastIdx))
217
195
  }
218
196
  })
219
197
 
220
- legendMemo.current = newLegendMemo
221
-
222
198
  // before returning the legend result
223
199
  // add property for bin number and set to index location
224
200
  setBinNumbers(result)
225
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
+
226
208
  // Move all special legend items from "Special Classes" to the end of the legend
227
209
  sortSpecialClassesLast(result, legend)
228
210
 
211
+ // Update legend memo to reflect new positions after sorting for categorical legends
212
+ if (legend.showSpecialClassesLast) {
213
+ const updatedLegendMemo = new Map()
214
+
215
+ // Create a mapping from old index to new index
216
+ const indexMapping = new Map()
217
+
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
+ }
226
+ })
227
+
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]
250
+
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
+ })
259
+
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
+ }
283
+
229
284
  const assignSpecialClassLastIndex = (value, key) => {
230
285
  const newIndex = result.items.findIndex(d => d.bin === value)
231
286
  newLegendSpecialClassLastMemo.set(key, newIndex)
232
287
  }
233
288
 
234
- newLegendMemo.forEach(assignSpecialClassLastIndex)
289
+ // Use the current legend memo (which might have been updated after sorting)
290
+ legendMemo.current.forEach(assignSpecialClassLastIndex)
235
291
  legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
236
292
 
237
293
  return result
@@ -248,30 +304,26 @@ export const generateRuntimeLegend = (
248
304
  if (true === legend.separateZero && !general.equalNumberOptIn) {
249
305
  let addLegendItem = false
250
306
 
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)
251
314
  for (let i = 0; i < dataSet.length; i++) {
252
315
  if (dataSet[i][primaryColName] === 0) {
253
316
  addLegendItem = true
254
317
 
255
318
  let row = dataSet.splice(i, 1)[0]
256
319
 
257
- newLegendMemo.set(hashObj(row), result.items.length)
320
+ newLegendMemo.set(String(hashObj(row)), 0) // Assign to the zero bucket at index 0
258
321
  i--
259
322
  }
260
323
  }
261
324
 
262
325
  if (addLegendItem) {
263
326
  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)
275
327
  }
276
328
  }
277
329
 
@@ -313,7 +365,7 @@ export const generateRuntimeLegend = (
313
365
 
314
366
  // eslint-disable-next-line
315
367
  removedRows.forEach(row => {
316
- newLegendMemo.set(hashObj(row), result.items.length)
368
+ newLegendMemo.set(String(hashObj(row)), result.items.length)
317
369
  })
318
370
 
319
371
  result.items.push({
@@ -321,35 +373,25 @@ export const generateRuntimeLegend = (
321
373
  max
322
374
  })
323
375
 
324
- result.items[result.items.length - 1].color = applyColorToLegend(
325
- result.items.length - 1,
326
- configObj,
327
- result.items
328
- )
329
-
330
376
  changingNumber -= 1
331
377
  numberOfRows -= chunkAmt
332
378
  }
333
379
  } 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
+
334
386
  let colors = colorPalettes[configObj.color]
335
387
  let colorRange = colors.slice(0, legend.numberOfItems)
336
388
 
337
389
  const getDomain = () => {
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]))))
390
+ return _.uniq(dataSet.map(item => convertAndRoundValue(item[columns.primary.name], roundingPrecision)))
345
391
  }
346
392
 
347
393
  const getBreaks = scale => {
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)))
394
+ return scale.quantiles().map(b => convertAndRoundValue(b, roundingPrecision))
353
395
  }
354
396
 
355
397
  let scale = d3
@@ -363,85 +405,125 @@ export const generateRuntimeLegend = (
363
405
  cachedBreaks = breaks
364
406
  }
365
407
 
366
- // if separating zero force it into breaks
367
- if (cachedBreaks[0] !== 0) {
368
- cachedBreaks.unshift(0)
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
+ })
369
423
  }
370
424
 
371
- // eslint-disable-next-line array-callback-return
372
- cachedBreaks.map((item, index) => {
373
- const setMin = index => {
374
- let min = cachedBreaks[index]
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
375
429
 
376
- // if first break is a seperated zero, min is zero
377
- if (index === 0 && legend.separateZero) {
378
- min = 0
379
- }
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
+ }
380
437
 
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
- }
438
+ const nonZeroDomain = getNonZeroDomain()
439
+ const numberOfBuckets = legend.separateZero ? legend.numberOfItems - 1 : legend.numberOfItems
385
440
 
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
- }
441
+ if (nonZeroDomain.length > 0 && numberOfBuckets > 0) {
442
+ let nonZeroScale = d3
443
+ .scaleQuantile()
444
+ .domain(nonZeroDomain)
445
+ .range(colorPalettes[configObj.color].slice(0, numberOfBuckets))
391
446
 
392
- return min
393
- }
447
+ const quantileBreaks = nonZeroScale.quantiles().map(b => convertAndRoundValue(b, roundingPrecision))
394
448
 
395
- const getDecimalPlace = n => {
396
- return Math.pow(10, -n)
397
- }
449
+ // Create buckets based on quantile breaks
450
+ const createBuckets = () => {
451
+ const buckets = []
452
+ const sortedDomain = nonZeroDomain.sort((a: number, b: number) => a - b)
398
453
 
399
- const setMax = index => {
400
- let max = Number(breaks[index + 1])
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
+ })
401
466
 
402
- if (index === 0 && legend.separateZero) {
403
- max = 0
404
- }
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
+ }
475
+
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
+ }
405
485
 
406
- if (index + 1 === breaks.length) {
407
- max = Number(domainNums[domainNums.length - 1])
486
+ return buckets
408
487
  }
409
488
 
410
- return max
411
- }
489
+ const buckets = createBuckets()
412
490
 
413
- let min = setMin(index)
414
- let max = setMax(index)
491
+ // Add buckets to result
492
+ buckets.forEach(bucket => {
493
+ result.items.push(bucket)
494
+ })
415
495
 
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
- )
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
425
500
 
426
- dataSet.forEach(row => {
427
- let number = row[columns.primary.name]
428
- let updated = result.items.length - 1
501
+ for (let itemIndex = legend.separateZero ? 1 : 0; itemIndex < result.items.length; itemIndex++) {
502
+ const item = result.items[itemIndex]
429
503
 
430
- if (result.items?.[updated]?.min === undefined || result.items?.[updated]?.max === undefined) return
504
+ if (item.min === undefined || item.max === undefined) continue
431
505
 
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)
506
+ if (number >= item.min && number <= item.max) {
507
+ newLegendMemo.set(String(hashObj(row)), itemIndex)
508
+ assigned = true
509
+ break
510
+ }
436
511
  }
437
- }
438
- })
439
- })
512
+
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)
518
+ }
519
+ })
520
+ }
521
+ }
440
522
 
441
523
  // Final pass: handle any unassigned rows
442
524
  dataSet.forEach(row => {
443
- if (!newLegendMemo.has(hashObj(row))) {
444
- let number = row[columns.primary.name]
525
+ if (!newLegendMemo.has(String(hashObj(row)))) {
526
+ let number = convertAndRoundValue(row[columns.primary.name], roundingPrecision)
445
527
  let assigned = false
446
528
 
447
529
  // Find the correct range for this value - check both boundaries
@@ -452,7 +534,7 @@ export const generateRuntimeLegend = (
452
534
 
453
535
  // Check if value falls within range (inclusive of both min and max)
454
536
  if (number >= item.min && number <= item.max) {
455
- newLegendMemo.set(hashObj(row), itemIndex)
537
+ newLegendMemo.set(String(hashObj(row)), itemIndex)
456
538
  assigned = true
457
539
  break
458
540
  }
@@ -462,7 +544,7 @@ export const generateRuntimeLegend = (
462
544
  if (!assigned) {
463
545
  console.warn('Value not assigned to any range:', number, 'assigning to closest range')
464
546
  let closestIndex = 0
465
- let minDistance = Math.abs(number - ((result.items[0].min + result.items[0].max) / 2))
547
+ let minDistance = Math.abs(number - (result.items[0].min + result.items[0].max) / 2)
466
548
 
467
549
  for (let i = 1; i < result.items.length; i++) {
468
550
  const midpoint = (result.items[i].min + result.items[i].max) / 2
@@ -473,7 +555,7 @@ export const generateRuntimeLegend = (
473
555
  }
474
556
  }
475
557
 
476
- newLegendMemo.set(hashObj(row), closestIndex)
558
+ newLegendMemo.set(String(hashObj(row)), closestIndex)
477
559
  }
478
560
  }
479
561
  })
@@ -509,7 +591,7 @@ export const generateRuntimeLegend = (
509
591
 
510
592
  // Add rows in dataSet that belong to this new legend item since we've got the data sorted
511
593
  while (pointer < dataSet.length && dataSet[pointer][primaryColName] <= max) {
512
- newLegendMemo.set(hashObj(dataSet[pointer]), result.items.length)
594
+ newLegendMemo.set(String(hashObj(dataSet[pointer])), result.items.length)
513
595
  pointer += 1
514
596
  }
515
597
 
@@ -519,19 +601,11 @@ export const generateRuntimeLegend = (
519
601
  }
520
602
 
521
603
  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
- )
528
604
  }
529
605
  }
530
606
 
531
607
  setBinNumbers(result)
532
608
 
533
- legendMemo.current = newLegendMemo
534
-
535
609
  if (general.geoType === 'world') {
536
610
  const runtimeDataKeys = Object.keys(runtimeData)
537
611
  const isCountriesWithNoDataState =
@@ -547,13 +621,123 @@ export const generateRuntimeLegend = (
547
621
  }
548
622
 
549
623
  setBinNumbers(result)
550
- sortSpecialClassesLast(result, legend)
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
+ }
551
733
 
552
734
  const assignSpecialClassLastIndex = (value, key) => {
553
735
  const newIndex = result.items.findIndex(d => d.bin === value)
554
736
  newLegendSpecialClassLastMemo.set(key, newIndex)
555
737
  }
556
- newLegendMemo.forEach(assignSpecialClassLastIndex)
738
+
739
+ // Use the current legend memo (which might have been updated after sorting)
740
+ legendMemo.current.forEach(assignSpecialClassLastIndex)
557
741
  legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
558
742
 
559
743
  return result
@@ -1,8 +1,8 @@
1
1
  @import 'mixins';
2
2
  @import 'editor-panel';
3
+ @import '@cdc/core/styles/accessibility';
3
4
 
4
5
  .type-map--has-error {
5
-
6
6
  .waiting {
7
7
  display: flex;
8
8
  overflow: hidden !important;