@cdc/map 4.25.10 → 4.26.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.
- package/.claude/agents/typescript-organizer.md +118 -0
- package/dist/{cdcmap-fce76882.es.js → cdcmap-BnB1QM5d.es.js} +6 -13
- package/dist/{cdcmap-c55ac1ea.es.js → cdcmap-D6CG2-Hb.es.js} +5 -12
- package/dist/{cdcmap-31a33da1.es.js → cdcmap-MXgURbdZ.es.js} +6 -13
- package/dist/{cdcmap-1a1724a1.es.js → cdcmap-dgT_1dIT.es.js} +136 -151
- package/dist/cdcmap.js +58397 -55987
- package/examples/example-city-state.json +9 -1
- package/examples/multi-country-centering.json +45 -0
- package/examples/private/city_styles_variable.json +877 -0
- package/examples/private/colors-2.json +221 -0
- package/examples/private/colors.json +221 -0
- package/examples/private/map-filter-issue.json +2260 -0
- package/examples/private/map-legend.json +5303 -0
- package/index.html +27 -36
- package/package.json +6 -5
- package/src/CdcMapComponent.tsx +86 -26
- package/src/_stories/CdcMap.ColumnWrap.stories.tsx +31 -0
- package/src/_stories/CdcMap.DistrictOfColumbia.stories.tsx +320 -0
- package/src/_stories/CdcMap.Editor.stories.tsx +3426 -0
- package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
- package/src/_stories/CdcMap.stories.tsx +116 -4
- package/src/_stories/_mock/column-wrap-test.json +265 -0
- package/src/_stories/_mock/multi-country-hide.json +78 -0
- package/src/_stories/_mock/multi-country.json +95 -0
- package/src/_stories/_mock/multi-state.json +887 -20403
- package/src/_stories/_mock/small_multiples/multi-state-small-multiples.json +8399 -0
- package/src/_stories/_mock/small_multiples/region-small-multiples.json +657 -0
- package/src/_stories/_mock/small_multiples/wastewater-map-small-multiples.json +221 -0
- package/src/_stories/_mock/usa-state-gradient.json +3 -4
- package/src/components/BubbleList.tsx +1 -1
- package/src/components/CityList.tsx +24 -18
- package/src/components/EditorPanel/components/EditorPanel.tsx +2380 -2206
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +55 -93
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +0 -19
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +27 -37
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +351 -0
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/Geo.tsx +20 -3
- package/src/components/Legend/components/Legend.tsx +58 -75
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +1 -1
- package/src/components/Legend/components/index.scss +23 -6
- package/src/components/NavigationMenu.tsx +16 -13
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +163 -0
- package/src/components/SmallMultiples/SmallMultiples.css +32 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +150 -0
- package/src/components/SmallMultiples/SynchronizedTooltip.tsx +105 -0
- package/src/components/SmallMultiples/index.tsx +3 -0
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +18 -3
- package/src/components/UsaMap/components/TerritoriesSection.tsx +26 -12
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +30 -4
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +29 -9
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +7 -0
- package/src/components/UsaMap/components/UsaMap.County.tsx +16 -4
- package/src/components/UsaMap/components/UsaMap.Region.tsx +14 -1
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +29 -12
- package/src/components/UsaMap/components/UsaMap.State.tsx +30 -5
- package/src/components/UsaMap/helpers/map.ts +2 -2
- package/src/components/UsaMap/helpers/shapes.ts +9 -6
- package/src/components/WorldMap/WorldMap.tsx +81 -11
- package/src/data/initial-state.js +11 -0
- package/src/data/supported-geos.js +8 -76
- package/src/helpers/addUIDs.ts +13 -2
- package/src/helpers/applyColorToLegend.ts +25 -1
- package/src/helpers/applyLegendToRow.ts +5 -3
- package/src/helpers/constants.ts +3 -15
- package/src/helpers/displayGeoName.ts +22 -4
- package/src/helpers/generateRuntimeFilters.ts +1 -1
- package/src/helpers/generateRuntimeLegend.ts +1 -3
- package/src/helpers/generateRuntimeLegendHash.ts +1 -1
- package/src/helpers/getCountriesPicked.ts +103 -0
- package/src/helpers/getMapContainerClasses.ts +7 -0
- package/src/helpers/getPatternForRow.ts +2 -5
- package/src/helpers/index.ts +2 -4
- package/src/helpers/isLegendItemDisabled.ts +2 -2
- package/src/helpers/resetLegendToggles.ts +1 -0
- package/src/helpers/smallMultiplesHelpers.ts +359 -0
- package/src/helpers/tests/hashObj.test.ts +1 -1
- package/src/helpers/tests/titleCase.test.ts +76 -0
- package/src/helpers/titleCase.ts +13 -13
- package/src/helpers/toggleLegendActive.ts +76 -8
- package/src/helpers/urlDataHelpers.ts +1 -1
- package/src/hooks/useCountryZoom.tsx +241 -0
- package/src/hooks/useGeoClickHandler.ts +1 -1
- package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
- package/src/hooks/useResizeObserver.ts +8 -2
- package/src/hooks/useStateZoom.tsx +7 -4
- package/src/hooks/useSynchronizedGeographies.ts +56 -0
- package/src/index.jsx +1 -0
- package/src/scss/editor-panel.scss +4 -440
- package/src/scss/main.scss +1 -1
- package/src/scss/map.scss +12 -15
- package/src/store/map.actions.ts +7 -7
- package/src/test/CdcMap.test.jsx +1 -1
- package/src/types/MapConfig.ts +32 -11
- package/src/types/MapContext.ts +6 -0
- package/src/types/runtimeLegend.ts +2 -1
- package/LICENSE +0 -201
- package/src/components/DataTable.tsx +0 -413
- package/src/components/EditorPanel/components/Inputs.tsx +0 -59
- package/src/components/MapControls.tsx +0 -44
- package/src/helpers/getUniqueValues.ts +0 -19
- package/src/helpers/hashObj.ts +0 -25
- package/src/hooks/useActiveElement.ts +0 -19
- package/src/hooks/useLegendSeparators.ts +0 -26
- package/src/scss/mixins.scss +0 -47
- package/src/types/Annotations.ts +0 -24
- /package/dist/{cdcmap-548642e6.es.js → cdcmap-Ct2SB0vL.es.js} +0 -0
package/src/helpers/constants.ts
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
1
|
export const SVG_WIDTH = 880
|
|
2
2
|
export const SVG_HEIGHT = 500
|
|
3
|
-
export const SVG_PADDING =
|
|
3
|
+
export const SVG_PADDING = 25
|
|
4
4
|
export const SVG_VIEWBOX = `0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`
|
|
5
|
-
export const HEADER_COLORS = [
|
|
6
|
-
'theme-blue',
|
|
7
|
-
'theme-purple',
|
|
8
|
-
'theme-brown',
|
|
9
|
-
'theme-teal',
|
|
10
|
-
'theme-pink',
|
|
11
|
-
'theme-orange',
|
|
12
|
-
'theme-slate',
|
|
13
|
-
'theme-indigo',
|
|
14
|
-
'theme-cyan',
|
|
15
|
-
'theme-green',
|
|
16
|
-
'theme-amber'
|
|
17
|
-
]
|
|
18
5
|
export const MAX_ZOOM_LEVEL = 4
|
|
19
6
|
|
|
20
7
|
export const SUPPORTED_DC_NAMES = [
|
|
@@ -43,9 +30,10 @@ export const GEOCODE_TYPES = {
|
|
|
43
30
|
|
|
44
31
|
export const DEFAULT_MAP_BACKGROUND = '#DFE1E2'
|
|
45
32
|
|
|
33
|
+
export const DISABLED_MAP_COLOR = '#FFFFFF'
|
|
34
|
+
|
|
46
35
|
// Component constants
|
|
47
36
|
export const LOGO_MAX_WIDTH = '50px'
|
|
48
|
-
export const STORYBOOK_PORT = 6006
|
|
49
37
|
|
|
50
38
|
// CSV Parsing Configuration
|
|
51
39
|
export const CSV_PARSE_CONFIG = {
|
|
@@ -4,10 +4,12 @@ import {
|
|
|
4
4
|
supportedTerritories,
|
|
5
5
|
supportedCountries,
|
|
6
6
|
supportedCounties,
|
|
7
|
+
supportedCities,
|
|
7
8
|
stateKeys,
|
|
8
9
|
territoryKeys,
|
|
9
10
|
countryKeys,
|
|
10
|
-
countyKeys
|
|
11
|
+
countyKeys,
|
|
12
|
+
cityKeys
|
|
11
13
|
} from '../data/supported-geos'
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -20,14 +22,17 @@ import {
|
|
|
20
22
|
export const displayGeoName = (key: string, convertFipsCodes = true): string => {
|
|
21
23
|
if (!convertFipsCodes) return key
|
|
22
24
|
let value = key
|
|
25
|
+
let wasLookedUp = false
|
|
23
26
|
|
|
24
27
|
// Map to first item in values array which is the preferred label
|
|
25
28
|
if (stateKeys.includes(value)) {
|
|
26
29
|
value = titleCase(supportedStates[key][0])
|
|
30
|
+
wasLookedUp = true
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
if (territoryKeys.includes(value)) {
|
|
30
34
|
value = titleCase(supportedTerritories[key][0])
|
|
35
|
+
wasLookedUp = true
|
|
31
36
|
if (value === 'U.s. Virgin Islands') {
|
|
32
37
|
value = 'U.S. Virgin Islands'
|
|
33
38
|
}
|
|
@@ -35,27 +40,40 @@ export const displayGeoName = (key: string, convertFipsCodes = true): string =>
|
|
|
35
40
|
|
|
36
41
|
if (countryKeys.includes(value)) {
|
|
37
42
|
value = titleCase(supportedCountries[key][0])
|
|
43
|
+
wasLookedUp = true
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
if (countyKeys.includes(value)) {
|
|
41
47
|
value = titleCase(supportedCounties[key])
|
|
48
|
+
wasLookedUp = true
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
// Check dictionary replacements before city lookup to handle special cases like DC
|
|
44
52
|
const dict = {
|
|
45
53
|
'Washington D.C.': 'District of Columbia',
|
|
46
54
|
'WASHINGTON DC': 'District of Columbia',
|
|
47
55
|
DC: 'District of Columbia',
|
|
48
56
|
'WASHINGTON DC.': 'District of Columbia',
|
|
57
|
+
'DISTRICT OF COLUMBIA': 'District of Columbia',
|
|
58
|
+
Dc: 'District of Columbia',
|
|
49
59
|
Congo: 'Republic of the Congo'
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
if (Object.keys(dict).includes(value)) {
|
|
53
63
|
value = dict[value]
|
|
64
|
+
wasLookedUp = true
|
|
54
65
|
}
|
|
55
|
-
|
|
56
|
-
if (value
|
|
66
|
+
|
|
67
|
+
if (cityKeys.includes(value)) {
|
|
68
|
+
value = titleCase(String(value) || '')
|
|
69
|
+
wasLookedUp = true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If value was looked up from our dictionaries and needs formatting, or if it's a 2-letter abbreviation, return as-is
|
|
73
|
+
if (value?.length === 2 || value === 'U.S. Virgin Islands' || wasLookedUp) {
|
|
57
74
|
return value
|
|
58
75
|
} else {
|
|
59
|
-
|
|
76
|
+
// Apply titleCase to unrecognized values (e.g., "DISTRICT OF COLUMBIA" -> "District of Columbia")
|
|
77
|
+
return titleCase(value)
|
|
60
78
|
}
|
|
61
79
|
}
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
addUIDs,
|
|
5
5
|
applyColorToLegend,
|
|
6
6
|
getGeoFillColor,
|
|
7
|
-
hashObj,
|
|
8
7
|
indexOfIgnoreType,
|
|
9
8
|
setBinNumbers,
|
|
10
9
|
sortSpecialClassesLast
|
|
11
10
|
} from '.'
|
|
11
|
+
import { hashObj } from '@cdc/core/helpers/hashObj'
|
|
12
12
|
|
|
13
13
|
import _ from 'lodash'
|
|
14
14
|
import * as d3 from 'd3'
|
|
@@ -598,5 +598,3 @@ export const generateRuntimeLegend = (
|
|
|
598
598
|
}
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
|
-
|
|
602
|
-
export default generateRuntimeLegend
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { supportedCountries } from '../data/supported-geos'
|
|
2
|
+
import type { MapConfig } from '../types/MapConfig'
|
|
3
|
+
|
|
4
|
+
export interface CountryPickedInfo {
|
|
5
|
+
iso: string
|
|
6
|
+
name: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const getCountriesPicked = (config: MapConfig): CountryPickedInfo[] => {
|
|
10
|
+
if (!config.general.countriesPicked || config.general.countriesPicked.length === 0) {
|
|
11
|
+
return []
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return config.general.countriesPicked.map(country => {
|
|
15
|
+
// Validate that the ISO code exists in our supported countries
|
|
16
|
+
if (!supportedCountries[country.iso]) {
|
|
17
|
+
console.error(`Country ISO code "${country.iso}" not found in supported countries.`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
iso: country.iso,
|
|
22
|
+
name: country.name
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* ISO codes that are in supported-geos.js but don't have geometries in world-topo.json
|
|
29
|
+
* These are filtered out to prevent users from selecting countries that won't render
|
|
30
|
+
*/
|
|
31
|
+
const EXCLUDED_ISOS = new Set([
|
|
32
|
+
// US Territories (not in topology - grouped with USA or missing)
|
|
33
|
+
'ASM',
|
|
34
|
+
'GUM',
|
|
35
|
+
'MNP',
|
|
36
|
+
'VIR',
|
|
37
|
+
// Small territories/islands without separate geometries
|
|
38
|
+
'ALA',
|
|
39
|
+
'AIA',
|
|
40
|
+
'AND',
|
|
41
|
+
'ABW',
|
|
42
|
+
'BES',
|
|
43
|
+
'BMU',
|
|
44
|
+
'BVT',
|
|
45
|
+
'CXR',
|
|
46
|
+
'CCK',
|
|
47
|
+
'COK',
|
|
48
|
+
'CUW',
|
|
49
|
+
'FRO',
|
|
50
|
+
'GGY',
|
|
51
|
+
'HMD',
|
|
52
|
+
'IMN',
|
|
53
|
+
'JEY',
|
|
54
|
+
'LIE',
|
|
55
|
+
'MCO',
|
|
56
|
+
'MSR',
|
|
57
|
+
'NRU',
|
|
58
|
+
'NIU',
|
|
59
|
+
'NFK',
|
|
60
|
+
'PCN',
|
|
61
|
+
'SGS',
|
|
62
|
+
'SJM',
|
|
63
|
+
'TKL',
|
|
64
|
+
'TCA',
|
|
65
|
+
'TUV',
|
|
66
|
+
'VAT',
|
|
67
|
+
'WLF'
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Helper to get all supported countries formatted for dropdown options
|
|
72
|
+
* Filters to only valid ISO 3166-1 alpha-3 codes and removes countries without topology
|
|
73
|
+
*/
|
|
74
|
+
export const getSupportedCountryOptions = () => {
|
|
75
|
+
return Object.keys(supportedCountries)
|
|
76
|
+
.filter(iso => /^[A-Z]{3}$/.test(iso)) // Only proper 3-letter ISO codes
|
|
77
|
+
.filter(iso => !EXCLUDED_ISOS.has(iso)) // Exclude countries without topology
|
|
78
|
+
.map(iso => ({
|
|
79
|
+
value: iso,
|
|
80
|
+
label: supportedCountries[iso][0] // Use the first (primary) name
|
|
81
|
+
}))
|
|
82
|
+
.sort((a, b) => a.label.localeCompare(b.label)) // Sort alphabetically by name
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Helper to determine if the map should show only selected countries
|
|
87
|
+
* Returns true if countries are selected, false if showing all countries
|
|
88
|
+
*/
|
|
89
|
+
export const isMultiCountryActive = (config: MapConfig): boolean => {
|
|
90
|
+
return Boolean(config.general.countriesPicked && config.general.countriesPicked.length > 0)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Helper to determine display mode for unselected countries
|
|
95
|
+
* Returns 'hidden' if hideUnselectedCountries is true, 'grayed' if false (default)
|
|
96
|
+
*/
|
|
97
|
+
export const getUnselectedCountryDisplayMode = (config: MapConfig): 'hidden' | 'grayed' | 'normal' => {
|
|
98
|
+
if (!isMultiCountryActive(config)) {
|
|
99
|
+
return 'normal' // Show all countries normally when none are specifically selected
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return config.general.hideUnselectedCountries ? 'hidden' : 'grayed'
|
|
103
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type MapConfig } from './../types/MapConfig'
|
|
2
|
+
import { isMultiCountryActive } from './getCountriesPicked'
|
|
2
3
|
|
|
3
4
|
export const getMapContainerClasses = (state: MapConfig, modal) => {
|
|
4
5
|
const { general } = state
|
|
@@ -19,5 +20,11 @@ export const getMapContainerClasses = (state: MapConfig, modal) => {
|
|
|
19
20
|
if (general.type === 'navigation' && true === general.fullBorder) {
|
|
20
21
|
mapContainerClasses.push('full-border')
|
|
21
22
|
}
|
|
23
|
+
|
|
24
|
+
// Add multi-country class when multi-country mode is active
|
|
25
|
+
if (isMultiCountryActive(state)) {
|
|
26
|
+
mapContainerClasses.push('multi-country-selected')
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
return mapContainerClasses
|
|
23
30
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MapConfig } from '../types/MapConfig'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface PatternInfo {
|
|
4
4
|
pattern?: string
|
|
5
5
|
dataKey: string
|
|
6
6
|
size?: string
|
|
@@ -8,10 +8,7 @@ export interface PatternInfo {
|
|
|
8
8
|
color?: string
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export const getPatternForRow = (
|
|
12
|
-
rowObj: Record<string, any>,
|
|
13
|
-
config: MapConfig
|
|
14
|
-
): PatternInfo | null => {
|
|
11
|
+
export const getPatternForRow = (rowObj: Record<string, any>, config: MapConfig): PatternInfo | null => {
|
|
15
12
|
if (!config.map?.patterns || !rowObj) {
|
|
16
13
|
return null
|
|
17
14
|
}
|
package/src/helpers/index.ts
CHANGED
|
@@ -5,10 +5,8 @@ export { formatLegendLocation } from './formatLegendLocation'
|
|
|
5
5
|
export { generateColorsArray } from '@cdc/core/helpers/generateColorsArray'
|
|
6
6
|
export { generateRuntimeLegendHash } from './generateRuntimeLegendHash'
|
|
7
7
|
export { getGeoStrokeColor, getGeoFillColor } from './colors'
|
|
8
|
-
export { getUniqueValues } from './getUniqueValues'
|
|
9
8
|
export { handleMapAriaLabels } from './handleMapAriaLabels'
|
|
10
9
|
export { handleMapTabbing } from './handleMapTabbing'
|
|
11
|
-
export { hashObj } from './hashObj'
|
|
12
10
|
export { indexOfIgnoreType } from './indexOfIgnoreType'
|
|
13
11
|
export { isLegendItemDisabled } from './isLegendItemDisabled'
|
|
14
12
|
export { navigationHandler } from './navigationHandler'
|
|
@@ -24,7 +22,7 @@ export {
|
|
|
24
22
|
SVG_WIDTH,
|
|
25
23
|
SVG_PADDING,
|
|
26
24
|
SVG_VIEWBOX,
|
|
27
|
-
HEADER_COLORS,
|
|
28
25
|
MAX_ZOOM_LEVEL,
|
|
29
|
-
DEFAULT_MAP_BACKGROUND
|
|
26
|
+
DEFAULT_MAP_BACKGROUND,
|
|
27
|
+
DISABLED_MAP_COLOR
|
|
30
28
|
} from './constants'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { hashObj } from '
|
|
1
|
+
import { hashObj } from '@cdc/core/helpers/hashObj'
|
|
2
2
|
|
|
3
3
|
export const isLegendItemDisabled = (
|
|
4
4
|
dataForCheck: any,
|
|
@@ -12,5 +12,5 @@ export const isLegendItemDisabled = (
|
|
|
12
12
|
if (!legendMemo.current.has(hash)) return false
|
|
13
13
|
const idx = legendMemo.current.get(hash)
|
|
14
14
|
const disabledIdx = config.legend.showSpecialClassesLast ? legendSpecialClassLastMemo.current.get(hash) ?? idx : idx
|
|
15
|
-
return runtimeLegend.items[disabledIdx]?.disabled || false
|
|
15
|
+
return runtimeLegend.items[disabledIdx]?.disabled || runtimeLegend.items[disabledIdx]?.hidden || false
|
|
16
16
|
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { DataRow, MapConfig } from '../types/MapConfig'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get unique values from a specific column in the data
|
|
5
|
+
* These values will become the tiles in small multiples view
|
|
6
|
+
*
|
|
7
|
+
* @param data - The full dataset
|
|
8
|
+
* @param tileColumn - The column name to extract unique values from
|
|
9
|
+
* @returns Array of unique values, sorted alphabetically, with null/undefined filtered out
|
|
10
|
+
*/
|
|
11
|
+
export const getTileValues = (data: DataRow[], tileColumn: string): (string | number)[] => {
|
|
12
|
+
if (!data || !tileColumn) return []
|
|
13
|
+
|
|
14
|
+
const uniqueValues = Array.from(new Set(data.map(row => row[tileColumn])))
|
|
15
|
+
.filter(val => val != null && val !== '')
|
|
16
|
+
.sort()
|
|
17
|
+
|
|
18
|
+
return uniqueValues as (string | number)[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Filter data for a specific tile based on the tile column and value
|
|
23
|
+
*
|
|
24
|
+
* @param allData - The complete dataset
|
|
25
|
+
* @param tileColumn - The column to filter by
|
|
26
|
+
* @param tileValue - The value to filter for
|
|
27
|
+
* @returns Filtered data containing only rows where column === value
|
|
28
|
+
*/
|
|
29
|
+
export const getTileData = (allData: DataRow[], tileColumn: string, tileValue: string | number): DataRow[] => {
|
|
30
|
+
if (!allData || !tileColumn) return []
|
|
31
|
+
|
|
32
|
+
return allData.filter(row => row[tileColumn] === tileValue)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the display title for a tile
|
|
37
|
+
* Uses custom title if configured, otherwise returns the column value
|
|
38
|
+
*
|
|
39
|
+
* @param tileValue - The value from the tile column
|
|
40
|
+
* @param tileTitles - Object mapping values to custom titles
|
|
41
|
+
* @returns The display title for the tile
|
|
42
|
+
*/
|
|
43
|
+
export const getTileDisplayTitle = (tileValue: string | number, tileTitles?: { [key: string]: string }): string => {
|
|
44
|
+
if (tileTitles && tileTitles[String(tileValue)]) {
|
|
45
|
+
return tileTitles[String(tileValue)]
|
|
46
|
+
}
|
|
47
|
+
return String(tileValue)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Apply tile ordering based on configuration
|
|
52
|
+
* Supports ascending, descending, and custom ordering
|
|
53
|
+
*
|
|
54
|
+
* @param tileValues - Array of tile values to order
|
|
55
|
+
* @param orderType - Type of ordering: 'asc', 'desc', or 'custom'
|
|
56
|
+
* @param customOrder - Custom order array (used when orderType is 'custom')
|
|
57
|
+
* @param tileTitles - Custom titles for display (used for sorting by title)
|
|
58
|
+
* @returns Ordered array of tile values
|
|
59
|
+
*/
|
|
60
|
+
export const applyTileOrder = (
|
|
61
|
+
tileValues: (string | number)[],
|
|
62
|
+
orderType?: 'asc' | 'desc' | 'custom',
|
|
63
|
+
customOrder?: string[],
|
|
64
|
+
tileTitles?: { [key: string]: string }
|
|
65
|
+
): (string | number)[] => {
|
|
66
|
+
if (!orderType || !tileValues.length) {
|
|
67
|
+
return tileValues
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
switch (orderType) {
|
|
71
|
+
case 'asc':
|
|
72
|
+
return [...tileValues].sort((a, b) => {
|
|
73
|
+
const titleA = getTileDisplayTitle(a, tileTitles).toLowerCase()
|
|
74
|
+
const titleB = getTileDisplayTitle(b, tileTitles).toLowerCase()
|
|
75
|
+
return titleA.localeCompare(titleB)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
case 'desc':
|
|
79
|
+
return [...tileValues].sort((a, b) => {
|
|
80
|
+
const titleA = getTileDisplayTitle(a, tileTitles).toLowerCase()
|
|
81
|
+
const titleB = getTileDisplayTitle(b, tileTitles).toLowerCase()
|
|
82
|
+
return titleB.localeCompare(titleA)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
case 'custom':
|
|
86
|
+
if (!customOrder || customOrder.length === 0) {
|
|
87
|
+
return tileValues
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Sort tiles based on their position in customOrder array
|
|
91
|
+
return [...tileValues].sort((a, b) => {
|
|
92
|
+
const keyA = String(a)
|
|
93
|
+
const keyB = String(b)
|
|
94
|
+
|
|
95
|
+
const orderA = customOrder.indexOf(keyA)
|
|
96
|
+
const orderB = customOrder.indexOf(keyB)
|
|
97
|
+
|
|
98
|
+
// Items not in customOrder go to the end
|
|
99
|
+
const finalOrderA = orderA === -1 ? 999999 : orderA
|
|
100
|
+
const finalOrderB = orderB === -1 ? 999999 : orderB
|
|
101
|
+
|
|
102
|
+
return finalOrderA - finalOrderB
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
default:
|
|
106
|
+
return tileValues
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get tile keys for editor/configuration purposes
|
|
112
|
+
* This is used in the editor to show available tiles for ordering/titling
|
|
113
|
+
*
|
|
114
|
+
* @param config - The map configuration
|
|
115
|
+
* @param data - The dataset
|
|
116
|
+
* @returns Array of tile keys (same as getTileValues but specifically for editor use)
|
|
117
|
+
*/
|
|
118
|
+
export const getTileKeys = (config: MapConfig, data: DataRow[]): (string | number)[] => {
|
|
119
|
+
if (!config.smallMultiples?.tileColumn || !data) {
|
|
120
|
+
return []
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return getTileValues(data, config.smallMultiples.tileColumn)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Pivot data from long format to wide format for DataTable display
|
|
128
|
+
* Transforms data so each unique tileColumn value becomes its own column
|
|
129
|
+
*
|
|
130
|
+
* Example:
|
|
131
|
+
* From: [{geo: "AL", value: 100, pathogen: "COVID"}, {geo: "AL", value: 50, pathogen: "Flu"}]
|
|
132
|
+
* To: [{geo: "AL", COVID: 100, Flu: 50}]
|
|
133
|
+
*
|
|
134
|
+
* @param data - Original data in long format
|
|
135
|
+
* @param tileColumn - Column to pivot on (e.g., "pathogen")
|
|
136
|
+
* @param valueColumn - Column containing values to pivot (e.g., "value")
|
|
137
|
+
* @param geoColumn - Geography column name (e.g., "geo")
|
|
138
|
+
* @param tileValues - Ordered array of tile values (determines column order)
|
|
139
|
+
* @returns Pivoted data in wide format
|
|
140
|
+
*/
|
|
141
|
+
export const pivotDataForDataTable = (
|
|
142
|
+
data: DataRow[],
|
|
143
|
+
tileColumn: string,
|
|
144
|
+
valueColumn: string,
|
|
145
|
+
geoColumn: string,
|
|
146
|
+
tileValues: (string | number)[]
|
|
147
|
+
): DataRow[] => {
|
|
148
|
+
if (!data || !tileColumn || !valueColumn || !geoColumn) return []
|
|
149
|
+
|
|
150
|
+
// Group data by geography
|
|
151
|
+
const geoGroups = new Map<string, DataRow[]>()
|
|
152
|
+
|
|
153
|
+
data.forEach(row => {
|
|
154
|
+
const geoKey = String(row[geoColumn])
|
|
155
|
+
if (!geoGroups.has(geoKey)) {
|
|
156
|
+
geoGroups.set(geoKey, [])
|
|
157
|
+
}
|
|
158
|
+
geoGroups.get(geoKey)!.push(row)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Create pivoted rows
|
|
162
|
+
const pivotedData: DataRow[] = []
|
|
163
|
+
|
|
164
|
+
geoGroups.forEach((rows, geoKey) => {
|
|
165
|
+
const pivotedRow: DataRow = {
|
|
166
|
+
[geoColumn]: geoKey
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Copy non-value, non-tile columns from first row (they should be the same for all rows of this geo)
|
|
170
|
+
const firstRow = rows[0]
|
|
171
|
+
Object.keys(firstRow).forEach(key => {
|
|
172
|
+
if (key !== tileColumn && key !== valueColumn && key !== geoColumn) {
|
|
173
|
+
pivotedRow[key] = firstRow[key]
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Add a column for each tile value
|
|
178
|
+
rows.forEach(row => {
|
|
179
|
+
const tileValue = row[tileColumn]
|
|
180
|
+
if (tileValue != null && tileValue !== '') {
|
|
181
|
+
pivotedRow[String(tileValue)] = row[valueColumn]
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
pivotedData.push(pivotedRow)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
return pivotedData
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Pivot runtimeData from long format to wide format
|
|
193
|
+
* RuntimeData is an object keyed by UID, so we need to pivot the values within each UID
|
|
194
|
+
*
|
|
195
|
+
* @param runtimeData - Original runtimeData object keyed by UID
|
|
196
|
+
* @param tileColumn - Column to pivot on (e.g., "pathogen")
|
|
197
|
+
* @param valueColumn - Column containing values to pivot (e.g., "activity_level_label")
|
|
198
|
+
* @param geoColumn - Geography column name (e.g., "State")
|
|
199
|
+
* @param allData - Full dataset to find all rows for each geo
|
|
200
|
+
* @param tileValues - Ordered array of tile values
|
|
201
|
+
* @returns Pivoted runtimeData
|
|
202
|
+
*/
|
|
203
|
+
export const pivotRuntimeDataForDataTable = (
|
|
204
|
+
runtimeData: { [uid: string]: any },
|
|
205
|
+
tileColumn: string,
|
|
206
|
+
valueColumn: string,
|
|
207
|
+
geoColumn: string,
|
|
208
|
+
allData: DataRow[],
|
|
209
|
+
tileValues: (string | number)[]
|
|
210
|
+
): { [uid: string]: any } => {
|
|
211
|
+
if (!runtimeData || !tileColumn || !valueColumn || !geoColumn || !allData) return runtimeData
|
|
212
|
+
|
|
213
|
+
const pivotedRuntimeData: { [uid: string]: any } = {}
|
|
214
|
+
|
|
215
|
+
// For each UID in runtimeData
|
|
216
|
+
Object.keys(runtimeData).forEach(uid => {
|
|
217
|
+
const baseRow = runtimeData[uid]
|
|
218
|
+
const geoValue = baseRow[geoColumn]
|
|
219
|
+
|
|
220
|
+
// Find all rows in allData for this geo
|
|
221
|
+
const rowsForThisGeo = allData.filter(row => row[geoColumn] === geoValue)
|
|
222
|
+
|
|
223
|
+
// Create pivoted row starting with base row
|
|
224
|
+
const pivotedRow = { ...baseRow }
|
|
225
|
+
|
|
226
|
+
// Add a property for each tile value
|
|
227
|
+
rowsForThisGeo.forEach(row => {
|
|
228
|
+
const tileValue = row[tileColumn]
|
|
229
|
+
if (tileValue != null && tileValue !== '') {
|
|
230
|
+
pivotedRow[String(tileValue)] = row[valueColumn]
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// Remove the original value column and tile column
|
|
235
|
+
delete pivotedRow[valueColumn]
|
|
236
|
+
delete pivotedRow[tileColumn]
|
|
237
|
+
|
|
238
|
+
pivotedRuntimeData[uid] = pivotedRow
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
return pivotedRuntimeData
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Create column configurations for pivoted data table
|
|
246
|
+
* Generates one column config for each tile value, copying formatting from the original value column
|
|
247
|
+
* Preserves column order by inserting new columns where the value column was
|
|
248
|
+
*
|
|
249
|
+
* @param originalColumns - Original columns configuration
|
|
250
|
+
* @param valueColumnName - Name of the value column to clone config from
|
|
251
|
+
* @param tileColumnName - Name of the tile column to remove
|
|
252
|
+
* @param tileValues - Array of tile values (becomes new column names)
|
|
253
|
+
* @param tileTitles - Custom titles for columns
|
|
254
|
+
* @returns New columns configuration with geo column + one column per tile value
|
|
255
|
+
*/
|
|
256
|
+
export const createPivotedColumns = (
|
|
257
|
+
originalColumns: any,
|
|
258
|
+
valueColumnName: string,
|
|
259
|
+
tileColumnName: string,
|
|
260
|
+
tileValues: (string | number)[],
|
|
261
|
+
tileTitles?: { [key: string]: string }
|
|
262
|
+
): any => {
|
|
263
|
+
// Find the original value column config to clone its formatting
|
|
264
|
+
// Need to search by column.name, not by key
|
|
265
|
+
let valueColumnConfig = {}
|
|
266
|
+
let valueColumnKey = null
|
|
267
|
+
|
|
268
|
+
Object.keys(originalColumns).forEach(key => {
|
|
269
|
+
if (originalColumns[key].name === valueColumnName) {
|
|
270
|
+
valueColumnConfig = originalColumns[key]
|
|
271
|
+
valueColumnKey = key
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
// Create new columns object preserving order
|
|
276
|
+
const newColumns = {}
|
|
277
|
+
|
|
278
|
+
// Iterate through original columns
|
|
279
|
+
Object.keys(originalColumns).forEach(key => {
|
|
280
|
+
const column = originalColumns[key]
|
|
281
|
+
|
|
282
|
+
// Check if this column's name matches the value column
|
|
283
|
+
if (column.name === valueColumnName) {
|
|
284
|
+
// Replace value column with pivoted columns
|
|
285
|
+
tileValues.forEach(tileValue => {
|
|
286
|
+
const columnKey = String(tileValue)
|
|
287
|
+
newColumns[columnKey] = {
|
|
288
|
+
...valueColumnConfig,
|
|
289
|
+
name: columnKey,
|
|
290
|
+
label: getTileDisplayTitle(tileValue, tileTitles),
|
|
291
|
+
dataTable: true
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
} else if (column.name === tileColumnName) {
|
|
295
|
+
// Skip tile column - don't add it to new columns
|
|
296
|
+
return
|
|
297
|
+
} else {
|
|
298
|
+
// Keep all other columns
|
|
299
|
+
newColumns[key] = originalColumns[key]
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
return newColumns
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Prepare data table props for small multiples display
|
|
308
|
+
* If small multiples is enabled, pivots data and columns. Otherwise returns originals.
|
|
309
|
+
*
|
|
310
|
+
* @param config - Map configuration
|
|
311
|
+
* @param columns - Original columns configuration
|
|
312
|
+
* @param runtimeData - Original runtime data
|
|
313
|
+
* @returns Object with modified config, columns, and runtimeData (or originals if not small multiples)
|
|
314
|
+
*/
|
|
315
|
+
export const prepareSmallMultiplesDataTable = (
|
|
316
|
+
config: MapConfig,
|
|
317
|
+
columns: any,
|
|
318
|
+
runtimeData: any
|
|
319
|
+
): { config: MapConfig; columns: any; runtimeData: any } => {
|
|
320
|
+
const { tileColumn, tileOrderType, tileOrder, tileTitles } = config.smallMultiples
|
|
321
|
+
const valueColumn = config.columns.primary?.name
|
|
322
|
+
const geoColumn = config.columns.geo?.name
|
|
323
|
+
|
|
324
|
+
// If required columns aren't configured, return originals
|
|
325
|
+
if (!valueColumn || !geoColumn) {
|
|
326
|
+
return { config, columns, runtimeData }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Get ordered tile values
|
|
330
|
+
const rawTileValues = getTileValues(config.data, tileColumn)
|
|
331
|
+
const orderedTileValues = applyTileOrder(rawTileValues, tileOrderType, tileOrder, tileTitles)
|
|
332
|
+
|
|
333
|
+
// Pivot data
|
|
334
|
+
const pivotedData = pivotDataForDataTable(config.data, tileColumn, valueColumn, geoColumn, orderedTileValues)
|
|
335
|
+
|
|
336
|
+
// Pivot runtimeData
|
|
337
|
+
const pivotedRuntimeData = pivotRuntimeDataForDataTable(
|
|
338
|
+
runtimeData,
|
|
339
|
+
tileColumn,
|
|
340
|
+
valueColumn,
|
|
341
|
+
geoColumn,
|
|
342
|
+
config.data,
|
|
343
|
+
orderedTileValues
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
// Create pivoted columns
|
|
347
|
+
const pivotedColumns = createPivotedColumns(columns, valueColumn, tileColumn, orderedTileValues, tileTitles)
|
|
348
|
+
|
|
349
|
+
// Return modified config with pivoted data and columns
|
|
350
|
+
return {
|
|
351
|
+
config: {
|
|
352
|
+
...config,
|
|
353
|
+
data: pivotedData,
|
|
354
|
+
columns: pivotedColumns
|
|
355
|
+
},
|
|
356
|
+
columns: pivotedColumns,
|
|
357
|
+
runtimeData: pivotedRuntimeData
|
|
358
|
+
}
|
|
359
|
+
}
|