@cdc/map 4.25.3 → 4.25.5-1

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 (111) hide show
  1. package/dist/cdcmap.js +38945 -41511
  2. package/examples/hex-colors.json +3 -3
  3. package/examples/private/test.json +470 -1457
  4. package/examples/private/{mmr.json → wastewatermap.json} +86 -115
  5. package/index.html +13 -41
  6. package/package.json +4 -10
  7. package/src/CdcMap.tsx +51 -1555
  8. package/src/CdcMapComponent.tsx +594 -0
  9. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +10 -0
  10. package/src/_stories/CdcMap.Legend.stories.tsx +67 -0
  11. package/src/_stories/CdcMap.stories.tsx +4 -1
  12. package/src/_stories/UsaMap.NoData.stories.tsx +4 -4
  13. package/{examples/private/default-patterns.json → src/_stories/_mock/legends/legend-tests.json} +36 -131
  14. package/src/cdcMapComponent.styles.css +9 -0
  15. package/src/components/Annotation/Annotation.Draggable.tsx +27 -26
  16. package/src/components/Annotation/AnnotationDropdown.tsx +5 -6
  17. package/src/components/BubbleList.tsx +135 -49
  18. package/src/components/CityList.tsx +89 -87
  19. package/src/components/DataTable.tsx +8 -8
  20. package/src/components/EditorPanel/components/EditorPanel.tsx +714 -820
  21. package/src/components/EditorPanel/components/Error.tsx +9 -2
  22. package/src/components/EditorPanel/components/HexShapeSettings.tsx +127 -141
  23. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +55 -86
  24. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +89 -75
  25. package/src/components/EditorPanel/components/editorPanel.styles.css +95 -0
  26. package/src/components/Geo.tsx +9 -1
  27. package/src/components/GoogleMap/components/GoogleMap.tsx +1 -1
  28. package/src/components/Legend/components/Legend.tsx +92 -87
  29. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +128 -0
  30. package/src/components/Legend/components/LegendGroup/legend.group.css +27 -0
  31. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -1
  32. package/src/components/Legend/components/index.scss +18 -6
  33. package/src/components/Modal.tsx +17 -7
  34. package/src/components/NavigationMenu.tsx +11 -9
  35. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +12 -8
  36. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +4 -4
  37. package/src/components/UsaMap/components/TerritoriesSection.tsx +33 -10
  38. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +12 -10
  39. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +12 -14
  40. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +2 -1
  41. package/src/components/UsaMap/components/UsaMap.County.tsx +138 -96
  42. package/src/components/UsaMap/components/UsaMap.Region.styles.css +72 -0
  43. package/src/components/UsaMap/components/UsaMap.Region.tsx +56 -103
  44. package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +10 -0
  45. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +59 -66
  46. package/src/components/UsaMap/components/UsaMap.State.tsx +112 -91
  47. package/src/components/UsaMap/helpers/map.ts +1 -1
  48. package/src/components/UsaMap/helpers/shapes.ts +20 -7
  49. package/src/components/WorldMap/WorldMap.tsx +64 -118
  50. package/src/components/WorldMap/worldMap.styles.css +28 -0
  51. package/src/components/ZoomControls.tsx +15 -13
  52. package/src/components/zoomControls.styles.css +53 -0
  53. package/src/context.ts +17 -9
  54. package/src/data/initial-state.js +5 -2
  55. package/src/helpers/addUIDs.ts +151 -0
  56. package/src/helpers/applyColorToLegend.ts +39 -64
  57. package/src/helpers/applyLegendToRow.ts +51 -0
  58. package/src/helpers/colorDistributions.ts +12 -0
  59. package/src/helpers/constants.ts +44 -0
  60. package/src/helpers/displayGeoName.ts +9 -2
  61. package/src/helpers/generateColorsArray.ts +2 -1
  62. package/src/helpers/generateRuntimeData.ts +74 -0
  63. package/src/helpers/generateRuntimeFilters.ts +63 -0
  64. package/src/helpers/generateRuntimeLegend.ts +537 -0
  65. package/src/helpers/generateRuntimeLegendHash.ts +16 -15
  66. package/src/helpers/getColumnNames.ts +19 -0
  67. package/src/helpers/getMapContainerClasses.ts +23 -0
  68. package/src/helpers/handleMapTabbing.ts +31 -0
  69. package/src/helpers/hashObj.ts +1 -1
  70. package/src/helpers/index.ts +22 -0
  71. package/src/helpers/navigationHandler.ts +3 -3
  72. package/src/helpers/resetLegendToggles.ts +13 -0
  73. package/src/helpers/setBinNumbers.ts +5 -0
  74. package/src/helpers/sortSpecialClassesLast.ts +7 -0
  75. package/src/helpers/tests/getColumnNames.test.ts +52 -0
  76. package/src/helpers/titleCase.ts +1 -1
  77. package/src/helpers/toggleLegendActive.ts +25 -0
  78. package/src/hooks/useApplyTooltipsToGeo.tsx +51 -0
  79. package/src/hooks/useColumnsRequiredChecker.ts +51 -0
  80. package/src/hooks/useGeoClickHandler.ts +45 -0
  81. package/src/hooks/useLegendSeparators.ts +26 -0
  82. package/src/hooks/useMapLayers.tsx +34 -60
  83. package/src/hooks/useModal.ts +22 -0
  84. package/src/hooks/useResizeObserver.ts +4 -5
  85. package/src/hooks/useStateZoom.tsx +52 -75
  86. package/src/hooks/useTooltip.ts +2 -3
  87. package/src/index.jsx +3 -9
  88. package/src/scss/editor-panel.scss +3 -99
  89. package/src/scss/main.scss +1 -19
  90. package/src/scss/map.scss +15 -220
  91. package/src/store/map.actions.ts +46 -0
  92. package/src/store/map.reducer.ts +96 -0
  93. package/src/types/Annotations.ts +24 -0
  94. package/src/types/MapConfig.ts +23 -3
  95. package/src/types/MapContext.ts +36 -35
  96. package/src/types/Modal.ts +1 -0
  97. package/src/types/RuntimeData.ts +3 -0
  98. package/LICENSE +0 -201
  99. package/examples/private/DEV-9644.json +0 -184
  100. package/examples/private/DEV-9989.json +0 -229
  101. package/examples/private/ardi.json +0 -180
  102. package/examples/private/colors 2.json +0 -416
  103. package/examples/private/colors.json +0 -416
  104. package/examples/private/colors.json.zip +0 -0
  105. package/examples/private/customColors.json +0 -45348
  106. package/examples/test.json +0 -183
  107. package/src/helpers/closeModal.ts +0 -9
  108. package/src/scss/btn.scss +0 -69
  109. package/src/scss/filters.scss +0 -27
  110. package/src/scss/variables.scss +0 -1
  111. /package/src/hooks/{useActiveElement.js → useActiveElement.ts} +0 -0
