@cdc/map 4.25.8 → 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/.claude/settings.local.json +30 -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 +56991 -53706
- package/examples/example-city-state.json +9 -1
- package/examples/multi-country-centering.json +45 -0
- package/examples/private/c.json +290 -0
- package/examples/private/canvas-city-hover.json +787 -0
- package/examples/private/colors-2.json +221 -0
- package/examples/private/colors.json +221 -0
- package/examples/private/d.json +345 -0
- package/examples/private/g.json +1 -0
- package/examples/private/h.json +105911 -0
- package/examples/private/measles-data.json +378 -0
- package/examples/private/measles.json +211 -0
- package/examples/private/north-dakota.json +1132 -0
- package/examples/private/state-with-pattern.json +883 -0
- package/index.html +36 -34
- package/package.json +26 -5
- package/src/CdcMap.tsx +23 -8
- package/src/CdcMapComponent.tsx +238 -308
- 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.Filters.stories.tsx +2 -2
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
- package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
- package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
- package/src/_stories/CdcMap.SmallMultiples.stories.tsx +35 -0
- package/src/_stories/CdcMap.Table.stories.tsx +2 -2
- package/src/_stories/CdcMap.stories.tsx +37 -9
- package/src/_stories/GoogleMap.stories.tsx +2 -2
- package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
- package/src/_stories/_mock/column-wrap-test.json +265 -0
- package/src/_stories/_mock/equal-number.json +1109 -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/us-bubble-cities.json +306 -0
- package/src/_stories/_mock/usa-state-gradient.json +2 -4
- package/src/components/BubbleList.tsx +17 -13
- package/src/components/CityList.tsx +85 -107
- package/src/components/EditorPanel/components/EditorPanel.tsx +787 -709
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +58 -95
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +34 -42
- 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 +22 -3
- package/src/components/Legend/components/Legend.tsx +76 -40
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
- package/src/components/Legend/components/index.scss +1 -1
- package/src/components/MapContainer.tsx +52 -0
- package/src/components/MapControls.tsx +44 -0
- package/src/components/NavigationMenu.tsx +27 -15
- 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 +36 -4
- 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 +123 -37
- package/src/components/UsaMap/components/UsaMap.Region.tsx +36 -5
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +30 -10
- package/src/components/UsaMap/components/UsaMap.State.tsx +53 -12
- package/src/components/UsaMap/helpers/map.ts +4 -4
- package/src/components/UsaMap/helpers/shapes.ts +9 -6
- package/src/components/WorldMap/WorldMap.tsx +193 -35
- package/src/components/ZoomControls.tsx +6 -9
- package/src/context/LegendMemoContext.tsx +30 -0
- package/src/context.ts +1 -40
- package/src/data/initial-state.js +153 -130
- package/src/data/supported-geos.js +25 -78
- package/src/helpers/addUIDs.ts +13 -2
- package/src/helpers/applyColorToLegend.ts +140 -20
- package/src/helpers/applyLegendToRow.ts +10 -6
- package/src/helpers/componentHelpers.ts +8 -0
- package/src/helpers/constants.ts +12 -14
- package/src/helpers/dataTableHelpers.ts +6 -0
- package/src/helpers/displayGeoName.ts +18 -3
- package/src/helpers/generateRuntimeLegend.ts +44 -10
- package/src/helpers/generateRuntimeLegendHash.ts +4 -2
- package/src/helpers/getColumnNames.ts +1 -1
- package/src/helpers/getCountriesPicked.ts +103 -0
- package/src/helpers/getMapContainerClasses.ts +7 -0
- package/src/helpers/getPatternForRow.ts +33 -0
- package/src/helpers/getStatesPicked.ts +8 -5
- package/src/helpers/index.ts +3 -3
- package/src/helpers/isLegendItemDisabled.ts +16 -0
- package/src/helpers/mapObserverHelpers.ts +40 -0
- package/src/helpers/resetLegendToggles.ts +3 -2
- 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/toggleLegendActive.ts +6 -11
- package/src/helpers/urlDataHelpers.ts +70 -0
- package/src/hooks/useCountryZoom.tsx +241 -0
- package/src/hooks/useGeoClickHandler.ts +36 -2
- package/src/hooks/useLegendMemo.ts +17 -0
- package/src/hooks/useMapLayers.tsx +5 -4
- package/src/hooks/useProgrammaticMapTooltip.ts +110 -0
- package/src/hooks/useResizeObserver.ts +5 -2
- package/src/hooks/useStateZoom.tsx +30 -8
- package/src/hooks/useSynchronizedGeographies.ts +56 -0
- package/src/hooks/useTooltip.ts +1 -2
- package/src/index.jsx +1 -2
- 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/store/map.reducer.ts +17 -6
- package/src/test/CdcMap.test.jsx +11 -0
- package/src/types/MapConfig.ts +46 -18
- package/src/types/MapContext.ts +6 -7
- package/src/types/runtimeLegend.ts +17 -1
- package/vite.config.js +2 -7
- package/vitest.config.ts +16 -0
- package/src/components/DataTable.tsx +0 -385
- package/src/components/EditorPanel/components/Inputs.tsx +0 -59
- package/src/coreStyles_map.scss +0 -3
- package/src/helpers/colorDistributions.ts +0 -12
- package/src/helpers/generateColorsArray.ts +0 -14
- package/src/helpers/tests/generateColorsArray.test.ts +0 -18
- package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
- 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/types/MapConfig.ts
CHANGED
|
@@ -3,8 +3,13 @@ import { type Visualization } from '@cdc/core/types/Visualization'
|
|
|
3
3
|
import { type EditorColumnProperties } from '@cdc/core/types/EditorColumnProperties'
|
|
4
4
|
import { type Version } from '@cdc/core/types/Version'
|
|
5
5
|
import { type VizFilter } from '@cdc/core/types/VizFilter'
|
|
6
|
+
import { type Annotation } from '@cdc/core/types/Annotation'
|
|
7
|
+
import { MarkupConfig } from '@cdc/core/types/MarkupVariable'
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
// Runtime data types
|
|
10
|
+
export type RuntimeFilters = VizFilter[] & { fromHash?: number }
|
|
11
|
+
|
|
12
|
+
type MapVisualSettings = {
|
|
8
13
|
/** minBubbleSize - Minimum Circle Size when the map has a type of bubble */
|
|
9
14
|
minBubbleSize: number
|
|
10
15
|
/** maxBubbleSize - Maximum Circle Size when the map has a type of bubble */
|
|
@@ -38,16 +43,21 @@ export type PatternSelection = {
|
|
|
38
43
|
contrastCheck: boolean
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
'dataTable' | 'label' | 'name' | 'prefix' | 'suffix' | 'tooltip'
|
|
48
|
-
>
|
|
46
|
+
// Base column properties with name required, all others optional
|
|
47
|
+
type BaseColumnProperties = Pick<EditorColumnProperties, 'name'> &
|
|
48
|
+
Partial<Pick<EditorColumnProperties, 'label' | 'tooltip' | 'dataTable' | 'prefix' | 'suffix'>>
|
|
49
|
+
|
|
50
|
+
// Simple column type for name-only columns
|
|
51
|
+
type SimpleColumnProperties = Pick<EditorColumnProperties, 'name'>
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
// Specific column types for better semantics
|
|
54
|
+
type GeoColumnProperties = BaseColumnProperties
|
|
55
|
+
type LatitudeColumnProperties = SimpleColumnProperties
|
|
56
|
+
type LongitudeColumnProperties = SimpleColumnProperties
|
|
57
|
+
type NavigateColumnProperties = SimpleColumnProperties
|
|
58
|
+
type PrimaryColumnProperties = BaseColumnProperties
|
|
59
|
+
|
|
60
|
+
type LegendShapeItem = {
|
|
51
61
|
column: string
|
|
52
62
|
key: string
|
|
53
63
|
operator: '=' | '≠' | '<' | '>' | '<=' | '>='
|
|
@@ -55,13 +65,13 @@ export type LegendShapeItem = {
|
|
|
55
65
|
value: string
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
|
|
68
|
+
type LegendGrouping = {
|
|
59
69
|
legendTitle: string
|
|
60
70
|
legendDescription: string
|
|
61
71
|
items: LegendShapeItem[]
|
|
62
72
|
}
|
|
63
73
|
|
|
64
|
-
|
|
74
|
+
type HexMapSettings = {
|
|
65
75
|
type: 'shapes' | 'standard'
|
|
66
76
|
shapeGroups: LegendGrouping[]
|
|
67
77
|
}
|
|
@@ -70,15 +80,24 @@ export type Coordinate = [number, number]
|
|
|
70
80
|
|
|
71
81
|
export type DataRow = {
|
|
72
82
|
uid?: string // optional 'uid' property
|
|
73
|
-
[key: string]:
|
|
83
|
+
[key: string]: string | number | boolean | null | undefined // allowing primitive data types for dynamic columns
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type SmallMultiples = {
|
|
87
|
+
mode?: 'by-column'
|
|
88
|
+
tileColumn?: string
|
|
89
|
+
tilesPerRowDesktop?: number
|
|
90
|
+
tilesPerRowMobile?: number
|
|
91
|
+
tileOrderType?: 'asc' | 'desc' | 'custom'
|
|
92
|
+
tileOrder?: string[]
|
|
93
|
+
tileTitles?: { [key: string]: string }
|
|
94
|
+
synchronizedTooltips?: boolean
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
export type MapConfig = Visualization & {
|
|
77
98
|
annotations: Annotation[]
|
|
78
99
|
// map color palette
|
|
79
100
|
color: string
|
|
80
|
-
// custom color palette
|
|
81
|
-
customColors: string[]
|
|
82
101
|
columns: {
|
|
83
102
|
geo: GeoColumnProperties
|
|
84
103
|
primary: PrimaryColumnProperties
|
|
@@ -93,6 +112,7 @@ export type MapConfig = Visualization & {
|
|
|
93
112
|
filters: VizFilter[]
|
|
94
113
|
general: {
|
|
95
114
|
navigationTarget: '_self' | '_blank'
|
|
115
|
+
noDataMessage: string // single-state no data message
|
|
96
116
|
subtext: string
|
|
97
117
|
introText: string
|
|
98
118
|
allowMapZoom: boolean
|
|
@@ -121,6 +141,10 @@ export type MapConfig = Visualization & {
|
|
|
121
141
|
language: string
|
|
122
142
|
palette: {
|
|
123
143
|
isReversed: boolean
|
|
144
|
+
name: string
|
|
145
|
+
version: string
|
|
146
|
+
customColors?: string[]
|
|
147
|
+
customColorsOrdered?: string[]
|
|
124
148
|
}
|
|
125
149
|
showDownloadMediaButton: boolean
|
|
126
150
|
showDownloadImgButton: boolean
|
|
@@ -131,6 +155,11 @@ export type MapConfig = Visualization & {
|
|
|
131
155
|
fipsCode: string
|
|
132
156
|
stateName: string
|
|
133
157
|
}[]
|
|
158
|
+
countriesPicked?: {
|
|
159
|
+
iso: string
|
|
160
|
+
name: string
|
|
161
|
+
}[]
|
|
162
|
+
hideUnselectedCountries?: boolean // When true, hide unselected countries; when false (default), gray them out
|
|
134
163
|
territoriesAlwaysShow: boolean
|
|
135
164
|
territoriesLabel: string
|
|
136
165
|
title: string
|
|
@@ -180,8 +209,6 @@ export type MapConfig = Visualization & {
|
|
|
180
209
|
}
|
|
181
210
|
runtime: {
|
|
182
211
|
editorErrorMessage: string[]
|
|
183
|
-
// when a single state map doesn't include a fips code show a message...
|
|
184
|
-
noStateFoundMessage: string
|
|
185
212
|
}
|
|
186
213
|
mapPosition: { coordinates: Coordinate; zoom: number }
|
|
187
214
|
map: {
|
|
@@ -192,8 +219,9 @@ export type MapConfig = Visualization & {
|
|
|
192
219
|
filterBehavior: string
|
|
193
220
|
filterIntro: string
|
|
194
221
|
visual: MapVisualSettings
|
|
222
|
+
smallMultiples?: SmallMultiples
|
|
195
223
|
// visualization type
|
|
196
224
|
type: 'map'
|
|
197
225
|
// version of the map
|
|
198
226
|
version: Version
|
|
199
|
-
}
|
|
227
|
+
} & MarkupConfig
|
package/src/types/MapContext.ts
CHANGED
|
@@ -2,12 +2,13 @@ import { DataRow, type MapConfig } from './MapConfig'
|
|
|
2
2
|
import { type ViewPort } from '@cdc/core/types/ViewPort'
|
|
3
3
|
import { DimensionsType } from '@cdc/core/types/Dimensions'
|
|
4
4
|
import { VizFilter } from '@cdc/core/types/VizFilter'
|
|
5
|
-
import {
|
|
5
|
+
import { MapRefInterface } from '../hooks/useProgrammaticMapTooltip'
|
|
6
|
+
import { MutableRefObject } from 'react'
|
|
6
7
|
|
|
7
8
|
export type MapContext = {
|
|
8
9
|
currentViewport: ViewPort
|
|
10
|
+
vizViewport?: ViewPort
|
|
9
11
|
content: { geoName: string; keyedData: Record<string, any> }
|
|
10
|
-
data: DataRow[]
|
|
11
12
|
dimensions: DimensionsType
|
|
12
13
|
displayDataAsText: string | number
|
|
13
14
|
displayGeoName: (key: string, convertFipsCodes: boolean) => string
|
|
@@ -22,23 +23,19 @@ export type MapContext = {
|
|
|
22
23
|
handleCircleClick: Function
|
|
23
24
|
handleDragStateChange: Function
|
|
24
25
|
isDraggingAnnotation: boolean
|
|
25
|
-
innerContainerRef: RefObject<HTMLDivElement>
|
|
26
26
|
isDashboard: boolean
|
|
27
27
|
isEditor: boolean
|
|
28
28
|
isFilterValueSupported: boolean
|
|
29
|
+
useDynamicViewbox?: boolean
|
|
29
30
|
loadConfig: (configObj: MapConfig) => void
|
|
30
31
|
logo: string
|
|
31
32
|
mapId: string
|
|
32
33
|
position: 'side' | 'top' | 'bottom'
|
|
33
34
|
resetLegendToggles: Function
|
|
34
35
|
runtimeFilters: Function
|
|
35
|
-
legendMemo: Function
|
|
36
|
-
legendSpecialClassLastMemo: Function
|
|
37
36
|
runtimeLegend
|
|
38
37
|
setParentConfig: Function
|
|
39
38
|
setRuntimeData: Function
|
|
40
|
-
setRuntimeFilters: Function
|
|
41
|
-
setRuntimeLegend: Function
|
|
42
39
|
setSharedFilterValue: Function
|
|
43
40
|
setConfig: (newState: MapConfig) => MapConfig
|
|
44
41
|
config: MapConfig
|
|
@@ -50,4 +47,6 @@ export type MapContext = {
|
|
|
50
47
|
runtimeData: Object[]
|
|
51
48
|
tooltipId: string
|
|
52
49
|
interactionLabel?: string
|
|
50
|
+
handleSmallMultipleHover?: (geoId: string | null, yCoordinate?: number) => void
|
|
51
|
+
mapRefForSync?: MutableRefObject<MapRefInterface | null>
|
|
53
52
|
}
|
|
@@ -1 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
type RuntimeLegendItem = {
|
|
2
|
+
disabled?: boolean
|
|
3
|
+
bin?: number
|
|
4
|
+
color?: string
|
|
5
|
+
special?: boolean
|
|
6
|
+
value?: string | number
|
|
7
|
+
label?: string
|
|
8
|
+
min?: number
|
|
9
|
+
max?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type RuntimeLegend = {
|
|
13
|
+
items: RuntimeLegendItem[]
|
|
14
|
+
disabledAmt?: number
|
|
15
|
+
fromHash?: number
|
|
16
|
+
runtimeDataHash?: number
|
|
17
|
+
}
|
package/vite.config.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import GenerateViteConfig from '
|
|
1
|
+
import GenerateViteConfig from '@cdc/core/generateViteConfig.js'
|
|
2
2
|
import { moduleName } from './package.json'
|
|
3
3
|
|
|
4
|
-
export default GenerateViteConfig(moduleName
|
|
5
|
-
jsxImportSource: '@emotion/react',
|
|
6
|
-
babel: {
|
|
7
|
-
plugins: ['@emotion/babel-plugin']
|
|
8
|
-
}
|
|
9
|
-
})
|
|
4
|
+
export default GenerateViteConfig(moduleName)
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
environment: 'jsdom',
|
|
6
|
+
globals: true,
|
|
7
|
+
setupFiles: ['../../vitest.setup.ts'],
|
|
8
|
+
exclude: [
|
|
9
|
+
'**/node_modules/**',
|
|
10
|
+
'**/dist/**',
|
|
11
|
+
'**/.storybook/**',
|
|
12
|
+
'**/*.stories.*',
|
|
13
|
+
'**/storybook-static/**'
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
})
|
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState, memo, useContext } from 'react'
|
|
2
|
-
|
|
3
|
-
import Papa from 'papaparse'
|
|
4
|
-
import ExternalIcon from '../images/external-link.svg' // TODO: Move to Icon component
|
|
5
|
-
import Icon from '@cdc/core/components/ui/Icon'
|
|
6
|
-
|
|
7
|
-
import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
|
|
8
|
-
import LegendShape from '@cdc/core/components/LegendShape'
|
|
9
|
-
import MediaControls from '@cdc/core/components/MediaControls'
|
|
10
|
-
import SkipTo from '@cdc/core/components/elements/SkipTo'
|
|
11
|
-
|
|
12
|
-
import Loading from '@cdc/core/components/Loading'
|
|
13
|
-
import { navigationHandler } from '../helpers'
|
|
14
|
-
import ConfigContext, { MapDispatchContext } from '../context'
|
|
15
|
-
import { publishAnalyticsEvent } from '@cdc/core/helpers/metrics/helpers'
|
|
16
|
-
|
|
17
|
-
const DataTable = props => {
|
|
18
|
-
const {
|
|
19
|
-
state,
|
|
20
|
-
tableTitle,
|
|
21
|
-
indexTitle,
|
|
22
|
-
mapTitle,
|
|
23
|
-
rawData,
|
|
24
|
-
runtimeData,
|
|
25
|
-
headerColor,
|
|
26
|
-
expandDataTable,
|
|
27
|
-
columns,
|
|
28
|
-
displayDataAsText,
|
|
29
|
-
applyLegendToRow,
|
|
30
|
-
displayGeoName,
|
|
31
|
-
formatLegendLocation,
|
|
32
|
-
tabbingId,
|
|
33
|
-
interactionLabel
|
|
34
|
-
} = props
|
|
35
|
-
|
|
36
|
-
const dispatch = useContext(MapDispatchContext)
|
|
37
|
-
const { currentViewport: viewport } = useContext(ConfigContext)
|
|
38
|
-
const [expanded, setExpanded] = useState(expandDataTable)
|
|
39
|
-
const [sortBy, setSortBy] = useState({ column: 'geo', asc: false })
|
|
40
|
-
const [accessibilityLabel, setAccessibilityLabel] = useState('')
|
|
41
|
-
const fileName = `${mapTitle || 'data-table'}.csv`
|
|
42
|
-
|
|
43
|
-
// Catch all sorting method used on load by default but also on user click
|
|
44
|
-
// Having a custom method means we can add in any business logic we want going forward
|
|
45
|
-
const customSort = (a, b) => {
|
|
46
|
-
const digitRegex = /\d+/
|
|
47
|
-
|
|
48
|
-
const hasNumber = value => digitRegex.test(value)
|
|
49
|
-
|
|
50
|
-
// force null and undefined to the bottom
|
|
51
|
-
a = a === null || a === undefined ? '' : a
|
|
52
|
-
b = b === null || b === undefined ? '' : b
|
|
53
|
-
|
|
54
|
-
// convert any strings that are actually numbers to proper data type
|
|
55
|
-
const aNum = Number(a)
|
|
56
|
-
|
|
57
|
-
if (!Number.isNaN(aNum)) {
|
|
58
|
-
a = aNum
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const bNum = Number(b)
|
|
62
|
-
|
|
63
|
-
if (!Number.isNaN(bNum)) {
|
|
64
|
-
b = bNum
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// remove iso code prefixes
|
|
68
|
-
if (typeof a === 'string') {
|
|
69
|
-
a = a.replace('us-', '')
|
|
70
|
-
a = displayGeoName(a)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (typeof b === 'string') {
|
|
74
|
-
b = b.replace('us-', '')
|
|
75
|
-
b = displayGeoName(b)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// force any string values to lowercase
|
|
79
|
-
a = typeof a === 'string' ? a.toLowerCase() : a
|
|
80
|
-
b = typeof b === 'string' ? b.toLowerCase() : b
|
|
81
|
-
|
|
82
|
-
// If the string contains a number, remove the text from the value and only sort by the number. Only uses the first number it finds.
|
|
83
|
-
if (typeof a === 'string' && hasNumber(a) === true) {
|
|
84
|
-
a = a.match(digitRegex)[0]
|
|
85
|
-
|
|
86
|
-
a = Number(a)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (typeof b === 'string' && hasNumber(b) === true) {
|
|
90
|
-
b = b.match(digitRegex)[0]
|
|
91
|
-
|
|
92
|
-
b = Number(b)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// When comparing a number to a string, always send string to bottom
|
|
96
|
-
if (typeof a === 'number' && typeof b === 'string') {
|
|
97
|
-
return 1
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (typeof b === 'number' && typeof a === 'string') {
|
|
101
|
-
return -1
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Return either 1 or -1 to indicate a sort priority
|
|
105
|
-
if (a > b) {
|
|
106
|
-
return 1
|
|
107
|
-
}
|
|
108
|
-
if (a < b) {
|
|
109
|
-
return -1
|
|
110
|
-
}
|
|
111
|
-
// returning 0, undefined or any falsey value will use subsequent sorts or
|
|
112
|
-
// the index as a tiebreaker
|
|
113
|
-
return 0
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Optionally wrap cell with anchor if config defines a navigation url
|
|
117
|
-
const getCellAnchor = (markup, row) => {
|
|
118
|
-
if (columns.navigate && row[columns.navigate.name]) {
|
|
119
|
-
markup = (
|
|
120
|
-
<span
|
|
121
|
-
onClick={() => navigationHandler(state.general.navigationTarget, row[columns.navigate.name])}
|
|
122
|
-
className='table-link'
|
|
123
|
-
title='Click for more information (Opens in a new window)'
|
|
124
|
-
role='link'
|
|
125
|
-
tabIndex='0'
|
|
126
|
-
onKeyDown={e => {
|
|
127
|
-
if (e.keyCode === 13) {
|
|
128
|
-
navigationHandler(state.general.navigationTarget, row[columns.navigate.name])
|
|
129
|
-
}
|
|
130
|
-
}}
|
|
131
|
-
>
|
|
132
|
-
{markup}
|
|
133
|
-
<ExternalIcon className='inline-icon' />
|
|
134
|
-
</span>
|
|
135
|
-
)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return markup
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const rand = Math.random().toString(16).substr(2, 8)
|
|
142
|
-
const skipId = `btn__${rand}`
|
|
143
|
-
|
|
144
|
-
const mapLookup = {
|
|
145
|
-
'us-county': 'United States County Map',
|
|
146
|
-
'single-state': 'State Map',
|
|
147
|
-
us: 'United States Map',
|
|
148
|
-
world: 'World Map'
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const DownloadButton = memo(() => {
|
|
152
|
-
let csvData
|
|
153
|
-
if (state.general.type === 'bubble' || !state.table.showFullGeoNameInCSV) {
|
|
154
|
-
// Just Unparse
|
|
155
|
-
csvData = Papa.unparse(rawData)
|
|
156
|
-
} else if (state.general.geoType !== 'us-county' || state.general.type === 'us-geocode') {
|
|
157
|
-
// Unparse + Add column for full Geo name
|
|
158
|
-
csvData = Papa.unparse(rawData.map(row => ({ FullGeoName: displayGeoName(row[state.columns.geo.name]), ...row })))
|
|
159
|
-
} else {
|
|
160
|
-
// Unparse + Add column for full Geo name
|
|
161
|
-
csvData = Papa.unparse(
|
|
162
|
-
rawData.map(row => ({ FullGeoName: formatLegendLocation(row[state.columns.geo.name]), ...row }))
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' })
|
|
167
|
-
|
|
168
|
-
const saveBlob = () => {
|
|
169
|
-
//@ts-ignore
|
|
170
|
-
if (typeof window.navigator.msSaveBlob === 'function') {
|
|
171
|
-
//@ts-ignore
|
|
172
|
-
navigator.msSaveBlob(blob, fileName)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return (
|
|
177
|
-
<a
|
|
178
|
-
download={fileName}
|
|
179
|
-
type='button'
|
|
180
|
-
onClick={() => {
|
|
181
|
-
saveBlob
|
|
182
|
-
publishAnalyticsEvent('data_downloaded', 'click', interactionLabel)
|
|
183
|
-
}}
|
|
184
|
-
href={URL.createObjectURL(blob)}
|
|
185
|
-
aria-label='Download this data in a CSV file format.'
|
|
186
|
-
className={`${headerColor} no-border`}
|
|
187
|
-
id={`${skipId}`}
|
|
188
|
-
data-html2canvas-ignore={true}
|
|
189
|
-
role='button'
|
|
190
|
-
>
|
|
191
|
-
Download Data (CSV)
|
|
192
|
-
</a>
|
|
193
|
-
)
|
|
194
|
-
}, [rawData, state.table])
|
|
195
|
-
|
|
196
|
-
const TableMediaControls = ({ belowTable }) => {
|
|
197
|
-
return (
|
|
198
|
-
<MediaControls.Section classes={['download-links']}>
|
|
199
|
-
<MediaControls.Link config={state} interactionLabel={interactionLabel} />
|
|
200
|
-
{state.table.download && <DownloadButton />}
|
|
201
|
-
</MediaControls.Section>
|
|
202
|
-
)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Change accessibility label depending on expanded status
|
|
206
|
-
useEffect(() => {
|
|
207
|
-
const expandedLabel = 'Accessible data table.'
|
|
208
|
-
const collapsedLabel =
|
|
209
|
-
'Accessible data table. This table is currently collapsed visually but can still be read using a screen reader.'
|
|
210
|
-
|
|
211
|
-
if (expanded === true && accessibilityLabel !== expandedLabel) {
|
|
212
|
-
setAccessibilityLabel(expandedLabel)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (expanded === false && accessibilityLabel !== collapsedLabel) {
|
|
216
|
-
setAccessibilityLabel(collapsedLabel)
|
|
217
|
-
}
|
|
218
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
219
|
-
}, [expanded])
|
|
220
|
-
|
|
221
|
-
if (!state.data) return <Loading />
|
|
222
|
-
|
|
223
|
-
const rows = Object.keys(runtimeData)
|
|
224
|
-
.filter(row => applyLegendToRow(runtimeData[row], state))
|
|
225
|
-
.sort((a, b) => {
|
|
226
|
-
const sortVal = customSort(
|
|
227
|
-
runtimeData[a][state.columns[sortBy.column].name],
|
|
228
|
-
runtimeData[b][state.columns[sortBy.column].name]
|
|
229
|
-
)
|
|
230
|
-
if (!sortBy.asc) return sortVal
|
|
231
|
-
if (sortVal === 0) return 0
|
|
232
|
-
if (sortVal < 0) return 1
|
|
233
|
-
return -1
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
return (
|
|
237
|
-
<ErrorBoundary component='DataTable'>
|
|
238
|
-
{!state.table.showDownloadLinkBelow && <TableMediaControls />}
|
|
239
|
-
<section
|
|
240
|
-
id={tabbingId.replace('#', '')}
|
|
241
|
-
className={`data-table-container ${viewport}`}
|
|
242
|
-
aria-label={accessibilityLabel}
|
|
243
|
-
>
|
|
244
|
-
<SkipTo skipId={skipId} skipMessage='Skip Data Table' />
|
|
245
|
-
<div
|
|
246
|
-
className={expanded ? 'data-table-heading' : 'collapsed data-table-heading'}
|
|
247
|
-
onClick={() => {
|
|
248
|
-
setExpanded(!expanded)
|
|
249
|
-
}}
|
|
250
|
-
tabIndex='0'
|
|
251
|
-
onKeyDown={e => {
|
|
252
|
-
if (e.keyCode === 13) {
|
|
253
|
-
setExpanded(!expanded)
|
|
254
|
-
}
|
|
255
|
-
}}
|
|
256
|
-
>
|
|
257
|
-
<Icon display={expanded ? 'minus' : 'plus'} base />
|
|
258
|
-
{tableTitle}
|
|
259
|
-
</div>
|
|
260
|
-
<div
|
|
261
|
-
className='table-container'
|
|
262
|
-
style={{ maxHeight: state.dataTable.limitHeight && `${state.dataTable.height}px`, overflowY: 'scroll' }}
|
|
263
|
-
>
|
|
264
|
-
<table
|
|
265
|
-
height={expanded ? null : 0}
|
|
266
|
-
role='table'
|
|
267
|
-
aria-live='assertive'
|
|
268
|
-
className={expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'}
|
|
269
|
-
hidden={!expanded}
|
|
270
|
-
aria-rowcount={state?.data.length ? state.data.length : '-1'}
|
|
271
|
-
>
|
|
272
|
-
<caption className='cdcdataviz-sr-only'>
|
|
273
|
-
{state.dataTable.caption
|
|
274
|
-
? state.dataTable.caption
|
|
275
|
-
: `Datatable showing data for the ${mapLookup[state.general.geoType]} figure.`}
|
|
276
|
-
</caption>
|
|
277
|
-
<thead style={{ position: 'sticky', top: 0, zIndex: 999 }}>
|
|
278
|
-
<tr>
|
|
279
|
-
{Object.keys(columns)
|
|
280
|
-
.filter(column => columns[column].dataTable === true && columns[column].name)
|
|
281
|
-
.map(column => {
|
|
282
|
-
let text
|
|
283
|
-
if (column !== 'geo') {
|
|
284
|
-
text = columns[column].label ? columns[column].label : columns[column].name
|
|
285
|
-
} else {
|
|
286
|
-
text = indexTitle || 'Location'
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return (
|
|
290
|
-
<th
|
|
291
|
-
key={`col-header-${column}`}
|
|
292
|
-
tabIndex={0}
|
|
293
|
-
title={text}
|
|
294
|
-
role='columnheader'
|
|
295
|
-
scope='col'
|
|
296
|
-
onClick={() => {
|
|
297
|
-
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
|
|
298
|
-
}}
|
|
299
|
-
onKeyDown={e => {
|
|
300
|
-
if (e.keyCode === 13) {
|
|
301
|
-
setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false })
|
|
302
|
-
}
|
|
303
|
-
}}
|
|
304
|
-
className={
|
|
305
|
-
sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'
|
|
306
|
-
}
|
|
307
|
-
{...(sortBy.column === column
|
|
308
|
-
? sortBy.asc
|
|
309
|
-
? { 'aria-sort': 'ascending' }
|
|
310
|
-
: { 'aria-sort': 'descending' }
|
|
311
|
-
: null)}
|
|
312
|
-
>
|
|
313
|
-
{text}
|
|
314
|
-
<span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${
|
|
315
|
-
sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'
|
|
316
|
-
} order`}</span>
|
|
317
|
-
</th>
|
|
318
|
-
)
|
|
319
|
-
})}
|
|
320
|
-
</tr>
|
|
321
|
-
</thead>
|
|
322
|
-
<tbody>
|
|
323
|
-
{rows.map(row => {
|
|
324
|
-
return (
|
|
325
|
-
<tr role='row'>
|
|
326
|
-
{Object.keys(columns)
|
|
327
|
-
.filter(column => columns[column].dataTable === true && columns[column].name)
|
|
328
|
-
.map(column => {
|
|
329
|
-
let cellValue
|
|
330
|
-
|
|
331
|
-
if (column === 'geo') {
|
|
332
|
-
const rowObj = runtimeData[row]
|
|
333
|
-
const legendColor = applyLegendToRow(rowObj, state)
|
|
334
|
-
|
|
335
|
-
var labelValue
|
|
336
|
-
if (state.general.geoType !== 'us-county' || state.general.type === 'us-geocode') {
|
|
337
|
-
labelValue = displayGeoName(row)
|
|
338
|
-
} else {
|
|
339
|
-
labelValue = formatLegendLocation(row)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
labelValue = getCellAnchor(labelValue, rowObj)
|
|
343
|
-
|
|
344
|
-
cellValue = (
|
|
345
|
-
<>
|
|
346
|
-
<LegendShape fill={legendColor[0]} />
|
|
347
|
-
{labelValue}
|
|
348
|
-
</>
|
|
349
|
-
)
|
|
350
|
-
} else {
|
|
351
|
-
cellValue = displayDataAsText(runtimeData[row][state.columns[column].name], column, state)
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return (
|
|
355
|
-
<td
|
|
356
|
-
tabIndex='0'
|
|
357
|
-
role='gridcell'
|
|
358
|
-
onClick={e =>
|
|
359
|
-
state.general.type === 'bubble' &&
|
|
360
|
-
state.general.allowMapZoom &&
|
|
361
|
-
state.general.geoType === 'world'
|
|
362
|
-
? dispatch({ type: 'SET_FILTERED_COUNTRY_CODE', payload: row })
|
|
363
|
-
: true
|
|
364
|
-
}
|
|
365
|
-
>
|
|
366
|
-
{cellValue}
|
|
367
|
-
</td>
|
|
368
|
-
)
|
|
369
|
-
})}
|
|
370
|
-
</tr>
|
|
371
|
-
)
|
|
372
|
-
})}
|
|
373
|
-
</tbody>
|
|
374
|
-
</table>
|
|
375
|
-
</div>
|
|
376
|
-
</section>
|
|
377
|
-
{state.table.showDownloadLinkBelow && <TableMediaControls belowTable={true} />}
|
|
378
|
-
<div id={skipId} className='cdcdataviz-sr-only'>
|
|
379
|
-
Skipped data table.
|
|
380
|
-
</div>
|
|
381
|
-
</ErrorBoundary>
|
|
382
|
-
)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
export default DataTable
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { memo, useState, useEffect } from 'react'
|
|
2
|
-
import { useDebounce } from 'use-debounce'
|
|
3
|
-
|
|
4
|
-
// todo: look into combining these with core
|
|
5
|
-
const CheckBox = memo(({ label, value, fieldName, section = null, subsection = null, tooltip, updateField, ...attributes }) => (
|
|
6
|
-
<label className='checkbox column-heading'>
|
|
7
|
-
<input
|
|
8
|
-
type='checkbox'
|
|
9
|
-
name={fieldName}
|
|
10
|
-
checked={value}
|
|
11
|
-
onChange={e => {
|
|
12
|
-
updateField(section, subsection, fieldName, !value)
|
|
13
|
-
}}
|
|
14
|
-
{...attributes}
|
|
15
|
-
/>
|
|
16
|
-
<span className='edit-label'>
|
|
17
|
-
{label}
|
|
18
|
-
{tooltip}
|
|
19
|
-
</span>
|
|
20
|
-
</label>
|
|
21
|
-
))
|
|
22
|
-
|
|
23
|
-
const TextField = ({ label, section = null, subsection = null, fieldName, updateField, value: stateValue, type = 'input', tooltip, ...attributes }) => {
|
|
24
|
-
const [value, setValue] = useState(stateValue)
|
|
25
|
-
|
|
26
|
-
const [debouncedValue] = useDebounce(value, 500)
|
|
27
|
-
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
if ('string' === typeof debouncedValue && stateValue !== debouncedValue) {
|
|
30
|
-
updateField(section, subsection, fieldName, debouncedValue)
|
|
31
|
-
}
|
|
32
|
-
}, [debouncedValue]) // eslint-disable-line
|
|
33
|
-
|
|
34
|
-
let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`
|
|
35
|
-
|
|
36
|
-
const onChange = e => setValue(e.target.value)
|
|
37
|
-
|
|
38
|
-
let formElement = <input type='text' name={name} onChange={onChange} {...attributes} value={value} />
|
|
39
|
-
|
|
40
|
-
if ('textarea' === type) {
|
|
41
|
-
formElement = <textarea name={name} onChange={onChange} {...attributes} value={value}></textarea>
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if ('number' === type) {
|
|
45
|
-
formElement = <input type='number' name={name} onChange={onChange} {...attributes} value={value} />
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<label>
|
|
50
|
-
<span className='edit-label column-heading'>
|
|
51
|
-
{label}
|
|
52
|
-
{tooltip}
|
|
53
|
-
</span>
|
|
54
|
-
{formElement}
|
|
55
|
-
</label>
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export { CheckBox, TextField }
|
package/src/coreStyles_map.scss
DELETED