@cdc/map 4.25.3 → 4.25.6

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 (119) hide show
  1. package/.idea/map.iml +12 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/vcs.xml +6 -0
  4. package/dist/cdcmap.js +31254 -32242
  5. package/examples/hex-colors.json +3 -3
  6. package/examples/m2.json +32904 -0
  7. package/examples/private/test.json +470 -1457
  8. package/examples/private/{mmr.json → wastewatermap.json} +86 -115
  9. package/index.html +36 -63
  10. package/package.json +7 -19
  11. package/src/CdcMap.tsx +56 -1552
  12. package/src/CdcMapComponent.tsx +608 -0
  13. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +10 -0
  14. package/src/_stories/CdcMap.Legend.stories.tsx +67 -0
  15. package/src/_stories/CdcMap.Table.stories.tsx +19 -0
  16. package/src/_stories/CdcMap.stories.tsx +12 -1
  17. package/src/_stories/UsaMap.NoData.stories.tsx +4 -4
  18. package/src/_stories/_mock/default-patterns.json +8 -5
  19. package/src/_stories/_mock/legend-bins.json +428 -0
  20. package/{examples/private/default-patterns.json → src/_stories/_mock/legends/legend-tests.json} +36 -131
  21. package/src/cdcMapComponent.styles.css +9 -0
  22. package/src/components/Annotation/Annotation.Draggable.tsx +27 -26
  23. package/src/components/Annotation/AnnotationDropdown.tsx +5 -6
  24. package/src/components/BubbleList.tsx +135 -49
  25. package/src/components/CityList.tsx +89 -87
  26. package/src/components/DataTable.tsx +8 -8
  27. package/src/components/EditorPanel/components/EditorPanel.tsx +823 -885
  28. package/src/components/EditorPanel/components/Error.tsx +9 -2
  29. package/src/components/EditorPanel/components/HexShapeSettings.tsx +127 -141
  30. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +55 -86
  31. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +89 -75
  32. package/src/components/EditorPanel/components/editorPanel.styles.css +95 -0
  33. package/src/components/Geo.tsx +9 -1
  34. package/src/components/GoogleMap/components/GoogleMap.tsx +1 -1
  35. package/src/components/Legend/components/Legend.tsx +92 -87
  36. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +128 -0
  37. package/src/components/Legend/components/LegendGroup/legend.group.css +27 -0
  38. package/src/components/Legend/components/LegendItem.Hex.tsx +4 -1
  39. package/src/components/Legend/components/index.scss +74 -17
  40. package/src/components/Modal.tsx +17 -7
  41. package/src/components/NavigationMenu.tsx +11 -9
  42. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +12 -8
  43. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +4 -4
  44. package/src/components/UsaMap/components/TerritoriesSection.tsx +33 -10
  45. package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +12 -10
  46. package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +12 -14
  47. package/src/components/UsaMap/components/Territory/TerritoryShape.ts +2 -1
  48. package/src/components/UsaMap/components/UsaMap.County.tsx +138 -96
  49. package/src/components/UsaMap/components/UsaMap.Region.styles.css +72 -0
  50. package/src/components/UsaMap/components/UsaMap.Region.tsx +56 -103
  51. package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +10 -0
  52. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +65 -74
  53. package/src/components/UsaMap/components/UsaMap.State.tsx +112 -91
  54. package/src/components/UsaMap/helpers/map.ts +1 -1
  55. package/src/components/UsaMap/helpers/shapes.ts +20 -7
  56. package/src/components/WorldMap/WorldMap.tsx +64 -118
  57. package/src/components/WorldMap/worldMap.styles.css +28 -0
  58. package/src/components/ZoomControls.tsx +15 -13
  59. package/src/components/zoomControls.styles.css +53 -0
  60. package/src/context.ts +17 -9
  61. package/src/data/initial-state.js +5 -2
  62. package/src/helpers/addUIDs.ts +150 -0
  63. package/src/helpers/applyColorToLegend.ts +39 -64
  64. package/src/helpers/applyLegendToRow.ts +51 -0
  65. package/src/helpers/colorDistributions.ts +12 -0
  66. package/src/helpers/constants.ts +44 -0
  67. package/src/helpers/displayGeoName.ts +9 -2
  68. package/src/helpers/formatLegendLocation.ts +3 -2
  69. package/src/helpers/generateColorsArray.ts +2 -1
  70. package/src/helpers/generateRuntimeData.ts +78 -0
  71. package/src/helpers/generateRuntimeFilters.ts +63 -0
  72. package/src/helpers/generateRuntimeLegend.ts +566 -0
  73. package/src/helpers/generateRuntimeLegendHash.ts +16 -15
  74. package/src/helpers/getColumnNames.ts +19 -0
  75. package/src/helpers/getMapContainerClasses.ts +23 -0
  76. package/src/helpers/getStatePicked.ts +8 -0
  77. package/src/helpers/handleMapTabbing.ts +31 -0
  78. package/src/helpers/hashObj.ts +1 -1
  79. package/src/helpers/index.ts +22 -0
  80. package/src/helpers/navigationHandler.ts +3 -3
  81. package/src/helpers/resetLegendToggles.ts +13 -0
  82. package/src/helpers/setBinNumbers.ts +5 -0
  83. package/src/helpers/sortSpecialClassesLast.ts +7 -0
  84. package/src/helpers/tests/getColumnNames.test.ts +52 -0
  85. package/src/helpers/titleCase.ts +1 -1
  86. package/src/helpers/toggleLegendActive.ts +25 -0
  87. package/src/hooks/useApplyTooltipsToGeo.tsx +51 -0
  88. package/src/hooks/useColumnsRequiredChecker.ts +51 -0
  89. package/src/hooks/useGeoClickHandler.ts +45 -0
  90. package/src/hooks/useLegendSeparators.ts +26 -0
  91. package/src/hooks/useMapLayers.tsx +34 -60
  92. package/src/hooks/useModal.ts +22 -0
  93. package/src/hooks/useResizeObserver.ts +4 -5
  94. package/src/hooks/useStateZoom.tsx +52 -75
  95. package/src/hooks/useTooltip.ts +2 -3
  96. package/src/index.jsx +3 -9
  97. package/src/scss/editor-panel.scss +3 -99
  98. package/src/scss/main.scss +1 -19
  99. package/src/scss/map.scss +15 -220
  100. package/src/store/map.actions.ts +46 -0
  101. package/src/store/map.reducer.ts +96 -0
  102. package/src/types/Annotations.ts +24 -0
  103. package/src/types/MapConfig.ts +23 -3
  104. package/src/types/MapContext.ts +36 -35
  105. package/src/types/Modal.ts +1 -0
  106. package/src/types/RuntimeData.ts +3 -0
  107. package/examples/private/DEV-9644.json +0 -184
  108. package/examples/private/DEV-9989.json +0 -229
  109. package/examples/private/ardi.json +0 -180
  110. package/examples/private/colors 2.json +0 -416
  111. package/examples/private/colors.json +0 -416
  112. package/examples/private/colors.json.zip +0 -0
  113. package/examples/private/customColors.json +0 -45348
  114. package/examples/test.json +0 -183
  115. package/src/helpers/closeModal.ts +0 -9
  116. package/src/scss/btn.scss +0 -69
  117. package/src/scss/filters.scss +0 -27
  118. package/src/scss/variables.scss +0 -1
  119. /package/src/hooks/{useActiveElement.js → useActiveElement.ts} +0 -0
