@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.
- package/dist/cdcmap.js +43118 -22395
- package/examples/private/m.json +427 -0
- package/index.html +9 -0
- package/package.json +3 -3
- package/src/CdcMap.tsx +5 -0
- package/src/CdcMapComponent.tsx +31 -4
- package/src/_stories/CdcMap.stories.tsx +11 -1
- package/src/_stories/_mock/floating-point.json +427 -0
- package/src/components/EditorPanel/components/EditorPanel.tsx +52 -51
- package/src/components/Legend/components/Legend.tsx +33 -3
- package/src/helpers/applyColorToLegend.ts +43 -24
- package/src/helpers/applyLegendToRow.ts +7 -5
- package/src/helpers/generateRuntimeLegend.ts +334 -150
- package/src/scss/main.scss +1 -1
- package/src/types/runtimeLegend.ts +15 -1
- package/examples/m2.json +0 -32904
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
367
|
-
if (
|
|
368
|
-
|
|
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
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
min = 1
|
|
384
|
-
}
|
|
438
|
+
const nonZeroDomain = getNonZeroDomain()
|
|
439
|
+
const numberOfBuckets = legend.separateZero ? legend.numberOfItems - 1 : legend.numberOfItems
|
|
385
440
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
393
|
-
}
|
|
447
|
+
const quantileBreaks = nonZeroScale.quantiles().map(b => convertAndRoundValue(b, roundingPrecision))
|
|
394
448
|
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
403
|
-
|
|
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
|
-
|
|
407
|
-
max = Number(domainNums[domainNums.length - 1])
|
|
486
|
+
return buckets
|
|
408
487
|
}
|
|
409
488
|
|
|
410
|
-
|
|
411
|
-
}
|
|
489
|
+
const buckets = createBuckets()
|
|
412
490
|
|
|
413
|
-
|
|
414
|
-
|
|
491
|
+
// Add buckets to result
|
|
492
|
+
buckets.forEach(bucket => {
|
|
493
|
+
result.items.push(bucket)
|
|
494
|
+
})
|
|
415
495
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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
|
-
|
|
504
|
+
if (item.min === undefined || item.max === undefined) continue
|
|
431
505
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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 - (
|
|
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
|
-
|
|
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
|
-
|
|
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
|