@@ -0,0 +1,537 @@
1
+ import { useCallback, useContext } from 'react'
2
+ import ConfigContext from '../context'
3
+ import {
4
+ applyColorToLegend,
5
+ getGeoFillColor,
6
+ hashObj,
7
+ indexOfIgnoreType,
8
+ setBinNumbers,
9
+ sortSpecialClassesLast
10
+ } from '.'
11
+
12
+ import _ from 'lodash'
13
+ import * as d3 from 'd3'
14
+
15
+ // Cdc
16
+ import colorPalettes from '@cdc/core/data/colorPalettes'
17
+ import { supportedCountries } from '../data/supported-geos'
18
+
19
+ type LegendItem = {
20
+ special?: boolean
21
+ value: string | number
22
+ label?: string
23
+ color?: string
24
+ min?: number
25
+ max?: number
26
+ bin?: number
27
+ }
28
+
29
+ export type GeneratedLegend = {
30
+ fromHash: number
31
+ runtimeDataHash: number
32
+ items: LegendItem[] | []
33
+ }
34
+
35
+ export const generateRuntimeLegend = (
36
+ configObj,
37
+ runtimeData: object[],
38
+ hash: string,
39
+ setConfig: Function,
40
+ runtimeFilters: object[],
41
+ legendMemo: React.MutableRefObject<Map<string, number>>,
42
+ legendSpecialClassLastMemo: React.MutableRefObject<Map<string, number>>
43
+ ): GeneratedLegend | [] => {
44
+ try {
45
+ // Throw errors if args missing
46
+ if (!runtimeData) Error('No runtime data provided')
47
+ if (!hash) Error('No hash provided')
48
+ if (!configObj) Error('No config object provided')
49
+ if (!legendMemo) Error('No legend memo provided')
50
+ if (!legendSpecialClassLastMemo) Error('No legend special class last memo provided')
51
+
52
+ // Define variables..
53
+ const newLegendMemo = new Map() // Reset memoization
54
+ const newLegendSpecialClassLastMemo = new Map() // Reset bin memoization
55
+ const countryKeys = Object.keys(supportedCountries)
56
+ const { data, legend, columns, general } = configObj
57
+ const primaryColName = columns.primary.name
58
+ const isBubble = general.type === 'bubble'
59
+ const categoricalCol = columns.categorical ? columns.categorical.name : undefined
60
+
61
+ const result = {
62
+ fromHash: null,
63
+ runtimeDataHash: null,
64
+ items: []
65
+ }
66
+
67
+ // Add a hash for what we're working from if passed
68
+ if (hash) {
69
+ result.fromHash = hash
70
+ }
71
+
72
+ result.runtimeDataHash = runtimeFilters?.fromHash
73
+
74
+ // Unified will base the legend off ALL the data maps received. Otherwise, it will use
75
+ let dataSet = legend.unified ? data : Object?.values(runtimeData)
76
+
77
+ let domainNums = Array.from(new Set(dataSet?.map(item => item[configObj.columns.primary.name])))
78
+ .filter(d => typeof d === 'number' && !isNaN(d))
79
+ .sort((a, b) => a - b)
80
+
81
+ let specialClasses = 0
82
+ let specialClassesHash = {}
83
+
84
+ if (legend.specialClasses.length) {
85
+ if (typeof legend.specialClasses[0] === 'object') {
86
+ legend.specialClasses.forEach(specialClass => {
87
+ const val = String(specialClass.value)
88
+ if (undefined === specialClassesHash[val]) {
89
+ specialClassesHash[val] = true
90
+ result.items.push({
91
+ special: true,
92
+ value: val,
93
+ label: specialClass.label
94
+ })
95
+ result.items[result.items.length - 1].color = applyColorToLegend(
96
+ result.items.length - 1,
97
+ configObj,
98
+ result.items
99
+ )
100
+ specialClasses += 1
101
+ }
102
+ // Optionally, still map any rows that match this special class
103
+ dataSet.forEach(row => {
104
+ const rowVal = String(row[specialClass.key])
105
+ if (rowVal === val) {
106
+ let specialColor = result.items.findIndex(p => p.value === val)
107
+ newLegendMemo.set(hashObj(row), specialColor)
108
+ }
109
+ })
110
+ })
111
+ } else {
112
+ dataSet = dataSet.filter(row => {
113
+ const val = row[primaryColName]
114
+
115
+ if (legend.specialClasses.includes(val)) {
116
+ // apply the special color to the legend
117
+ if (undefined === specialClassesHash[val]) {
118
+ specialClassesHash[val] = true
119
+
120
+ result.items.push({
121
+ special: true,
122
+ value: val
123
+ })
124
+
125
+ result.items[result.items.length - 1].color = applyColorToLegend(
126
+ result.items.length - 1,
127
+ configObj,
128
+ result.items
129
+ )
130
+
131
+ specialClasses += 1
132
+ }
133
+
134
+ let specialColor = 0
135
+
136
+ // color the configObj if val is in row
137
+ if (Object.values(row).includes(val)) {
138
+ specialColor = result.items.findIndex(p => p.value === val)
139
+ }
140
+
141
+ newLegendMemo.set(hashObj(row), specialColor)
142
+
143
+ return false
144
+ }
145
+
146
+ return true
147
+ })
148
+ }
149
+ }
150
+
151
+ // Category
152
+ if (legend.type === 'category') {
153
+ let uniqueValues = new Map()
154
+ let count = 0
155
+
156
+ for (let i = 0; i < dataSet.length; i++) {
157
+ let row = dataSet[i]
158
+ let value = isBubble && categoricalCol && row[categoricalCol] ? row[categoricalCol] : row[primaryColName]
159
+ if (undefined === value) continue
160
+
161
+ if (false === uniqueValues.has(value)) {
162
+ uniqueValues.set(value, [hashObj(row)])
163
+ count++
164
+ } else {
165
+ uniqueValues.get(value).push(hashObj(row))
166
+ }
167
+ }
168
+
169
+ let sorted = [...uniqueValues.keys()]
170
+
171
+ if (legend.additionalCategories) {
172
+ legend.additionalCategories.forEach(additionalCategory => {
173
+ if (additionalCategory && indexOfIgnoreType(sorted, additionalCategory) === -1) {
174
+ sorted.push(additionalCategory)
175
+ }
176
+ })
177
+ }
178
+
179
+ // Apply custom sorting or regular sorting
180
+ let configuredOrder = legend.categoryValuesOrder ?? []
181
+
182
+ if (configuredOrder.length) {
183
+ sorted.sort((a, b) => {
184
+ let aVal = configuredOrder.indexOf(a)
185
+ let bVal = configuredOrder.indexOf(b)
186
+ if (aVal === bVal) return 0
187
+ if (aVal === -1) return 1
188
+ if (bVal === -1) return -1
189
+ return aVal - bVal
190
+ })
191
+ } else {
192
+ sorted.sort((a, b) => a - b)
193
+ }
194
+
195
+ // Add legend item for each
196
+ sorted.forEach(val => {
197
+ // Skip if this value is already a special class
198
+ if (result?.items?.some(item => item.value === val && item.special)) return
199
+ result.items.push({
200
+ value: val
201
+ })
202
+
203
+ let lastIdx = result.items.length - 1
204
+ let arr = uniqueValues.get(val)
205
+
206
+ if (arr) {
207
+ arr.forEach(hashedRow => newLegendMemo.set(hashedRow, lastIdx))
208
+ }
209
+ })
210
+
211
+ // Add color to new legend item (normal items only, not special classes)
212
+ for (let i = 0; i < result.items.length; i++) {
213
+ if (!result.items[i].special) {
214
+ result.items[i].color = applyColorToLegend(i, configObj, result.items)
215
+ }
216
+ }
217
+
218
+ // Now apply special class colors last, to overwrite if needed
219
+ for (let i = 0; i < result.items.length; i++) {
220
+ if (result.items[i].special) {
221
+ result.items[i].color = applyColorToLegend(i, configObj, result.items)
222
+ }
223
+ }
224
+
225
+ // Overwrite legendMemo for special class rows to ensure correct color lookup
226
+ result.items.forEach((item, idx) => {
227
+ if (item.special) {
228
+ // Find all rows in the data that match this special class value
229
+ let specialRows = data.filter(row => {
230
+ // If special class has a key, use it, otherwise use primaryColName
231
+ const key = legend.specialClasses.find(sc => String(sc.value) === String(item.value))?.key || primaryColName
232
+ return String(row[key]) === String(item.value)
233
+ })
234
+ specialRows.forEach(row => {
235
+ newLegendMemo.set(hashObj(row), idx)
236
+ })
237
+ }
238
+ })
239
+
240
+ legendMemo.current = newLegendMemo
241
+
242
+ // before returning the legend result
243
+ // add property for bin number and set to index location
244
+ setBinNumbers(result)
245
+
246
+ // Move all special legend items from "Special Classes" to the end of the legend
247
+ sortSpecialClassesLast(result, legend)
248
+
249
+ const assignSpecialClassLastIndex = (value, key) => {
250
+ const newIndex = result.items.findIndex(d => d.bin === value)
251
+ newLegendSpecialClassLastMemo.set(key, newIndex)
252
+ }
253
+
254
+ newLegendMemo.forEach(assignSpecialClassLastIndex)
255
+ legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
256
+
257
+ return result
258
+ }
259
+
260
+ let uniqueValues = {}
261
+ dataSet.forEach(datum => {
262
+ uniqueValues[datum[primaryColName]] = true
263
+ })
264
+
265
+ let legendNumber = Math.min(legend.numberOfItems, Object.keys(uniqueValues).length)
266
+
267
+ // Separate zero
268
+ if (true === legend.separateZero && !general.equalNumberOptIn) {
269
+ let addLegendItem = false
270
+
271
+ for (let i = 0; i < dataSet.length; i++) {
272
+ if (dataSet[i][primaryColName] === 0) {
273
+ addLegendItem = true
274
+
275
+ let row = dataSet.splice(i, 1)[0]
276
+
277
+ newLegendMemo.set(hashObj(row), result.items.length)
278
+ i--
279
+ }
280
+ }
281
+
282
+ if (addLegendItem) {
283
+ legendNumber -= 1 // This zero takes up one legend item
284
+
285
+ // Add new legend item
286
+ result.items.push({
287
+ min: 0,
288
+ max: 0
289
+ })
290
+
291
+ let lastIdx = result.items.length - 1
292
+
293
+ // Add color to new legend item
294
+ result.items[lastIdx].color = applyColorToLegend(lastIdx, configObj, result.items)
295
+ }
296
+ }
297
+
298
+ // Sort data for use in equalnumber or equalinterval
299
+ if (general.type !== 'us-geocode') {
300
+ dataSet = dataSet
301
+ .filter(row => typeof row[primaryColName] === 'number')
302
+ .sort((a, b) => {
303
+ let aNum = a[primaryColName]
304
+ let bNum = b[primaryColName]
305
+
306
+ return aNum - bNum
307
+ })
308
+ }
309
+
310
+ // Equal Number
311
+ if (legend.type === 'equalnumber') {
312
+ // start work on changing legend functionality
313
+ // FALSE === ignore old version for now.
314
+ if (!general.equalNumberOptIn) {
315
+ let numberOfRows = dataSet.length
316
+ let changingNumber = legendNumber
317
+ let remainder
318
+ let chunkAmt
319
+
320
+ // Loop through the array until it has been split into equal subarrays
321
+ while (numberOfRows > 0) {
322
+ remainder = numberOfRows % changingNumber
323
+ chunkAmt = Math.floor(numberOfRows / changingNumber)
324
+
325
+ if (remainder > 0) {
326
+ chunkAmt += 1
327
+ }
328
+
329
+ let removedRows = dataSet.splice(0, chunkAmt)
330
+
331
+ let min = removedRows[0][primaryColName],
332
+ max = removedRows[removedRows.length - 1][primaryColName]
333
+
334
+ // eslint-disable-next-line
335
+ removedRows.forEach(row => {
336
+ newLegendMemo.set(hashObj(row), result.items.length)
337
+ })
338
+
339
+ result.items.push({
340
+ min,
341
+ max
342
+ })
343
+
344
+ result.items[result.items.length - 1].color = applyColorToLegend(
345
+ result.items.length - 1,
346
+ configObj,
347
+ result.items
348
+ )
349
+
350
+ changingNumber -= 1
351
+ numberOfRows -= chunkAmt
352
+ }
353
+ } else {
354
+ let colors = colorPalettes[configObj.color]
355
+ let colorRange = colors.slice(0, legend.numberOfItems)
356
+
357
+ const getDomain = () => {
358
+ // backwards compatibility
359
+ if (columns?.primary?.roundToPlace !== undefined && general?.equalNumberOptIn) {
360
+ return _.uniq(
361
+ dataSet.map(item => Number(item[columns.primary.name]).toFixed(Number(columns?.primary?.roundToPlace)))
362
+ )
363
+ }
364
+ return _.uniq(dataSet.map(item => Math.round(Number(item[columns.primary.name]))))
365
+ }
366
+
367
+ const getBreaks = scale => {
368
+ // backwards compatibility
369
+ if (columns?.primary?.roundToPlace !== undefined && general?.equalNumberOptIn) {
370
+ return scale.quantiles().map(b => Number(b)?.toFixed(Number(columns?.primary?.roundToPlace)))
371
+ }
372
+ return scale.quantiles().map(item => Number(Math.round(item)))
373
+ }
374
+
375
+ let scale = d3
376
+ .scaleQuantile()
377
+ .domain(getDomain()) // min/max values
378
+ .range(colorRange) // set range to our colors array
379
+
380
+ const breaks = getBreaks(scale)
381
+ let cachedBreaks = null
382
+ if (!cachedBreaks) {
383
+ cachedBreaks = breaks
384
+ }
385
+
386
+ // if separating zero force it into breaks
387
+ if (cachedBreaks[0] !== 0) {
388
+ cachedBreaks.unshift(0)
389
+ }
390
+
391
+ // eslint-disable-next-line array-callback-return
392
+ cachedBreaks.map((item, index) => {
393
+ const setMin = index => {
394
+ let min = cachedBreaks[index]
395
+
396
+ // if first break is a seperated zero, min is zero
397
+ if (index === 0 && legend.separateZero) {
398
+ min = 0
399
+ }
400
+
401
+ // if we're on the second break, and separating out zero, increment min to 1.
402
+ if (index === 1 && legend.separateZero) {
403
+ min = 1
404
+ }
405
+
406
+ return min
407
+ }
408
+
409
+ const getDecimalPlace = n => {
410
+ return Math.pow(10, -n)
411
+ }
412
+
413
+ const setMax = index => {
414
+ let max = Number(breaks[index + 1]) - getDecimalPlace(Number(configObj?.columns?.primary?.roundToPlace))
415
+
416
+ if (index === 0 && legend.separateZero) {
417
+ max = 0
418
+ }
419
+
420
+ if (index + 1 === breaks.length) {
421
+ max = domainNums[domainNums.length - 1]
422
+ }
423
+
424
+ return max
425
+ }
426
+
427
+ let min = setMin(index)
428
+ let max = setMax(index)
429
+
430
+ result.items.push({
431
+ min,
432
+ max
433
+ })
434
+ result.items[result.items.length - 1].color = applyColorToLegend(
435
+ result.items.length - 1,
436
+ configObj,
437
+ result.items
438
+ )
439
+
440
+ dataSet.forEach(row => {
441
+ let number = row[columns.primary.name]
442
+ let updated = result.items.length - 1
443
+
444
+ if (result.items?.[updated]?.min === undefined || result.items?.[updated]?.max === undefined) return
445
+
446
+ if (number >= result?.items?.[updated].min && number <= result?.items?.[updated].max) {
447
+ newLegendMemo.set(hashObj(row), updated)
448
+ }
449
+ })
450
+ })
451
+ }
452
+ }
453
+
454
+ // Equal Interval
455
+ if (legend.type === 'equalinterval' && dataSet?.length !== 0) {
456
+ if (!dataSet || dataSet.length === 0) {
457
+ setConfig({
458
+ ...configObj,
459
+ runtime: {
460
+ ...configObj.runtime,
461
+ editorErrorMessage: 'Error setting equal interval legend type'
462
+ }
463
+ })
464
+ return
465
+ }
466
+ dataSet = dataSet.filter(row => row[primaryColName] !== undefined)
467
+ let dataMin = dataSet[0][primaryColName]
468
+ let dataMax = dataSet[dataSet.length - 1][primaryColName]
469
+
470
+ let pointer = 0 // Start at beginning of dataSet
471
+
472
+ for (let i = 0; i < legendNumber; i++) {
473
+ let interval = Math.abs(dataMax - dataMin) / legendNumber
474
+
475
+ let min = dataMin + interval * i
476
+ let max = min + interval
477
+
478
+ // If this is the last loop, assign actual max of data as the end point
479
+ if (i === legendNumber - 1) max = dataMax
480
+
481
+ // Add rows in dataSet that belong to this new legend item since we've got the data sorted
482
+ while (pointer < dataSet.length && dataSet[pointer][primaryColName] <= max) {
483
+ newLegendMemo.set(hashObj(dataSet[pointer]), result.items.length)
484
+ pointer += 1
485
+ }
486
+
487
+ let range = {
488
+ min: Math.round(min * 100) / 100,
489
+ max: Math.round(max * 100) / 100
490
+ }
491
+
492
+ result.items.push(range)
493
+
494
+ result.items[result.items.length - 1].color = applyColorToLegend(
495
+ result.items.length - 1,
496
+ configObj,
497
+ result.items
498
+ )
499
+ }
500
+ }
501
+
502
+ setBinNumbers(result)
503
+
504
+ legendMemo.current = newLegendMemo
505
+
506
+ if (general.geoType === 'world') {
507
+ const runtimeDataKeys = Object.keys(runtimeData)
508
+ const isCountriesWithNoDataState =
509
+ data === undefined ? false : !countryKeys.every(countryKey => runtimeDataKeys.includes(countryKey))
510
+
511
+ if (result.items.length > 0 && isCountriesWithNoDataState) {
512
+ result.items.push({
513
+ min: null,
514
+ max: null,
515
+ color: getGeoFillColor(configObj)
516
+ })
517
+ }
518
+ }
519
+
520
+ setBinNumbers(result)
521
+ sortSpecialClassesLast(result, legend)
522
+
523
+ const assignSpecialClassLastIndex = (value, key) => {
524
+ const newIndex = result.items.findIndex(d => d.bin === value)
525
+ newLegendSpecialClassLastMemo.set(key, newIndex)
526
+ }
527
+ newLegendMemo.forEach(assignSpecialClassLastIndex)
528
+ legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
529
+
530
+ return result
531
+ } catch (e) {
532
+ console.error(e)
533
+ return []
534
+ }
535
+ }
536
+
537
+ export default generateRuntimeLegend
@@ -1,22 +1,23 @@
1
1
  import { hashObj } from './hashObj'