@@ -0,0 +1,566 @@
1
+ import { useCallback, useContext } from 'react'
2
+ import ConfigContext from '../context'
3
+ import {
4
+ addUIDs,
5
+ applyColorToLegend,
6
+ getGeoFillColor,
7
+ hashObj,
8
+ indexOfIgnoreType,
9
+ setBinNumbers,
10
+ sortSpecialClassesLast
11
+ } from '.'
12
+
13
+ import _ from 'lodash'
14
+ import * as d3 from 'd3'
15
+
16
+ // Cdc
17
+ import colorPalettes from '@cdc/core/data/colorPalettes'
18
+ import { supportedCountries } from '../data/supported-geos'
19
+
20
+ type LegendItem = {
21
+ special?: boolean
22
+ value: string | number
23
+ label?: string
24
+ color?: string
25
+ min?: number
26
+ max?: number
27
+ bin?: number
28
+ }
29
+
30
+ export type GeneratedLegend = {
31
+ fromHash: number
32
+ runtimeDataHash: number
33
+ items: LegendItem[] | []
34
+ }
35
+
36
+ export const generateRuntimeLegend = (
37
+ configObj,
38
+ runtimeData: object[],
39
+ hash: string,
40
+ setConfig: Function,
41
+ runtimeFilters: object[],
42
+ legendMemo: React.MutableRefObject<Map<string, number>>,
43
+ legendSpecialClassLastMemo: React.MutableRefObject<Map<string, number>>
44
+ ): GeneratedLegend | [] => {
45
+ try {
46
+ // Throw errors if args missing
47
+ if (!runtimeData) Error('No runtime data provided')
48
+ if (!hash) Error('No hash provided')
49
+ if (!configObj) Error('No config object provided')
50
+ if (!legendMemo) Error('No legend memo provided')
51
+ if (!legendSpecialClassLastMemo) Error('No legend special class last memo provided')
52
+
53
+ // Define variables..
54
+ const newLegendMemo = new Map() // Reset memoization
55
+ const newLegendSpecialClassLastMemo = new Map() // Reset bin memoization
56
+ const countryKeys = Object.keys(supportedCountries)
57
+ const { legend, columns, general } = configObj
58
+ const primaryColName = columns.primary.name
59
+ const isBubble = general.type === 'bubble'
60
+ const categoricalCol = columns.categorical ? columns.categorical.name : undefined
61
+
62
+ // filter out rows without a geo column
63
+ addUIDs(configObj, configObj.columns.geo.name)
64
+ const data = configObj.data.filter(row => row.uid) // Filter out rows without UIDs
65
+
66
+ const result = {
67
+ fromHash: null,
68
+ runtimeDataHash: null,
69
+ items: []
70
+ }
71
+
72
+ // Add a hash for what we're working from if passed
73
+ if (hash) {
74
+ result.fromHash = hash
75
+ }
76
+
77
+ result.runtimeDataHash = runtimeFilters?.fromHash
78
+
79
+ // Unified will base the legend off ALL the data maps received. Otherwise, it will use
80
+ let dataSet = legend.unified ? data : Object?.values(runtimeData)
81
+
82
+ let domainNums = Array.from(new Set(dataSet?.map(item => item[configObj.columns.primary.name])))
83
+ .filter(d => typeof d === 'number' && !isNaN(d))
84
+ .sort((a, b) => a - b)
85
+
86
+ let specialClasses = 0
87
+ let specialClassesHash = {}
88
+
89
+ // Special classes
90
+ if (legend.specialClasses.length) {
91
+ if (typeof legend.specialClasses[0] === 'object') {
92
+ legend.specialClasses.forEach(specialClass => {
93
+ dataSet = dataSet.filter(row => {
94
+ const val = String(row[specialClass.key])
95
+
96
+ if (specialClass.value === val) {
97
+ if (undefined === specialClassesHash[val]) {
98
+ specialClassesHash[val] = true
99
+
100
+ result.items.push({
101
+ special: true,
102
+ value: val,
103
+ label: specialClass.label
104
+ })
105
+
106
+ result.items[result.items.length - 1].color = applyColorToLegend(
107
+ result.items.length - 1,
108
+ configObj,
109
+ result.items
110
+ )
111
+
112
+ specialClasses += 1
113
+ }
114
+
115
+ let specialColor: number
116
+
117
+ // color the configObj if val is in row
118
+ specialColor = result.items.findIndex(p => p.value === val)
119
+
120
+ newLegendMemo.set(hashObj(row), specialColor)
121
+
122
+ return false
123
+ }
124
+
125
+ return true
126
+ })
127
+ })
128
+ }
129
+ }
130
+
131
+ // Category
132
+ if (legend.type === 'category') {
133
+ let uniqueValues = new Map()
134
+ let count = 0
135
+
136
+ for (let i = 0; i < dataSet.length; i++) {
137
+ let row = dataSet[i]
138
+ let value = isBubble && categoricalCol && row[categoricalCol] ? row[categoricalCol] : row[primaryColName]
139
+ if (undefined === value) continue
140
+
141
+ if (false === uniqueValues.has(value)) {
142
+ uniqueValues.set(value, [hashObj(row)])
143
+ count++
144
+ } else {
145
+ uniqueValues.get(value).push(hashObj(row))
146
+ }
147
+ }
148
+
149
+ let sorted = [...uniqueValues.keys()]
150
+
151
+ if (legend.additionalCategories) {
152
+ legend.additionalCategories.forEach(additionalCategory => {
153
+ if (additionalCategory && indexOfIgnoreType(sorted, additionalCategory) === -1) {
154
+ sorted.push(additionalCategory)
155
+ }
156
+ })
157
+ }
158
+
159
+ // Apply custom sorting or regular sorting
160
+ let configuredOrder = legend.categoryValuesOrder ?? []
161
+
162
+ if (configuredOrder.length) {
163
+ sorted.sort((a, b) => {
164
+ let aVal = configuredOrder.indexOf(a)
165
+ let bVal = configuredOrder.indexOf(b)
166
+ if (aVal === bVal) return 0
167
+ if (aVal === -1) return 1
168
+ if (bVal === -1) return -1
169
+ return aVal - bVal
170
+ })
171
+ } else {
172
+ sorted.sort((a, b) => a - b)
173
+ }
174
+
175
+ // Add legend item for each
176
+ sorted.forEach(val => {
177
+ // Skip if this value is already a special class
178
+ if (result?.items?.some(item => item.value === val && item.special)) return
179
+ result.items.push({
180
+ value: val
181
+ })
182
+
183
+ let lastIdx = result.items.length - 1
184
+ let arr = uniqueValues.get(val)
185
+
186
+ 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
+ })
217
+ }
218
+ })
219
+
220
+ legendMemo.current = newLegendMemo
221
+
222
+ // before returning the legend result
223
+ // add property for bin number and set to index location
224
+ setBinNumbers(result)
225
+
226
+ // Move all special legend items from "Special Classes" to the end of the legend
227
+ sortSpecialClassesLast(result, legend)
228
+
229
+ const assignSpecialClassLastIndex = (value, key) => {
230
+ const newIndex = result.items.findIndex(d => d.bin === value)
231
+ newLegendSpecialClassLastMemo.set(key, newIndex)
232
+ }
233
+
234
+ newLegendMemo.forEach(assignSpecialClassLastIndex)
235
+ legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
236
+
237
+ return result
238
+ }
239
+
240
+ let uniqueValues = {}
241
+ dataSet.forEach(datum => {
242
+ uniqueValues[datum[primaryColName]] = true
243
+ })
244
+
245
+ let legendNumber = Math.min(legend.numberOfItems, Object.keys(uniqueValues).length)
246
+
247
+ // Separate zero
248
+ if (true === legend.separateZero && !general.equalNumberOptIn) {
249
+ let addLegendItem = false
250
+
251
+ for (let i = 0; i < dataSet.length; i++) {
252
+ if (dataSet[i][primaryColName] === 0) {
253
+ addLegendItem = true
254
+
255
+ let row = dataSet.splice(i, 1)[0]
256
+
257
+ newLegendMemo.set(hashObj(row), result.items.length)
258
+ i--
259
+ }
260
+ }
261
+
262
+ if (addLegendItem) {
263
+ legendNumber -= 1 // This zero takes up one legend item
264
+
265
+ // Add new legend item
266
+ result.items.push({
267
+ min: 0,
268
+ max: 0
269
+ })
270
+
271
+ let lastIdx = result.items.length - 1
272
+
273
+ // Add color to new legend item
274
+ result.items[lastIdx].color = applyColorToLegend(lastIdx, configObj, result.items)
275
+ }
276
+ }
277
+
278
+ // Sort data for use in equalnumber or equalinterval
279
+ if (general.type !== 'us-geocode') {
280
+ dataSet = dataSet
281
+ .filter(row => typeof row[primaryColName] === 'number')
282
+ .sort((a, b) => {
283
+ let aNum = a[primaryColName]
284
+ let bNum = b[primaryColName]
285
+
286
+ return aNum - bNum
287
+ })
288
+ }
289
+
290
+ // Equal Number
291
+ if (legend.type === 'equalnumber') {
292
+ // start work on changing legend functionality
293
+ // FALSE === ignore old version for now.
294
+ if (!general.equalNumberOptIn) {
295
+ let numberOfRows = dataSet.length
296
+ let changingNumber = legendNumber
297
+ let remainder
298
+ let chunkAmt
299
+
300
+ // Loop through the array until it has been split into equal subarrays
301
+ while (numberOfRows > 0) {
302
+ remainder = numberOfRows % changingNumber
303
+ chunkAmt = Math.floor(numberOfRows / changingNumber)
304
+
305
+ if (remainder > 0) {
306
+ chunkAmt += 1
307
+ }
308
+
309
+ let removedRows = dataSet.splice(0, chunkAmt)
310
+
311
+ let min = removedRows[0][primaryColName],
312
+ max = removedRows[removedRows.length - 1][primaryColName]
313
+
314
+ // eslint-disable-next-line
315
+ removedRows.forEach(row => {
316
+ newLegendMemo.set(hashObj(row), result.items.length)
317
+ })
318
+
319
+ result.items.push({
320
+ min,
321
+ max
322
+ })
323
+
324
+ result.items[result.items.length - 1].color = applyColorToLegend(
325
+ result.items.length - 1,
326
+ configObj,
327
+ result.items
328
+ )
329
+
330
+ changingNumber -= 1
331
+ numberOfRows -= chunkAmt
332
+ }
333
+ } else {
334
+ let colors = colorPalettes[configObj.color]
335
+ let colorRange = colors.slice(0, legend.numberOfItems)
336
+
337
+ 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]))))
345
+ }
346
+
347
+ 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)))
353
+ }
354
+
355
+ let scale = d3
356
+ .scaleQuantile()
357
+ .domain(getDomain()) // min/max values
358
+ .range(colorRange) // set range to our colors array
359
+
360
+ const breaks = getBreaks(scale)
361
+ let cachedBreaks = null
362
+ if (!cachedBreaks) {
363
+ cachedBreaks = breaks
364
+ }
365
+
366
+ // if separating zero force it into breaks
367
+ if (cachedBreaks[0] !== 0) {
368
+ cachedBreaks.unshift(0)
369
+ }
370
+
371
+ // eslint-disable-next-line array-callback-return
372
+ cachedBreaks.map((item, index) => {
373
+ const setMin = index => {
374
+ let min = cachedBreaks[index]
375
+
376
+ // if first break is a seperated zero, min is zero
377
+ if (index === 0 && legend.separateZero) {
378
+ min = 0
379
+ }
380
+
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
+ }
385
+
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
+ }
391
+
392
+ return min
393
+ }
394
+
395
+ const getDecimalPlace = n => {
396
+ return Math.pow(10, -n)
397
+ }
398
+
399
+ const setMax = index => {
400
+ let max = Number(breaks[index + 1])
401
+
402
+ if (index === 0 && legend.separateZero) {
403
+ max = 0
404
+ }
405
+
406
+ if (index + 1 === breaks.length) {
407
+ max = Number(domainNums[domainNums.length - 1])
408
+ }
409
+
410
+ return max
411
+ }
412
+
413
+ let min = setMin(index)
414
+ let max = setMax(index)
415
+
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
+ )
425
+
426
+ dataSet.forEach(row => {
427
+ let number = row[columns.primary.name]
428
+ let updated = result.items.length - 1
429
+
430
+ if (result.items?.[updated]?.min === undefined || result.items?.[updated]?.max === undefined) return
431
+
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)
436
+ }
437
+ }
438
+ })
439
+ })
440
+
441
+ // Final pass: handle any unassigned rows
442
+ dataSet.forEach(row => {
443
+ if (!newLegendMemo.has(hashObj(row))) {
444
+ let number = row[columns.primary.name]
445
+ let assigned = false
446
+
447
+ // Find the correct range for this value - check both boundaries
448
+ for (let itemIndex = 0; itemIndex < result.items.length; itemIndex++) {
449
+ const item = result.items[itemIndex]
450
+
451
+ if (item.min === undefined || item.max === undefined) continue
452
+
453
+ // Check if value falls within range (inclusive of both min and max)
454
+ if (number >= item.min && number <= item.max) {
455
+ newLegendMemo.set(hashObj(row), itemIndex)
456
+ assigned = true
457
+ break
458
+ }
459
+ }
460
+
461
+ // Fallback: if still not assigned, assign to closest range
462
+ if (!assigned) {
463
+ console.warn('Value not assigned to any range:', number, 'assigning to closest range')
464
+ let closestIndex = 0
465
+ let minDistance = Math.abs(number - ((result.items[0].min + result.items[0].max) / 2))
466
+
467
+ for (let i = 1; i < result.items.length; i++) {
468
+ const midpoint = (result.items[i].min + result.items[i].max) / 2
469
+ const distance = Math.abs(number - midpoint)
470
+ if (distance < minDistance) {
471
+ minDistance = distance
472
+ closestIndex = i
473
+ }
474
+ }
475
+
476
+ newLegendMemo.set(hashObj(row), closestIndex)
477
+ }
478
+ }
479
+ })
480
+ }
481
+ }
482
+
483
+ // Equal Interval
484
+ if (legend.type === 'equalinterval' && dataSet?.length !== 0) {
485
+ if (!dataSet || dataSet.length === 0) {
486
+ setConfig({
487
+ ...configObj,
488
+ runtime: {
489
+ ...configObj.runtime,
490
+ editorErrorMessage: 'Error setting equal interval legend type'
491
+ }
492
+ })
493
+ return
494
+ }
495
+ dataSet = dataSet.filter(row => row[primaryColName] !== undefined)
496
+ let dataMin = dataSet[0][primaryColName]
497
+ let dataMax = dataSet[dataSet.length - 1][primaryColName]
498
+
499
+ let pointer = 0 // Start at beginning of dataSet
500
+
501
+ for (let i = 0; i < legendNumber; i++) {
502
+ let interval = Math.abs(dataMax - dataMin) / legendNumber
503
+
504
+ let min = dataMin + interval * i
505
+ let max = min + interval
506
+
507
+ // If this is the last loop, assign actual max of data as the end point
508
+ if (i === legendNumber - 1) max = dataMax
509
+
510
+ // Add rows in dataSet that belong to this new legend item since we've got the data sorted
511
+ while (pointer < dataSet.length && dataSet[pointer][primaryColName] <= max) {
512
+ newLegendMemo.set(hashObj(dataSet[pointer]), result.items.length)
513
+ pointer += 1
514
+ }
515
+
516
+ let range = {
517
+ min: Math.round(min * 100) / 100,
518
+ max: Math.round(max * 100) / 100
519
+ }
520
+
521
+ result.items.push(range)
522
+
523
+ result.items[result.items.length - 1].color = applyColorToLegend(
524
+ result.items.length - 1,
525
+ configObj,
526
+ result.items
527
+ )
528
+ }
529
+ }
530
+
531
+ setBinNumbers(result)
532
+
533
+ legendMemo.current = newLegendMemo
534
+
535
+ if (general.geoType === 'world') {
536
+ const runtimeDataKeys = Object.keys(runtimeData)
537
+ const isCountriesWithNoDataState =
538
+ data === undefined ? false : !countryKeys.every(countryKey => runtimeDataKeys.includes(countryKey))
539
+
540
+ if (result.items.length > 0 && isCountriesWithNoDataState) {
541
+ result.items.push({
542
+ min: null,
543
+ max: null,
544
+ color: getGeoFillColor(configObj)
545
+ })
546
+ }
547
+ }
548
+
549
+ setBinNumbers(result)
550
+ sortSpecialClassesLast(result, legend)
551
+
552
+ const assignSpecialClassLastIndex = (value, key) => {
553
+ const newIndex = result.items.findIndex(d => d.bin === value)
554
+ newLegendSpecialClassLastMemo.set(key, newIndex)
555
+ }
556
+ newLegendMemo.forEach(assignSpecialClassLastIndex)
557
+ legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
558
+
559
+ return result
560
+ } catch (e) {
561
+ console.error(e)
562
+ return []
563
+ }
564
+ }
565
+
566
+ 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,8 @@
1
+ import { getFilterControllingStatePicked } from '../components/UsaMap/helpers/map'
2
+ import { supportedStatesFipsCodes } from '../data/supported-geos'
3
+
4
+ export const getStatePicked = (config, runtimeData) => {
5
+ const stateName = getFilterControllingStatePicked(config, runtimeData)
6
+ const fipsCode = Object.keys(supportedStatesFipsCodes).find(key => supportedStatesFipsCodes[key] === stateName)
7
+ return { stateName, fipsCode }
8
+ }
@@ -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