@cdc/map 4.25.10 → 4.25.11
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 +27405 -25783
- package/examples/example-city-state.json +9 -1
- package/examples/multi-country-centering.json +45 -0
- package/examples/private/colors-2.json +221 -0
- package/examples/private/colors.json +221 -0
- package/index.html +2 -1
- package/package.json +4 -4
- package/src/CdcMapComponent.tsx +44 -20
- 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 +3371 -0
- package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
- package/src/_stories/CdcMap.stories.tsx +22 -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 +2 -4
- package/src/components/BubbleList.tsx +1 -1
- package/src/components/EditorPanel/components/EditorPanel.tsx +630 -564
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +55 -93
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +27 -37
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +354 -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 +34 -34
- package/src/components/Legend/components/index.scss +1 -1
- 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 +23 -4
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +6 -0
- package/src/components/UsaMap/components/UsaMap.County.tsx +14 -2
- package/src/components/UsaMap/components/UsaMap.Region.tsx +14 -1
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +25 -5
- package/src/components/UsaMap/components/UsaMap.State.tsx +26 -3
- 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 +10 -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/constants.ts +1 -15
- package/src/helpers/displayGeoName.ts +19 -4
- package/src/helpers/generateRuntimeLegend.ts +0 -2
- 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 +1 -9
- package/src/helpers/smallMultiplesHelpers.ts +359 -0
- package/src/helpers/tests/titleCase.test.ts +76 -0
- package/src/helpers/titleCase.ts +13 -13
- 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 +5 -2
- package/src/hooks/useStateZoom.tsx +5 -2
- 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/types/MapConfig.ts +30 -11
- package/src/types/MapContext.ts +6 -0
- package/src/types/runtimeLegend.ts +1 -1
- package/src/components/DataTable.tsx +0 -413
- package/src/components/EditorPanel/components/Inputs.tsx +0 -59
- package/src/hooks/useActiveElement.ts +0 -19
- 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/index.ts
CHANGED
|
@@ -19,12 +19,4 @@ export { titleCase as toTitleCase } from './toTitleCase'
|
|
|
19
19
|
export { titleCase } from './titleCase'
|
|
20
20
|
export { validateFipsCodeLength } from './validateFipsCodeLength'
|
|
21
21
|
export { getMapContainerClasses } from './getMapContainerClasses'
|
|
22
|
-
export {
|
|
23
|
-
SVG_HEIGHT,
|
|
24
|
-
SVG_WIDTH,
|
|
25
|
-
SVG_PADDING,
|
|
26
|
-
SVG_VIEWBOX,
|
|
27
|
-
HEADER_COLORS,
|
|
28
|
-
MAX_ZOOM_LEVEL,
|
|
29
|
-
DEFAULT_MAP_BACKGROUND
|
|
30
|
-
} from './constants'
|
|
22
|
+
export { SVG_HEIGHT, SVG_WIDTH, SVG_PADDING, SVG_VIEWBOX, MAX_ZOOM_LEVEL, DEFAULT_MAP_BACKGROUND } from './constants'
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { titleCase } from '../titleCase'
|
|
2
|
+
|
|
3
|
+
describe('titleCase', () => {
|
|
4
|
+
it('should return undefined for falsy input', () => {
|
|
5
|
+
expect(titleCase(undefined)).toBeUndefined()
|
|
6
|
+
expect(titleCase(null)).toBeUndefined()
|
|
7
|
+
expect(titleCase('')).toBeUndefined()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should convert simple strings to title case', () => {
|
|
11
|
+
expect(titleCase('hello world')).toBe('Hello World')
|
|
12
|
+
expect(titleCase('HELLO WORLD')).toBe('Hello World')
|
|
13
|
+
expect(titleCase('HeLLo WoRLd')).toBe('Hello World')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should keep "of", "the", and "and" lowercase in title case', () => {
|
|
17
|
+
expect(titleCase('DISTRICT OF COLUMBIA')).toBe('District of Columbia')
|
|
18
|
+
expect(titleCase('District Of Columbia')).toBe('District of Columbia')
|
|
19
|
+
expect(titleCase('district of columbia')).toBe('District of Columbia')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should handle "Federated States of Micronesia" correctly', () => {
|
|
23
|
+
expect(titleCase('FEDERATED STATES OF MICRONESIA')).toBe('Federated States of Micronesia')
|
|
24
|
+
expect(titleCase('Federated States Of Micronesia')).toBe('Federated States of Micronesia')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should handle "Republic of the Congo" correctly', () => {
|
|
28
|
+
expect(titleCase('REPUBLIC OF THE CONGO')).toBe('Republic of the Congo')
|
|
29
|
+
expect(titleCase('Republic Of The Congo')).toBe('Republic of the Congo')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should handle "and" in country names correctly', () => {
|
|
33
|
+
expect(titleCase('ANTIGUA AND BARBUDA')).toBe('Antigua and Barbuda')
|
|
34
|
+
expect(titleCase('TRINIDAD AND TOBAGO')).toBe('Trinidad and Tobago')
|
|
35
|
+
expect(titleCase('BOSNIA AND HERZEGOVINA')).toBe('Bosnia and Herzegovina')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should handle hyphenated strings correctly', () => {
|
|
39
|
+
expect(titleCase('INTER-TRIBAL INDIAN RESERVATION')).toBe('Inter-Tribal Indian Reservation')
|
|
40
|
+
expect(titleCase('inter-tribal indian reservation')).toBe('Inter-Tribal Indian Reservation')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should handle en dash strings correctly', () => {
|
|
44
|
+
expect(titleCase('PUERTO RICO–VIRGIN ISLANDS')).toBe('Puerto Rico–Virgin Islands')
|
|
45
|
+
expect(titleCase('puerto rico–virgin islands')).toBe('Puerto Rico–Virgin Islands')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should handle mixed case strings with "of"', () => {
|
|
49
|
+
expect(titleCase('UNIVERSITY OF WASHINGTON')).toBe('University of Washington')
|
|
50
|
+
expect(titleCase('STATE OF ALASKA')).toBe('State of Alaska')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should handle single words', () => {
|
|
54
|
+
expect(titleCase('CALIFORNIA')).toBe('California')
|
|
55
|
+
expect(titleCase('california')).toBe('California')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should handle strings with multiple "of" and "the" occurrences', () => {
|
|
59
|
+
expect(titleCase('OFFICE OF THE STATE OF CALIFORNIA')).toBe('Office of the State of California')
|
|
60
|
+
expect(titleCase('DEMOCRATIC REPUBLIC OF THE CONGO')).toBe('Democratic Republic of the Congo')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should handle hyphenated strings with "of", "the", and "and"', () => {
|
|
64
|
+
expect(titleCase('KINGDOM OF THE NETHERLANDS-ARUBA')).toBe('Kingdom of the Netherlands-Aruba')
|
|
65
|
+
expect(titleCase('SAINT VINCENT AND THE GRENADINES-ISLAND')).toBe('Saint Vincent and the Grenadines-Island')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should handle "Commonwealth of the Northern Mariana Islands"', () => {
|
|
69
|
+
expect(titleCase('COMMONWEALTH OF THE NORTHERN MARIANA ISLANDS')).toBe(
|
|
70
|
+
'Commonwealth of the Northern Mariana Islands'
|
|
71
|
+
)
|
|
72
|
+
expect(titleCase('commonwealth of the northern mariana islands')).toBe(
|
|
73
|
+
'Commonwealth of the Northern Mariana Islands'
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
})
|
package/src/helpers/titleCase.ts
CHANGED
|
@@ -5,25 +5,25 @@ export const titleCase = string => {
|
|
|
5
5
|
// guard clause else error in editor
|
|
6
6
|
if (!string) return
|
|
7
7
|
if (string !== undefined) {
|
|
8
|
+
// Words that should remain lowercase in geographic names
|
|
9
|
+
const lowercaseWords = ['of', 'the', 'and']
|
|
10
|
+
|
|
11
|
+
const titleCaseWord = (word: string): string => {
|
|
12
|
+
const lowerWord = word.toLowerCase()
|
|
13
|
+
return lowercaseWords.includes(lowerWord)
|
|
14
|
+
? lowerWord
|
|
15
|
+
: word.charAt(0).toUpperCase() + word.substring(1).toLowerCase()
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
// if hyphen found, then split, uppercase each word, and put back together
|
|
9
19
|
if (string.includes('–') || string.includes('-')) {
|
|
10
20
|
let dashSplit = string.includes('–') ? string.split('–') : string.split('-') // determine hyphen or en dash to split on
|
|
11
21
|
let splitCharacter = string.includes('–') ? '–' : '-' // print hyphen or en dash later on.
|
|
12
|
-
let frontSplit = dashSplit[0]
|
|
13
|
-
|
|
14
|
-
.map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())
|
|
15
|
-
.join(' ')
|
|
16
|
-
let backSplit = dashSplit[1]
|
|
17
|
-
.split(' ')
|
|
18
|
-
.map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())
|
|
19
|
-
.join(' ')
|
|
22
|
+
let frontSplit = dashSplit[0].split(' ').map(titleCaseWord).join(' ')
|
|
23
|
+
let backSplit = dashSplit[1].split(' ').map(titleCaseWord).join(' ')
|
|
20
24
|
return frontSplit + splitCharacter + backSplit
|
|
21
25
|
} else {
|
|
22
|
-
|
|
23
|
-
return string
|
|
24
|
-
.split(' ')
|
|
25
|
-
.map(word => (word === 'of' ? word : word.charAt(0).toUpperCase() + word.substring(1).toLowerCase()))
|
|
26
|
-
.join(' ')
|
|
26
|
+
return string.split(' ').map(titleCaseWord).join(' ')
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -6,7 +6,7 @@ import { MapConfig } from '../types/MapConfig'
|
|
|
6
6
|
import { CSV_PARSE_CONFIG } from './constants'
|
|
7
7
|
import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const buildQueryString = (params: Record<string, string>): string =>
|
|
10
10
|
Object.keys(params)
|
|
11
11
|
.map((param, i) => {
|
|
12
12
|
let qs = i === 0 ? '?' : '&'
|