2
+ import { MapConfig } from '../types/MapConfig'
2
3
 
3
- export const generateRuntimeLegendHash = (state, runtimeFilters) => {
4
+ export const generateRuntimeLegendHash = (config: MapConfig, runtimeFilters) => {
4
5
  return hashObj({
5
- unified: state.legend.unified ?? false,
6
- equalNumberOptIn: state.general.equalNumberOptIn ?? false,
7
- specialClassesLast: state.legend.showSpecialClassesLast ?? false,
8
- color: state.color,
9
- customColors: state.customColors,
10
- numberOfItems: state.legend.numberOfItems,
11
- type: state.legend.type,
12
- separateZero: state.legend.separateZero ?? false,
13
- primary: state.columns.primary.name,
14
- categoryValuesOrder: state.legend.categoryValuesOrder,
15
- specialClasses: state.legend.specialClasses,
16
- geoType: state.general.geoType,
17
- data: state.data,
6
+ unified: config.legend.unified ?? false,
7
+ equalNumberOptIn: config.general.equalNumberOptIn ?? false,
8
+ specialClassesLast: config.legend.showSpecialClassesLast ?? false,
9
+ color: config.color,
10
+ customColors: config.customColors,
11
+ numberOfItems: config.legend.numberOfItems,
12
+ type: config.legend.type,
13
+ separateZero: config.legend.separateZero ?? false,
14
+ primary: config.columns.primary.name,
15
+ categoryValuesOrder: config.legend.categoryValuesOrder,
16
+ specialClasses: config.legend.specialClasses,
17
+ geoType: config.general.geoType,
18
+ data: config.data,
18
19
  filters: {
19
- ...state.filters
20
+ ...config.filters
20
21
  },
21
22
  ...runtimeFilters
22
23
  })
@@ -0,0 +1,19 @@
1
+ import { type MapConfig } from '../types/MapConfig'
2
+
3
+ type ColumnNames = {
4
+ geoColumnName: string | null
5
+ primaryColumnName: string | null
6
+ latitudeColumnName: string | null
7
+ longitudeColumnName: string | null
8
+ categoricalColumnName: string | null
9
+ } | null
10
+
11
+ export const getColumnNames = (columns?: Pick<MapConfig, 'columns'>): ColumnNames => {
12
+ if (!columns) return null
13
+ const geoColumnName = columns.geo?.name || null
14
+ const primaryColumnName = columns.primary?.name || null
15
+ const latitudeColumnName = columns.latitude?.name || null
16
+ const longitudeColumnName = columns.longitude?.name || null
17
+ const categoricalColumnName = columns.categorical?.name || null
18
+ return { geoColumnName, primaryColumnName, latitudeColumnName, longitudeColumnName, categoricalColumnName }
19
+ }
@@ -0,0 +1,23 @@
1
+ import { type MapConfig } from './../types/MapConfig'
2
+
3
+ export const getMapContainerClasses = (state: MapConfig, modal) => {
4
+ const { general } = state
5
+
6
+ let mapContainerClasses = [
7
+ 'map-container',
8
+ state.legend?.position,
9
+ state.general.type,
10
+ state.general.geoType,
11
+ 'outline-none',
12
+ 'position-relative'
13
+ ]
14
+
15
+ if (modal) {
16
+ mapContainerClasses.push('modal-background')
17
+ }
18
+
19
+ if (general.type === 'navigation' && true === general.fullBorder) {
20
+ mapContainerClasses.push('full-border')
21
+ }
22
+ return mapContainerClasses
23
+ }
@@ -0,0 +1,31 @@
1
+ import { type MapConfig } from '../types/MapConfig'
2
+
3
+ export const handleMapTabbing = (state: MapConfig, loading: boolean, legendId: string) => {
4
+ const { general, runtime, table } = state
5
+
6
+ const hasDataTable =
7
+ runtime?.editorErrorMessage.length === 0 &&
8
+ true === table.forceDisplay &&
9
+ general.type !== 'navigation' &&
10
+ false === loading
11
+
12
+ let tabbingID: string
13
+
14
+ // 1) skip to legend
15
+ if (general.showSidebar) {
16
+ tabbingID = legendId
17
+ }
18
+
19
+ // 2) skip to data table if it exists and not a navigation map
20
+ if (hasDataTable && !general.showSidebar) {
21
+ tabbingID = `dataTableSection__${Date.now()}`
22
+ }
23
+
24
+ // 3) if it's a navigation map skip to the dropdown.
25
+ if (state.general.type === 'navigation') {
26
+ tabbingID = `dropdown-${Date.now()}`
27
+ }
28
+
29
+ // 4) handle other options
30
+ return tabbingID || '!'
31
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export const hashObj = row => {
7
7
  try {
8
- if (!row || row === undefined) throw new Error('No row supplied to hashObj')
8
+ if (!row || row === undefined) return null
9
9
 
10
10
  let str = JSON.stringify(row)
11
11
  let hash = 0
@@ -0,0 +1,22 @@
1
+ export { addUIDs } from './addUIDs'
2
+ export { applyColorToLegend } from './applyColorToLegend'
3
+ export { colorDistributions } from './colorDistributions'
4
+ export { displayGeoName } from './displayGeoName'
5
+ export { formatLegendLocation } from './formatLegendLocation'
6
+ export { generateColorsArray } from './generateColorsArray'
7
+ export { generateRuntimeLegendHash } from './generateRuntimeLegendHash'
8
+ export { getGeoStrokeColor, getGeoFillColor } from './colors'
9
+ export { getUniqueValues } from './getUniqueValues'
10
+ export { handleMapAriaLabels } from './handleMapAriaLabels'
11
+ export { handleMapTabbing } from './handleMapTabbing'
12
+ export { hashObj } from './hashObj'
13
+ export { indexOfIgnoreType } from './indexOfIgnoreType'
14
+ export { navigationHandler } from './navigationHandler'
15
+ export { resetLegendToggles } from './resetLegendToggles'
16
+ export { setBinNumbers } from './setBinNumbers'
17
+ export { sortSpecialClassesLast } from './sortSpecialClassesLast'
18
+ export { titleCase as toTitleCase } from './toTitleCase'
19
+ export { titleCase } from './titleCase'
20
+ export { validateFipsCodeLength } from './validateFipsCodeLength'
21
+ export { getMapContainerClasses } from './getMapContainerClasses'
22
+ export { SVG_HEIGHT, SVG_WIDTH, SVG_PADDING, SVG_VIEWBOX, HEADER_COLORS, MAX_ZOOM_LEVEL } from './constants'
@@ -9,9 +9,9 @@ export const navigationHandler = (
9
9
  return
10
10
  }
11
11
 
12
- // Abort if value is blank
13
- if (0 === urlString.length) {
14
- throw Error('Blank string passed as URL. Navigation aborted.')
12
+ // Abort if urlString is not a valid string
13
+ if (typeof urlString !== 'string' || urlString.trim().length === 0) {
14
+ throw Error('Invalid or blank URL. Navigation aborted.')
15
15
  }
16
16
 
17
17
  const urlObj = new URL(urlString, window.location.origin)