@cdc/map 4.25.7 → 4.25.10
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/.claude/settings.local.json +30 -0
- package/CLAUDE.local.md +0 -0
- package/dist/cdcmap.js +54785 -53159
- package/examples/private/c.json +290 -0
- package/examples/private/canvas-city-hover.json +787 -0
- package/examples/private/d.json +345 -0
- package/examples/private/filter-map.json +909 -0
- package/examples/private/g.json +1 -0
- package/examples/private/h.json +105911 -0
- package/examples/private/measles-data.json +378 -0
- package/examples/private/measles.json +211 -0
- package/examples/private/north-dakota.json +1132 -0
- package/examples/private/rsv-data.json +532 -0
- package/examples/private/state-with-pattern.json +883 -0
- package/examples/private/test.json +222 -640
- package/index.html +1 -1
- package/package.json +26 -5
- package/src/CdcMap.tsx +28 -8
- package/src/CdcMapComponent.tsx +230 -306
- package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
- package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
- package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
- package/src/_stories/CdcMap.Table.stories.tsx +2 -2
- package/src/_stories/CdcMap.stories.tsx +18 -11
- package/src/_stories/GoogleMap.stories.tsx +2 -2
- package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
- package/src/_stories/_mock/equal-number.json +1109 -0
- package/src/_stories/_mock/multi-state.json +21389 -0
- package/src/_stories/_mock/us-bubble-cities.json +306 -0
- package/src/components/BubbleList.tsx +16 -12
- package/src/components/CityList.tsx +88 -110
- package/src/components/DataTable.tsx +44 -12
- package/src/components/EditorPanel/components/EditorPanel.tsx +201 -203
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +3 -2
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +7 -5
- package/src/components/Geo.tsx +2 -0
- package/src/components/Legend/components/Legend.tsx +117 -93
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
- package/src/components/MapContainer.tsx +52 -0
- package/src/components/MapControls.tsx +44 -0
- package/src/components/Modal.tsx +2 -8
- package/src/components/NavigationMenu.tsx +13 -1
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +24 -7
- package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +21 -15
- package/src/components/UsaMap/components/TerritoriesSection.tsx +2 -2
- package/src/components/UsaMap/components/UsaMap.County.tsx +112 -33
- package/src/components/UsaMap/components/UsaMap.Region.tsx +23 -5
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +38 -26
- package/src/components/UsaMap/components/UsaMap.State.tsx +28 -10
- package/src/components/UsaMap/helpers/map.ts +16 -8
- package/src/components/WorldMap/WorldMap.tsx +116 -11
- package/src/components/ZoomControls.tsx +6 -9
- package/src/context/LegendMemoContext.tsx +30 -0
- package/src/context.ts +1 -39
- package/src/data/initial-state.js +143 -128
- package/src/data/supported-geos.js +202 -4
- package/src/helpers/addUIDs.ts +8 -8
- package/src/helpers/applyColorToLegend.ts +122 -45
- package/src/helpers/applyLegendToRow.ts +15 -13
- package/src/helpers/componentHelpers.ts +8 -0
- package/src/helpers/constants.ts +12 -0
- package/src/helpers/dataTableHelpers.ts +6 -0
- package/src/helpers/displayGeoName.ts +12 -7
- package/src/helpers/formatLegendLocation.ts +1 -3
- package/src/helpers/generateRuntimeLegend.ts +192 -340
- package/src/helpers/generateRuntimeLegendHash.ts +4 -2
- package/src/helpers/getColumnNames.ts +1 -1
- package/src/helpers/getPatternForRow.ts +36 -0
- package/src/helpers/getStatesPicked.ts +14 -0
- package/src/helpers/handleMapAriaLabels.ts +2 -2
- package/src/helpers/index.ts +11 -3
- package/src/helpers/isLegendItemDisabled.ts +16 -0
- package/src/helpers/mapObserverHelpers.ts +40 -0
- package/src/helpers/resetLegendToggles.ts +3 -2
- package/src/helpers/toggleLegendActive.ts +6 -11
- package/src/helpers/urlDataHelpers.ts +70 -0
- package/src/hooks/useGeoClickHandler.ts +35 -1
- package/src/hooks/useLegendMemo.ts +17 -0
- package/src/hooks/useMapLayers.tsx +5 -4
- package/src/hooks/useStateZoom.tsx +137 -88
- package/src/hooks/useTooltip.ts +1 -2
- package/src/index.jsx +6 -3
- package/src/scss/main.scss +23 -12
- package/src/store/map.actions.ts +2 -2
- package/src/store/map.reducer.ts +21 -10
- package/src/test/CdcMap.test.jsx +11 -0
- package/src/types/MapConfig.ts +25 -17
- package/src/types/MapContext.ts +2 -8
- package/src/types/runtimeLegend.ts +12 -10
- package/vite.config.js +2 -7
- package/vitest.config.ts +16 -0
- package/src/_stories/_mock/floating-point.json +0 -427
- package/src/coreStyles_map.scss +0 -3
- package/src/helpers/colorDistributions.ts +0 -12
- package/src/helpers/generateColorsArray.ts +0 -14
- package/src/helpers/getStatePicked.ts +0 -8
- package/src/helpers/tests/generateColorsArray.test.ts +0 -18
- package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
|
@@ -14,8 +14,13 @@ import _ from 'lodash'
|
|
|
14
14
|
import * as d3 from 'd3'
|
|
15
15
|
|
|
16
16
|
// Cdc
|
|
17
|
-
import colorPalettes from '@cdc/core/data/colorPalettes'
|
|
17
|
+
import { mapColorPalettes as colorPalettes } from '@cdc/core/data/colorPalettes'
|
|
18
18
|
import { supportedCountries } from '../data/supported-geos'
|
|
19
|
+
import { getColorPaletteVersion } from '@cdc/core/helpers/getColorPaletteVersion'
|
|
20
|
+
import { v2ColorDistribution } from '@cdc/core/helpers/palettes/colorDistributions'
|
|
21
|
+
|
|
22
|
+
// Types
|
|
23
|
+
import { MapConfig, DataRow, RuntimeFilters } from '../types/MapConfig'
|
|
19
24
|
|
|
20
25
|
type LegendItem = {
|
|
21
26
|
special?: boolean
|
|
@@ -33,22 +38,12 @@ export type GeneratedLegend = {
|
|
|
33
38
|
items: LegendItem[] | []
|
|
34
39
|
}
|
|
35
40
|
|
|
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
41
|
export const generateRuntimeLegend = (
|
|
47
|
-
configObj,
|
|
48
|
-
runtimeData:
|
|
42
|
+
configObj: MapConfig,
|
|
43
|
+
runtimeData: DataRow[],
|
|
49
44
|
hash: string,
|
|
50
|
-
setConfig:
|
|
51
|
-
runtimeFilters:
|
|
45
|
+
setConfig: (newMapConfig: MapConfig) => void,
|
|
46
|
+
runtimeFilters: RuntimeFilters,
|
|
52
47
|
legendMemo: React.MutableRefObject<Map<string, number>>,
|
|
53
48
|
legendSpecialClassLastMemo: React.MutableRefObject<Map<string, number>>
|
|
54
49
|
): GeneratedLegend | [] => {
|
|
@@ -61,8 +56,8 @@ export const generateRuntimeLegend = (
|
|
|
61
56
|
if (!legendSpecialClassLastMemo) Error('No legend special class last memo provided')
|
|
62
57
|
|
|
63
58
|
// Define variables..
|
|
64
|
-
const newLegendMemo = new Map
|
|
65
|
-
const newLegendSpecialClassLastMemo = new Map
|
|
59
|
+
const newLegendMemo = new Map() // Reset memoization
|
|
60
|
+
const newLegendSpecialClassLastMemo = new Map() // Reset bin memoization
|
|
66
61
|
const countryKeys = Object.keys(supportedCountries)
|
|
67
62
|
const { legend, columns, general } = configObj
|
|
68
63
|
const primaryColName = columns.primary.name
|
|
@@ -89,12 +84,9 @@ export const generateRuntimeLegend = (
|
|
|
89
84
|
// Unified will base the legend off ALL the data maps received. Otherwise, it will use
|
|
90
85
|
let dataSet = legend.unified ? data : Object?.values(runtimeData)
|
|
91
86
|
|
|
92
|
-
|
|
93
|
-
let domainNums = Array.from(
|
|
94
|
-
new Set(dataSet?.map(item => convertAndRoundValue(item[configObj.columns.primary.name], roundToPlace)))
|
|
95
|
-
)
|
|
87
|
+
let domainNums = Array.from(new Set(dataSet?.map(item => item[configObj.columns.primary.name])))
|
|
96
88
|
.filter(d => typeof d === 'number' && !isNaN(d))
|
|
97
|
-
.sort((a, b) =>
|
|
89
|
+
.sort((a, b) => a - b)
|
|
98
90
|
|
|
99
91
|
let specialClasses = 0
|
|
100
92
|
let specialClassesHash = {}
|
|
@@ -116,6 +108,12 @@ export const generateRuntimeLegend = (
|
|
|
116
108
|
label: specialClass.label
|
|
117
109
|
})
|
|
118
110
|
|
|
111
|
+
result.items[result.items.length - 1].color = applyColorToLegend(
|
|
112
|
+
result.items.length - 1,
|
|
113
|
+
configObj,
|
|
114
|
+
result.items
|
|
115
|
+
)
|
|
116
|
+
|
|
119
117
|
specialClasses += 1
|
|
120
118
|
}
|
|
121
119
|
|
|
@@ -124,7 +122,7 @@ export const generateRuntimeLegend = (
|
|
|
124
122
|
// color the configObj if val is in row
|
|
125
123
|
specialColor = result.items.findIndex(p => p.value === val)
|
|
126
124
|
|
|
127
|
-
newLegendMemo.set(
|
|
125
|
+
newLegendMemo.set(hashObj(row), specialColor)
|
|
128
126
|
|
|
129
127
|
return false
|
|
130
128
|
}
|
|
@@ -146,14 +144,14 @@ export const generateRuntimeLegend = (
|
|
|
146
144
|
if (undefined === value) continue
|
|
147
145
|
|
|
148
146
|
if (false === uniqueValues.has(value)) {
|
|
149
|
-
uniqueValues.set(value, [
|
|
147
|
+
uniqueValues.set(value, [hashObj(row)])
|
|
150
148
|
count++
|
|
151
149
|
} else {
|
|
152
|
-
uniqueValues.get(value).push(
|
|
150
|
+
uniqueValues.get(value).push(hashObj(row))
|
|
153
151
|
}
|
|
154
152
|
}
|
|
155
153
|
|
|
156
|
-
let sorted =
|
|
154
|
+
let sorted = [...uniqueValues.keys()]
|
|
157
155
|
|
|
158
156
|
if (legend.additionalCategories) {
|
|
159
157
|
legend.additionalCategories.forEach(additionalCategory => {
|
|
@@ -191,103 +189,54 @@ export const generateRuntimeLegend = (
|
|
|
191
189
|
let arr = uniqueValues.get(val)
|
|
192
190
|
|
|
193
191
|
if (arr) {
|
|
194
|
-
arr.forEach(hashedRow => newLegendMemo.set(
|
|
192
|
+
arr.forEach(hashedRow => newLegendMemo.set(hashedRow, lastIdx))
|
|
195
193
|
}
|
|
196
194
|
})
|
|
197
195
|
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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()
|
|
196
|
+
// Add color to new legend item (normal items only, not special classes)
|
|
197
|
+
for (let i = 0; i < result.items.length; i++) {
|
|
198
|
+
if (!result.items[i].special) {
|
|
199
|
+
result.items[i].color = applyColorToLegend(i, configObj, result.items)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
214
202
|
|
|
215
|
-
|
|
216
|
-
|
|
203
|
+
// Now apply special class colors last, to overwrite if needed
|
|
204
|
+
for (let i = 0; i < result.items.length; i++) {
|
|
205
|
+
if (result.items[i].special) {
|
|
206
|
+
result.items[i].color = applyColorToLegend(i, configObj, result.items)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
217
209
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
210
|
+
// Overwrite legendMemo for special class rows to ensure correct color lookup
|
|
211
|
+
result.items.forEach((item, idx) => {
|
|
212
|
+
if (item.special) {
|
|
213
|
+
// Find all rows in the data that match this special class value
|
|
214
|
+
let specialRows = data.filter(row => {
|
|
215
|
+
// If special class has a key, use it, otherwise use primaryColName
|
|
216
|
+
const key = legend.specialClasses.find(sc => String(sc.value) === String(item.value))?.key || primaryColName
|
|
217
|
+
return String(row[key]) === String(item.value)
|
|
226
218
|
})
|
|
219
|
+
specialRows.forEach(row => {
|
|
220
|
+
newLegendMemo.set(hashObj(row), idx)
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
})
|
|
227
224
|
|
|
228
|
-
|
|
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]
|
|
225
|
+
legendMemo.current = newLegendMemo
|
|
250
226
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return item.special && item.value === currentItem.value
|
|
255
|
-
} else {
|
|
256
|
-
return !item.special && item.value === currentItem.value
|
|
257
|
-
}
|
|
258
|
-
})
|
|
227
|
+
// before returning the legend result
|
|
228
|
+
// add property for bin number and set to index location
|
|
229
|
+
setBinNumbers(result)
|
|
259
230
|
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
}
|
|
231
|
+
// Move all special legend items from "Special Classes" to the end of the legend
|
|
232
|
+
sortSpecialClassesLast(result, legend)
|
|
283
233
|
|
|
284
234
|
const assignSpecialClassLastIndex = (value, key) => {
|
|
285
235
|
const newIndex = result.items.findIndex(d => d.bin === value)
|
|
286
236
|
newLegendSpecialClassLastMemo.set(key, newIndex)
|
|
287
237
|
}
|
|
288
238
|
|
|
289
|
-
|
|
290
|
-
legendMemo.current.forEach(assignSpecialClassLastIndex)
|
|
239
|
+
newLegendMemo.forEach(assignSpecialClassLastIndex)
|
|
291
240
|
legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
|
|
292
241
|
|
|
293
242
|
return result
|
|
@@ -304,26 +253,30 @@ export const generateRuntimeLegend = (
|
|
|
304
253
|
if (true === legend.separateZero && !general.equalNumberOptIn) {
|
|
305
254
|
let addLegendItem = false
|
|
306
255
|
|
|
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
256
|
for (let i = 0; i < dataSet.length; i++) {
|
|
315
257
|
if (dataSet[i][primaryColName] === 0) {
|
|
316
258
|
addLegendItem = true
|
|
317
259
|
|
|
318
260
|
let row = dataSet.splice(i, 1)[0]
|
|
319
261
|
|
|
320
|
-
newLegendMemo.set(
|
|
262
|
+
newLegendMemo.set(hashObj(row), result.items.length)
|
|
321
263
|
i--
|
|
322
264
|
}
|
|
323
265
|
}
|
|
324
266
|
|
|
325
267
|
if (addLegendItem) {
|
|
326
268
|
legendNumber -= 1 // This zero takes up one legend item
|
|
269
|
+
|
|
270
|
+
// Add new legend item
|
|
271
|
+
result.items.push({
|
|
272
|
+
min: 0,
|
|
273
|
+
max: 0
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
let lastIdx = result.items.length - 1
|
|
277
|
+
|
|
278
|
+
// Add color to new legend item
|
|
279
|
+
result.items[lastIdx].color = applyColorToLegend(lastIdx, configObj, result.items)
|
|
327
280
|
}
|
|
328
281
|
}
|
|
329
282
|
|
|
@@ -365,7 +318,7 @@ export const generateRuntimeLegend = (
|
|
|
365
318
|
|
|
366
319
|
// eslint-disable-next-line
|
|
367
320
|
removedRows.forEach(row => {
|
|
368
|
-
newLegendMemo.set(
|
|
321
|
+
newLegendMemo.set(hashObj(row), result.items.length)
|
|
369
322
|
})
|
|
370
323
|
|
|
371
324
|
result.items.push({
|
|
@@ -373,25 +326,62 @@ export const generateRuntimeLegend = (
|
|
|
373
326
|
max
|
|
374
327
|
})
|
|
375
328
|
|
|
329
|
+
result.items[result.items.length - 1].color = applyColorToLegend(
|
|
330
|
+
result.items.length - 1,
|
|
331
|
+
configObj,
|
|
332
|
+
result.items
|
|
333
|
+
)
|
|
334
|
+
|
|
376
335
|
changingNumber -= 1
|
|
377
336
|
numberOfRows -= chunkAmt
|
|
378
337
|
}
|
|
379
338
|
} else {
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
339
|
+
const paletteName = configObj.general?.palette?.name || configObj.color
|
|
340
|
+
const version = getColorPaletteVersion(configObj)
|
|
341
|
+
let colors = colorPalettes?.[`v${version}`]?.[paletteName]
|
|
342
|
+
// Fallback to a default palette if none is selected or found
|
|
343
|
+
if (!colors) {
|
|
344
|
+
const defaultPalette = version === 1 ? 'sequential_blue_green' : 'sequential_blue'
|
|
345
|
+
colors = colorPalettes?.[`v${version}`]?.[defaultPalette]
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!colors) {
|
|
349
|
+
console.warn('No color palette found, using fallback colors')
|
|
350
|
+
colors = ['#d3d3d3', '#a0a0a0', '#707070', '#404040'] // Gray fallback
|
|
351
|
+
}
|
|
385
352
|
|
|
386
|
-
|
|
387
|
-
|
|
353
|
+
// Check if we should use v2 distribution logic for better contrast
|
|
354
|
+
const isSequentialOrDivergent =
|
|
355
|
+
paletteName && (paletteName.includes('sequential') || paletteName.includes('divergent'))
|
|
356
|
+
const useV2Distribution =
|
|
357
|
+
version === 2 && isSequentialOrDivergent && colors.length === 9 && legend.numberOfItems <= 9
|
|
358
|
+
|
|
359
|
+
let colorRange
|
|
360
|
+
if (useV2Distribution && v2ColorDistribution[legend.numberOfItems]) {
|
|
361
|
+
// Use strategic color distribution for v2 sequential/divergent palettes
|
|
362
|
+
const distributionIndices = v2ColorDistribution[legend.numberOfItems]
|
|
363
|
+
colorRange = distributionIndices.map(index => colors[index])
|
|
364
|
+
} else {
|
|
365
|
+
// Use existing logic for v1 palettes and other cases
|
|
366
|
+
colorRange = colors.slice(0, legend.numberOfItems)
|
|
367
|
+
}
|
|
388
368
|
|
|
389
369
|
const getDomain = () => {
|
|
390
|
-
|
|
370
|
+
// backwards compatibility
|
|
371
|
+
if (columns?.primary?.roundToPlace !== undefined && general?.equalNumberOptIn) {
|
|
372
|
+
return _.uniq(
|
|
373
|
+
dataSet.map(item => Number(item[columns.primary.name]).toFixed(Number(columns?.primary?.roundToPlace)))
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
return _.uniq(dataSet.map(item => Math.round(Number(item[columns.primary.name]))))
|
|
391
377
|
}
|
|
392
378
|
|
|
393
379
|
const getBreaks = scale => {
|
|
394
|
-
|
|
380
|
+
// backwards compatibility
|
|
381
|
+
if (columns?.primary?.roundToPlace !== undefined && general?.equalNumberOptIn) {
|
|
382
|
+
return scale.quantiles().map(b => Number(b)?.toFixed(Number(columns?.primary?.roundToPlace)))
|
|
383
|
+
}
|
|
384
|
+
return scale.quantiles().map(item => Number(Math.round(item)))
|
|
395
385
|
}
|
|
396
386
|
|
|
397
387
|
let scale = d3
|
|
@@ -405,125 +395,85 @@ export const generateRuntimeLegend = (
|
|
|
405
395
|
cachedBreaks = breaks
|
|
406
396
|
}
|
|
407
397
|
|
|
408
|
-
//
|
|
409
|
-
if (
|
|
410
|
-
|
|
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
|
-
})
|
|
398
|
+
// if separating zero force it into breaks
|
|
399
|
+
if (cachedBreaks[0] !== 0) {
|
|
400
|
+
cachedBreaks.unshift(0)
|
|
423
401
|
}
|
|
424
402
|
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
}
|
|
403
|
+
// eslint-disable-next-line array-callback-return
|
|
404
|
+
cachedBreaks.map((item, index) => {
|
|
405
|
+
const setMin = index => {
|
|
406
|
+
let min = cachedBreaks[index]
|
|
437
407
|
|
|
438
|
-
|
|
439
|
-
|
|
408
|
+
// if first break is a seperated zero, min is zero
|
|
409
|
+
if (index === 0 && legend.separateZero) {
|
|
410
|
+
min = 0
|
|
411
|
+
}
|
|
440
412
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
.range(colorPalettes[configObj.color].slice(0, numberOfBuckets))
|
|
413
|
+
// if we're on the second break, and separating out zero, increment min to 1.
|
|
414
|
+
if (index === 1 && legend.separateZero) {
|
|
415
|
+
min = 1
|
|
416
|
+
}
|
|
446
417
|
|
|
447
|
-
|
|
418
|
+
// For non-first ranges, add small increment to prevent overlap
|
|
419
|
+
if (index > 0 && !legend.separateZero) {
|
|
420
|
+
const decimalPlace = Number(configObj?.columns?.primary?.roundToPlace) || 1
|
|
421
|
+
min = Number(cachedBreaks[index]) + Math.pow(10, -decimalPlace)
|
|
422
|
+
}
|
|
448
423
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const buckets = []
|
|
452
|
-
const sortedDomain = nonZeroDomain.sort((a: number, b: number) => a - b)
|
|
424
|
+
return min
|
|
425
|
+
}
|
|
453
426
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
})
|
|
427
|
+
const getDecimalPlace = n => {
|
|
428
|
+
return Math.pow(10, -n)
|
|
429
|
+
}
|
|
466
430
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const increment = Math.pow(10, -roundingPrecision)
|
|
470
|
-
buckets.push({
|
|
471
|
-
min: convertAndRoundValue(quantileBreaks[i - 1] + increment, roundingPrecision),
|
|
472
|
-
max: quantileBreaks[i]
|
|
473
|
-
})
|
|
474
|
-
}
|
|
431
|
+
const setMax = index => {
|
|
432
|
+
let max = Number(breaks[index + 1])
|
|
475
433
|
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
}
|
|
485
|
-
|
|
486
|
-
return buckets
|
|
434
|
+
if (index === 0 && legend.separateZero) {
|
|
435
|
+
max = 0
|
|
487
436
|
}
|
|
488
437
|
|
|
489
|
-
|
|
438
|
+
if (index + 1 === breaks.length) {
|
|
439
|
+
max = Number(domainNums[domainNums.length - 1])
|
|
440
|
+
}
|
|
490
441
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
result.items.push(bucket)
|
|
494
|
-
})
|
|
442
|
+
return max
|
|
443
|
+
}
|
|
495
444
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
let number = convertAndRoundValue(row[columns.primary.name], roundingPrecision)
|
|
499
|
-
let assigned = false
|
|
445
|
+
let min = setMin(index)
|
|
446
|
+
let max = setMax(index)
|
|
500
447
|
|
|
501
|
-
|
|
502
|
-
|
|
448
|
+
result.items.push({
|
|
449
|
+
min,
|
|
450
|
+
max
|
|
451
|
+
})
|
|
452
|
+
result.items[result.items.length - 1].color = applyColorToLegend(
|
|
453
|
+
result.items.length - 1,
|
|
454
|
+
configObj,
|
|
455
|
+
result.items
|
|
456
|
+
)
|
|
503
457
|
|
|
504
|
-
|
|
458
|
+
dataSet.forEach(row => {
|
|
459
|
+
let number = row[columns.primary.name]
|
|
460
|
+
let updated = result.items.length - 1
|
|
505
461
|
|
|
506
|
-
|
|
507
|
-
newLegendMemo.set(String(hashObj(row)), itemIndex)
|
|
508
|
-
assigned = true
|
|
509
|
-
break
|
|
510
|
-
}
|
|
511
|
-
}
|
|
462
|
+
if (result.items?.[updated]?.min === undefined || result.items?.[updated]?.max === undefined) return
|
|
512
463
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
newLegendMemo.set(String(hashObj(row)), fallbackIndex)
|
|
464
|
+
// Check if this row hasn't been assigned yet to prevent double assignment
|
|
465
|
+
if (!newLegendMemo.has(hashObj(row))) {
|
|
466
|
+
if (number >= result.items[updated].min && number <= result.items[updated].max) {
|
|
467
|
+
newLegendMemo.set(hashObj(row), updated)
|
|
518
468
|
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
469
|
+
}
|
|
470
|
+
})
|
|
471
|
+
})
|
|
522
472
|
|
|
523
473
|
// Final pass: handle any unassigned rows
|
|
524
474
|
dataSet.forEach(row => {
|
|
525
|
-
if (!newLegendMemo.has(
|
|
526
|
-
let number =
|
|
475
|
+
if (!newLegendMemo.has(hashObj(row))) {
|
|
476
|
+
let number = row[columns.primary.name]
|
|
527
477
|
let assigned = false
|
|
528
478
|
|
|
529
479
|
// Find the correct range for this value - check both boundaries
|
|
@@ -534,7 +484,7 @@ export const generateRuntimeLegend = (
|
|
|
534
484
|
|
|
535
485
|
// Check if value falls within range (inclusive of both min and max)
|
|
536
486
|
if (number >= item.min && number <= item.max) {
|
|
537
|
-
newLegendMemo.set(
|
|
487
|
+
newLegendMemo.set(hashObj(row), itemIndex)
|
|
538
488
|
assigned = true
|
|
539
489
|
break
|
|
540
490
|
}
|
|
@@ -555,7 +505,7 @@ export const generateRuntimeLegend = (
|
|
|
555
505
|
}
|
|
556
506
|
}
|
|
557
507
|
|
|
558
|
-
newLegendMemo.set(
|
|
508
|
+
newLegendMemo.set(hashObj(row), closestIndex)
|
|
559
509
|
}
|
|
560
510
|
}
|
|
561
511
|
})
|
|
@@ -591,7 +541,7 @@ export const generateRuntimeLegend = (
|
|
|
591
541
|
|
|
592
542
|
// Add rows in dataSet that belong to this new legend item since we've got the data sorted
|
|
593
543
|
while (pointer < dataSet.length && dataSet[pointer][primaryColName] <= max) {
|
|
594
|
-
newLegendMemo.set(
|
|
544
|
+
newLegendMemo.set(hashObj(dataSet[pointer]), result.items.length)
|
|
595
545
|
pointer += 1
|
|
596
546
|
}
|
|
597
547
|
|
|
@@ -601,11 +551,19 @@ export const generateRuntimeLegend = (
|
|
|
601
551
|
}
|
|
602
552
|
|
|
603
553
|
result.items.push(range)
|
|
554
|
+
|
|
555
|
+
result.items[result.items.length - 1].color = applyColorToLegend(
|
|
556
|
+
result.items.length - 1,
|
|
557
|
+
configObj,
|
|
558
|
+
result.items
|
|
559
|
+
)
|
|
604
560
|
}
|
|
605
561
|
}
|
|
606
562
|
|
|
607
563
|
setBinNumbers(result)
|
|
608
564
|
|
|
565
|
+
legendMemo.current = newLegendMemo
|
|
566
|
+
|
|
609
567
|
if (general.geoType === 'world') {
|
|
610
568
|
const runtimeDataKeys = Object.keys(runtimeData)
|
|
611
569
|
const isCountriesWithNoDataState =
|
|
@@ -621,129 +579,23 @@ export const generateRuntimeLegend = (
|
|
|
621
579
|
}
|
|
622
580
|
|
|
623
581
|
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
|
-
}
|
|
582
|
+
sortSpecialClassesLast(result, legend)
|
|
733
583
|
|
|
734
584
|
const assignSpecialClassLastIndex = (value, key) => {
|
|
735
585
|
const newIndex = result.items.findIndex(d => d.bin === value)
|
|
736
586
|
newLegendSpecialClassLastMemo.set(key, newIndex)
|
|
737
587
|
}
|
|
738
|
-
|
|
739
|
-
// Use the current legend memo (which might have been updated after sorting)
|
|
740
|
-
legendMemo.current.forEach(assignSpecialClassLastIndex)
|
|
588
|
+
newLegendMemo.forEach(assignSpecialClassLastIndex)
|
|
741
589
|
legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
|
|
742
590
|
|
|
743
591
|
return result
|
|
744
592
|
} catch (e) {
|
|
745
593
|
console.error(e)
|
|
746
|
-
return
|
|
594
|
+
return {
|
|
595
|
+
fromHash: null,
|
|
596
|
+
runtimeDataHash: null,
|
|
597
|
+
items: []
|
|
598
|
+
}
|
|
747
599
|
}
|
|
748
600
|
}
|
|
749
601
|
|