@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.
- package/.idea/map.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/dist/cdcmap.js +31254 -32242
- package/examples/hex-colors.json +3 -3
- package/examples/m2.json +32904 -0
- package/examples/private/test.json +470 -1457
- package/examples/private/{mmr.json → wastewatermap.json} +86 -115
- package/index.html +36 -63
- package/package.json +7 -19
- package/src/CdcMap.tsx +56 -1552
- package/src/CdcMapComponent.tsx +608 -0
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +10 -0
- package/src/_stories/CdcMap.Legend.stories.tsx +67 -0
- package/src/_stories/CdcMap.Table.stories.tsx +19 -0
- package/src/_stories/CdcMap.stories.tsx +12 -1
- package/src/_stories/UsaMap.NoData.stories.tsx +4 -4
- package/src/_stories/_mock/default-patterns.json +8 -5
- package/src/_stories/_mock/legend-bins.json +428 -0
- package/{examples/private/default-patterns.json → src/_stories/_mock/legends/legend-tests.json} +36 -131
- package/src/cdcMapComponent.styles.css +9 -0
- package/src/components/Annotation/Annotation.Draggable.tsx +27 -26
- package/src/components/Annotation/AnnotationDropdown.tsx +5 -6
- package/src/components/BubbleList.tsx +135 -49
- package/src/components/CityList.tsx +89 -87
- package/src/components/DataTable.tsx +8 -8
- package/src/components/EditorPanel/components/EditorPanel.tsx +823 -885
- package/src/components/EditorPanel/components/Error.tsx +9 -2
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +127 -141
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +55 -86
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +89 -75
- package/src/components/EditorPanel/components/editorPanel.styles.css +95 -0
- package/src/components/Geo.tsx +9 -1
- package/src/components/GoogleMap/components/GoogleMap.tsx +1 -1
- package/src/components/Legend/components/Legend.tsx +92 -87
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +128 -0
- package/src/components/Legend/components/LegendGroup/legend.group.css +27 -0
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -1
- package/src/components/Legend/components/index.scss +74 -17
- package/src/components/Modal.tsx +17 -7
- package/src/components/NavigationMenu.tsx +11 -9
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +12 -8
- package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +4 -4
- package/src/components/UsaMap/components/TerritoriesSection.tsx +33 -10
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +12 -10
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +12 -14
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +2 -1
- package/src/components/UsaMap/components/UsaMap.County.tsx +138 -96
- package/src/components/UsaMap/components/UsaMap.Region.styles.css +72 -0
- package/src/components/UsaMap/components/UsaMap.Region.tsx +56 -103
- package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +10 -0
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +65 -74
- package/src/components/UsaMap/components/UsaMap.State.tsx +112 -91
- package/src/components/UsaMap/helpers/map.ts +1 -1
- package/src/components/UsaMap/helpers/shapes.ts +20 -7
- package/src/components/WorldMap/WorldMap.tsx +64 -118
- package/src/components/WorldMap/worldMap.styles.css +28 -0
- package/src/components/ZoomControls.tsx +15 -13
- package/src/components/zoomControls.styles.css +53 -0
- package/src/context.ts +17 -9
- package/src/data/initial-state.js +5 -2
- package/src/helpers/addUIDs.ts +150 -0
- package/src/helpers/applyColorToLegend.ts +39 -64
- package/src/helpers/applyLegendToRow.ts +51 -0
- package/src/helpers/colorDistributions.ts +12 -0
- package/src/helpers/constants.ts +44 -0
- package/src/helpers/displayGeoName.ts +9 -2
- package/src/helpers/formatLegendLocation.ts +3 -2
- package/src/helpers/generateColorsArray.ts +2 -1
- package/src/helpers/generateRuntimeData.ts +78 -0
- package/src/helpers/generateRuntimeFilters.ts +63 -0
- package/src/helpers/generateRuntimeLegend.ts +566 -0
- package/src/helpers/generateRuntimeLegendHash.ts +16 -15
- package/src/helpers/getColumnNames.ts +19 -0
- package/src/helpers/getMapContainerClasses.ts +23 -0
- package/src/helpers/getStatePicked.ts +8 -0
- package/src/helpers/handleMapTabbing.ts +31 -0
- package/src/helpers/hashObj.ts +1 -1
- package/src/helpers/index.ts +22 -0
- package/src/helpers/navigationHandler.ts +3 -3
- package/src/helpers/resetLegendToggles.ts +13 -0
- package/src/helpers/setBinNumbers.ts +5 -0
- package/src/helpers/sortSpecialClassesLast.ts +7 -0
- package/src/helpers/tests/getColumnNames.test.ts +52 -0
- package/src/helpers/titleCase.ts +1 -1
- package/src/helpers/toggleLegendActive.ts +25 -0
- package/src/hooks/useApplyTooltipsToGeo.tsx +51 -0
- package/src/hooks/useColumnsRequiredChecker.ts +51 -0
- package/src/hooks/useGeoClickHandler.ts +45 -0
- package/src/hooks/useLegendSeparators.ts +26 -0
- package/src/hooks/useMapLayers.tsx +34 -60
- package/src/hooks/useModal.ts +22 -0
- package/src/hooks/useResizeObserver.ts +4 -5
- package/src/hooks/useStateZoom.tsx +52 -75
- package/src/hooks/useTooltip.ts +2 -3
- package/src/index.jsx +3 -9
- package/src/scss/editor-panel.scss +3 -99
- package/src/scss/main.scss +1 -19
- package/src/scss/map.scss +15 -220
- package/src/store/map.actions.ts +46 -0
- package/src/store/map.reducer.ts +96 -0
- package/src/types/Annotations.ts +24 -0
- package/src/types/MapConfig.ts +23 -3
- package/src/types/MapContext.ts +36 -35
- package/src/types/Modal.ts +1 -0
- package/src/types/RuntimeData.ts +3 -0
- package/examples/private/DEV-9644.json +0 -184
- package/examples/private/DEV-9989.json +0 -229
- package/examples/private/ardi.json +0 -180
- package/examples/private/colors 2.json +0 -416
- package/examples/private/colors.json +0 -416
- package/examples/private/colors.json.zip +0 -0
- package/examples/private/customColors.json +0 -45348
- package/examples/test.json +0 -183
- package/src/helpers/closeModal.ts +0 -9
- package/src/scss/btn.scss +0 -69
- package/src/scss/filters.scss +0 -27
- package/src/scss/variables.scss +0 -1
- /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 = (
|
|
4
|
+
export const generateRuntimeLegendHash = (config: MapConfig, runtimeFilters) => {
|
|
4
5
|
return hashObj({
|
|
5
|
-
unified:
|
|
6
|
-
equalNumberOptIn:
|
|
7
|
-
specialClassesLast:
|
|
8
|
-
color:
|
|
9
|
-
customColors:
|
|
10
|
-
numberOfItems:
|
|
11
|
-
type:
|
|
12
|
-
separateZero:
|
|
13
|
-
primary:
|
|
14
|
-
categoryValuesOrder:
|
|
15
|
-
specialClasses:
|
|
16
|
-
geoType:
|
|
17
|
-
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
|
-
...
|
|
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
|
+
}
|