@cdc/map 4.25.3 → 4.25.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.idea/map.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/dist/cdcmap.js +31254 -32242
- package/examples/hex-colors.json +3 -3
- package/examples/m2.json +32904 -0
- package/examples/private/test.json +470 -1457
- package/examples/private/{mmr.json → wastewatermap.json} +86 -115
- package/index.html +36 -63
- package/package.json +7 -19
- package/src/CdcMap.tsx +56 -1552
- package/src/CdcMapComponent.tsx +608 -0
- package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +10 -0
- package/src/_stories/CdcMap.Legend.stories.tsx +67 -0
- package/src/_stories/CdcMap.Table.stories.tsx +19 -0
- package/src/_stories/CdcMap.stories.tsx +12 -1
- package/src/_stories/UsaMap.NoData.stories.tsx +4 -4
- package/src/_stories/_mock/default-patterns.json +8 -5
- package/src/_stories/_mock/legend-bins.json +428 -0
- package/{examples/private/default-patterns.json → src/_stories/_mock/legends/legend-tests.json} +36 -131
- package/src/cdcMapComponent.styles.css +9 -0
- package/src/components/Annotation/Annotation.Draggable.tsx +27 -26
- package/src/components/Annotation/AnnotationDropdown.tsx +5 -6
- package/src/components/BubbleList.tsx +135 -49
- package/src/components/CityList.tsx +89 -87
- package/src/components/DataTable.tsx +8 -8
- package/src/components/EditorPanel/components/EditorPanel.tsx +823 -885
- package/src/components/EditorPanel/components/Error.tsx +9 -2
- package/src/components/EditorPanel/components/HexShapeSettings.tsx +127 -141
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +55 -86
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +89 -75
- package/src/components/EditorPanel/components/editorPanel.styles.css +95 -0
- package/src/components/Geo.tsx +9 -1
- package/src/components/GoogleMap/components/GoogleMap.tsx +1 -1
- package/src/components/Legend/components/Legend.tsx +92 -87
- package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +128 -0
- package/src/components/Legend/components/LegendGroup/legend.group.css +27 -0
- package/src/components/Legend/components/LegendItem.Hex.tsx +4 -1
- package/src/components/Legend/components/index.scss +74 -17
- package/src/components/Modal.tsx +17 -7
- package/src/components/NavigationMenu.tsx +11 -9
- package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +12 -8
- package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +4 -4
- package/src/components/UsaMap/components/TerritoriesSection.tsx +33 -10
- package/src/components/UsaMap/components/Territory/Territory.Hexagon.tsx +12 -10
- package/src/components/UsaMap/components/Territory/Territory.Rectangle.tsx +12 -14
- package/src/components/UsaMap/components/Territory/TerritoryShape.ts +2 -1
- package/src/components/UsaMap/components/UsaMap.County.tsx +138 -96
- package/src/components/UsaMap/components/UsaMap.Region.styles.css +72 -0
- package/src/components/UsaMap/components/UsaMap.Region.tsx +56 -103
- package/src/components/UsaMap/components/UsaMap.SingleState.styles.css +10 -0
- package/src/components/UsaMap/components/UsaMap.SingleState.tsx +65 -74
- package/src/components/UsaMap/components/UsaMap.State.tsx +112 -91
- package/src/components/UsaMap/helpers/map.ts +1 -1
- package/src/components/UsaMap/helpers/shapes.ts +20 -7
- package/src/components/WorldMap/WorldMap.tsx +64 -118
- package/src/components/WorldMap/worldMap.styles.css +28 -0
- package/src/components/ZoomControls.tsx +15 -13
- package/src/components/zoomControls.styles.css +53 -0
- package/src/context.ts +17 -9
- package/src/data/initial-state.js +5 -2
- package/src/helpers/addUIDs.ts +150 -0
- package/src/helpers/applyColorToLegend.ts +39 -64
- package/src/helpers/applyLegendToRow.ts +51 -0
- package/src/helpers/colorDistributions.ts +12 -0
- package/src/helpers/constants.ts +44 -0
- package/src/helpers/displayGeoName.ts +9 -2
- package/src/helpers/formatLegendLocation.ts +3 -2
- package/src/helpers/generateColorsArray.ts +2 -1
- package/src/helpers/generateRuntimeData.ts +78 -0
- package/src/helpers/generateRuntimeFilters.ts +63 -0
- package/src/helpers/generateRuntimeLegend.ts +566 -0
- package/src/helpers/generateRuntimeLegendHash.ts +16 -15
- package/src/helpers/getColumnNames.ts +19 -0
- package/src/helpers/getMapContainerClasses.ts +23 -0
- package/src/helpers/getStatePicked.ts +8 -0
- package/src/helpers/handleMapTabbing.ts +31 -0
- package/src/helpers/hashObj.ts +1 -1
- package/src/helpers/index.ts +22 -0
- package/src/helpers/navigationHandler.ts +3 -3
- package/src/helpers/resetLegendToggles.ts +13 -0
- package/src/helpers/setBinNumbers.ts +5 -0
- package/src/helpers/sortSpecialClassesLast.ts +7 -0
- package/src/helpers/tests/getColumnNames.test.ts +52 -0
- package/src/helpers/titleCase.ts +1 -1
- package/src/helpers/toggleLegendActive.ts +25 -0
- package/src/hooks/useApplyTooltipsToGeo.tsx +51 -0
- package/src/hooks/useColumnsRequiredChecker.ts +51 -0
- package/src/hooks/useGeoClickHandler.ts +45 -0
- package/src/hooks/useLegendSeparators.ts +26 -0
- package/src/hooks/useMapLayers.tsx +34 -60
- package/src/hooks/useModal.ts +22 -0
- package/src/hooks/useResizeObserver.ts +4 -5
- package/src/hooks/useStateZoom.tsx +52 -75
- package/src/hooks/useTooltip.ts +2 -3
- package/src/index.jsx +3 -9
- package/src/scss/editor-panel.scss +3 -99
- package/src/scss/main.scss +1 -19
- package/src/scss/map.scss +15 -220
- package/src/store/map.actions.ts +46 -0
- package/src/store/map.reducer.ts +96 -0
- package/src/types/Annotations.ts +24 -0
- package/src/types/MapConfig.ts +23 -3
- package/src/types/MapContext.ts +36 -35
- package/src/types/Modal.ts +1 -0
- package/src/types/RuntimeData.ts +3 -0
- package/examples/private/DEV-9644.json +0 -184
- package/examples/private/DEV-9989.json +0 -229
- package/examples/private/ardi.json +0 -180
- package/examples/private/colors 2.json +0 -416
- package/examples/private/colors.json +0 -416
- package/examples/private/colors.json.zip +0 -0
- package/examples/private/customColors.json +0 -45348
- package/examples/test.json +0 -183
- package/src/helpers/closeModal.ts +0 -9
- package/src/scss/btn.scss +0 -69
- package/src/scss/filters.scss +0 -27
- package/src/scss/variables.scss +0 -1
- /package/src/hooks/{useActiveElement.js → useActiveElement.ts} +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { addUIDs } from './addUIDs'
|
|
2
|
+
export { applyColorToLegend } from './applyColorToLegend'
|
|
3
|
+
export { colorDistributions } from './colorDistributions'
|
|
4
|
+
export { displayGeoName } from './displayGeoName'
|
|
5
|
+
export { formatLegendLocation } from './formatLegendLocation'
|
|
6
|
+
export { generateColorsArray } from './generateColorsArray'
|
|
7
|
+
export { generateRuntimeLegendHash } from './generateRuntimeLegendHash'
|
|
8
|
+
export { getGeoStrokeColor, getGeoFillColor } from './colors'
|
|
9
|
+
export { getUniqueValues } from './getUniqueValues'
|
|
10
|
+
export { handleMapAriaLabels } from './handleMapAriaLabels'
|
|
11
|
+
export { handleMapTabbing } from './handleMapTabbing'
|
|
12
|
+
export { hashObj } from './hashObj'
|
|
13
|
+
export { indexOfIgnoreType } from './indexOfIgnoreType'
|
|
14
|
+
export { navigationHandler } from './navigationHandler'
|
|
15
|
+
export { resetLegendToggles } from './resetLegendToggles'
|
|
16
|
+
export { setBinNumbers } from './setBinNumbers'
|
|
17
|
+
export { sortSpecialClassesLast } from './sortSpecialClassesLast'
|
|
18
|
+
export { titleCase as toTitleCase } from './toTitleCase'
|
|
19
|
+
export { titleCase } from './titleCase'
|
|
20
|
+
export { validateFipsCodeLength } from './validateFipsCodeLength'
|
|
21
|
+
export { getMapContainerClasses } from './getMapContainerClasses'
|
|
22
|
+
export { SVG_HEIGHT, SVG_WIDTH, SVG_PADDING, SVG_VIEWBOX, HEADER_COLORS, MAX_ZOOM_LEVEL } from './constants'
|
|
@@ -9,9 +9,9 @@ export const navigationHandler = (
|
|
|
9
9
|
return
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
// Abort if
|
|
13
|
-
if (
|
|
14
|
-
throw Error('
|
|
12
|
+
// Abort if urlString is not a valid string
|
|
13
|
+
if (typeof urlString !== 'string' || urlString.trim().length === 0) {
|
|
14
|
+
throw Error('Invalid or blank URL. Navigation aborted.')
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const urlObj = new URL(urlString, window.location.origin)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
export const resetLegendToggles = (runtimeLegend, setRuntimeLegend) => {
|
|
3
|
+
const legendCopy = _.cloneDeep(runtimeLegend)
|
|
4
|
+
|
|
5
|
+
legendCopy.items.forEach(legendItem => {
|
|
6
|
+
delete legendItem.disabled
|
|
7
|
+
})
|
|
8
|
+
legendCopy.disabledAmt = 0
|
|
9
|
+
|
|
10
|
+
legendCopy.runtimeDataHash = runtimeLegend.runtimeDataHash
|
|
11
|
+
|
|
12
|
+
setRuntimeLegend(legendCopy)
|
|
13
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const sortSpecialClassesLast = (result, legend) => {
|
|
2
|
+
if (legend.showSpecialClassesLast) {
|
|
3
|
+
const specialRows = result.items.filter(d => d.special === true)
|
|
4
|
+
const otherRows = result.items.filter(d => !d.special)
|
|
5
|
+
result.items = [...otherRows, ...specialRows]
|
|
6
|
+
}
|
|
7
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getColumnNames } from './../getColumnNames'
|
|
2
|
+
import { MapConfig } from '../../types/MapConfig'
|
|
3
|
+
|
|
4
|
+
// write a test that returns null if columns are not provided
|
|
5
|
+
describe('missing config.columns parameter', () => {
|
|
6
|
+
it('should return null', () => {
|
|
7
|
+
const columns = null
|
|
8
|
+
expect(getColumnNames(columns)).toBeNull()
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('returns null for undefined column names', () => {
|
|
13
|
+
const columns: Pick<MapConfig, 'columns'> = {
|
|
14
|
+
columns: {
|
|
15
|
+
geo: { name: undefined },
|
|
16
|
+
primary: { name: undefined },
|
|
17
|
+
latitude: { name: undefined },
|
|
18
|
+
longitude: { name: undefined },
|
|
19
|
+
categorical: { name: undefined }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const result = getColumnNames(columns)
|
|
24
|
+
expect(result).toEqual({
|
|
25
|
+
geoColumnName: null,
|
|
26
|
+
primaryColumnName: null,
|
|
27
|
+
latitudeColumnName: null,
|
|
28
|
+
longitudeColumnName: null,
|
|
29
|
+
categoricalColumnName: null
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns null for empty column names', () => {
|
|
34
|
+
const columns: Pick<MapConfig, 'columns'> = {
|
|
35
|
+
columns: {
|
|
36
|
+
geo: { name: '' },
|
|
37
|
+
primary: { name: '' },
|
|
38
|
+
latitude: { name: '' },
|
|
39
|
+
longitude: { name: '' },
|
|
40
|
+
categorical: { name: '' }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = getColumnNames(columns)
|
|
45
|
+
expect(result).toEqual({
|
|
46
|
+
geoColumnName: null,
|
|
47
|
+
primaryColumnName: null,
|
|
48
|
+
latitudeColumnName: null,
|
|
49
|
+
longitudeColumnName: null,
|
|
50
|
+
categoricalColumnName: null
|
|
51
|
+
})
|
|
52
|
+
})
|
package/src/helpers/titleCase.ts
CHANGED
|
@@ -22,7 +22,7 @@ export const titleCase = string => {
|
|
|
22
22
|
// just return with each word uppercase
|
|
23
23
|
return string
|
|
24
24
|
.split(' ')
|
|
25
|
-
.map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())
|
|
25
|
+
.map(word => (word === 'of' ? word : word.charAt(0).toUpperCase() + word.substring(1).toLowerCase()))
|
|
26
26
|
.join(' ')
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
|
|
3
|
+
export const toggleLegendActive = (
|
|
4
|
+
i: number,
|
|
5
|
+
legendLabel: string,
|
|
6
|
+
runtimeLegend,
|
|
7
|
+
setRuntimeLegend,
|
|
8
|
+
setAccessibleStatus: (message: string) => void
|
|
9
|
+
) => {
|
|
10
|
+
const runtimeLegendCopy = _.cloneDeep(runtimeLegend)
|
|
11
|
+
|
|
12
|
+
// Create and toggle the new value
|
|
13
|
+
const newValue = !runtimeLegendCopy.items?.[i].disabled
|
|
14
|
+
runtimeLegendCopy.items[i].disabled = newValue
|
|
15
|
+
|
|
16
|
+
const disabledAmt = runtimeLegend.disabledAmt ?? 0
|
|
17
|
+
|
|
18
|
+
runtimeLegendCopy['disabledAmt'] = newValue ? disabledAmt + 1 : disabledAmt - 1
|
|
19
|
+
|
|
20
|
+
setRuntimeLegend(runtimeLegendCopy)
|
|
21
|
+
|
|
22
|
+
setAccessibleStatus(
|
|
23
|
+
`Disabled legend item ${legendLabel ?? ''}. Please reference the data table to see updated values.`
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ReactNode, useContext } from 'react'
|
|
2
|
+
import { displayGeoName, navigationHandler } from '../helpers'
|
|
3
|
+
import ConfigContext from '../context'
|
|
4
|
+
import useTooltip from './useTooltip'
|
|
5
|
+
import { supportedStatesFipsCodes } from './../data/supported-geos'
|
|
6
|
+
import parse from 'html-react-parser'
|
|
7
|
+
import isDomainExternal from '@cdc/core/helpers/isDomainExternal'
|
|
8
|
+
import ExternalIcon from './../images/external-link.svg'
|
|
9
|
+
|
|
10
|
+
const useApplyTooltipsToGeo = () => {
|
|
11
|
+
const { config, customNavigationHandler } = useContext(ConfigContext)
|
|
12
|
+
const navigationColumnName = config.columns.navigate.name
|
|
13
|
+
const { buildTooltip } = useTooltip({ config, displayGeoName, supportedStatesFipsCodes })
|
|
14
|
+
|
|
15
|
+
const applyTooltipsToGeo = (geoName: string, row: Object, returnType = 'string') => {
|
|
16
|
+
let toolTipText: string | ReactNode = buildTooltip(row, geoName, '')
|
|
17
|
+
|
|
18
|
+
// We convert the markup into JSX and add a navigation link if it's going into a modal.
|
|
19
|
+
if ('jsx' === returnType) {
|
|
20
|
+
if (typeof toolTipText === 'string') {
|
|
21
|
+
toolTipText = [<div key='modal-content'>{parse(toolTipText)}</div>]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (config.columns.hasOwnProperty('navigate') && row[navigationColumnName]) {
|
|
25
|
+
// Check that toolTipText is an array before pushing to it
|
|
26
|
+
if (Array.isArray(toolTipText)) {
|
|
27
|
+
toolTipText.push(
|
|
28
|
+
<a
|
|
29
|
+
href='#'
|
|
30
|
+
className='navigation-link'
|
|
31
|
+
key='modal-navigation-link'
|
|
32
|
+
onClick={e => {
|
|
33
|
+
e.preventDefault()
|
|
34
|
+
navigationHandler(config.general.navigationTarget, row[navigationColumnName], customNavigationHandler)
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{config.tooltips.linkLabel}
|
|
38
|
+
{isDomainExternal(row[navigationColumnName]) && <ExternalIcon className='inline-icon ms-1' />}
|
|
39
|
+
</a>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return toolTipText
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { applyTooltipsToGeo }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default useApplyTooltipsToGeo
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import ConfigContext, { MapDispatchContext } from '../context'
|
|
3
|
+
import { getColumnNames } from '../helpers/getColumnNames'
|
|
4
|
+
|
|
5
|
+
const useColumnsRequiredChecker = () => {
|
|
6
|
+
const { config } = useContext(ConfigContext)
|
|
7
|
+
const dispatch = useContext(MapDispatchContext)
|
|
8
|
+
|
|
9
|
+
const columnsRequiredChecker = () => {
|
|
10
|
+
const { primaryColumnName, geoColumnName } = getColumnNames(config.columns)
|
|
11
|
+
|
|
12
|
+
let columnList = []
|
|
13
|
+
|
|
14
|
+
// Geo is always required
|
|
15
|
+
if (!geoColumnName) {
|
|
16
|
+
columnList.push('Geography')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Primary is required if we're on a data map or a point map
|
|
20
|
+
if ('navigation' !== config.general.type && '' === primaryColumnName) {
|
|
21
|
+
columnList.push('Primary')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Navigate is required for navigation maps
|
|
25
|
+
if ('navigation' === config.general.type && '' === config.columns.navigate.name) {
|
|
26
|
+
columnList.push('Navigation')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
('us-geocode' === config.general.type || 'world-geocode' === config.general.type) &&
|
|
31
|
+
'' === config.columns.latitude.name
|
|
32
|
+
) {
|
|
33
|
+
columnList.push('Latitude')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
('us-geocode' === config.general.type || 'world-geocode' === config.general.type) &&
|
|
38
|
+
'' === config.columns.longitude.name
|
|
39
|
+
) {
|
|
40
|
+
columnList.push('Longitude')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (columnList.length === 0) columnList = null
|
|
44
|
+
|
|
45
|
+
dispatch({ type: 'SET_REQUIRED_COLUMNS', payload: columnList })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { columnsRequiredChecker }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default useColumnsRequiredChecker
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import ConfigContext, { MapDispatchContext } from '../context'
|
|
2
|
+
import { navigationHandler } from '../helpers'
|
|
3
|
+
import { useContext } from 'react'
|
|
4
|
+
|
|
5
|
+
const useGeoClickHandler = () => {
|
|
6
|
+
const { config: state, setConfig, setSharedFilter, customNavigationHandler } = useContext(ConfigContext)
|
|
7
|
+
const dispatch = useContext(MapDispatchContext)
|
|
8
|
+
|
|
9
|
+
const geoClickHandler = (geoDisplayName: string, geoData: object): void => {
|
|
10
|
+
if (setSharedFilter) {
|
|
11
|
+
setSharedFilter(state.uid, geoData)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// If world-geocode map zoom to geo point
|
|
15
|
+
if (['world-geocode'].includes(state.general.type)) {
|
|
16
|
+
const lat = geoData[state.columns.latitude.name]
|
|
17
|
+
const long = geoData[state.columns.longitude.name]
|
|
18
|
+
|
|
19
|
+
setConfig({
|
|
20
|
+
...state,
|
|
21
|
+
mapPosition: { coordinates: [long, lat], zoom: 3 }
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// If modals are set, or we are on a mobile viewport, display modal
|
|
26
|
+
if (window.matchMedia('(any-hover: none)').matches || 'click' === state.tooltips.appearanceType) {
|
|
27
|
+
const modalData = {
|
|
28
|
+
geoName: geoDisplayName,
|
|
29
|
+
keyedData: geoData
|
|
30
|
+
}
|
|
31
|
+
dispatch({ type: 'SET_MODAL', payload: modalData })
|
|
32
|
+
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Otherwise if this item has a link specified for it, do regular navigation.
|
|
37
|
+
if (state.columns.navigate && geoData[state.columns.navigate.name]) {
|
|
38
|
+
navigationHandler(state.general.navigationTarget, geoData[state.columns.navigate.name], customNavigationHandler)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { geoClickHandler }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default useGeoClickHandler
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { clamp } from 'lodash'
|
|
2
|
+
|
|
3
|
+
// TODO: generalize this to be used in legends other than linear block gradient
|
|
4
|
+
|
|
5
|
+
const LEGEND_SEPARATOR_SIZE = 0.02
|
|
6
|
+
const LEGEND_SEPARATOR_SIZE_MAX = 20
|
|
7
|
+
const LEGEND_SEPARATOR_SIZE_MIN = 8
|
|
8
|
+
|
|
9
|
+
const useLegendSeparators = (separators, legendWidth, allowsLegendSeparators) => {
|
|
10
|
+
const legendSeparators = allowsLegendSeparators
|
|
11
|
+
? separators?.replace(' ', '').split(',').map(Number).filter(Boolean) || []
|
|
12
|
+
: []
|
|
13
|
+
const separatorSize = clamp(legendWidth * LEGEND_SEPARATOR_SIZE, LEGEND_SEPARATOR_SIZE_MIN, LEGEND_SEPARATOR_SIZE_MAX)
|
|
14
|
+
const legendSeparatorsToSubtract = legendSeparators.length * separatorSize
|
|
15
|
+
const getTickSeparatorsAdjustment = (index: number) =>
|
|
16
|
+
legendSeparators.reduce((acc, separators) => (index >= separators ? acc + separatorSize : acc), 0)
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
legendSeparators,
|
|
20
|
+
separatorSize,
|
|
21
|
+
legendSeparatorsToSubtract,
|
|
22
|
+
getTickSeparatorsAdjustment
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default useLegendSeparators
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useEffect, useId, useState } from 'react'
|
|
1
|
+
import { useEffect, useId, useState, type MouseEvent, type ChangeEvent } from 'react'
|
|
2
2
|
import { feature } from 'topojson-client'
|
|
3
3
|
import { Group } from '@visx/group'
|
|
4
4
|
import { MapConfig } from '../types/MapConfig'
|
|
5
|
+
import _ from 'lodash'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* This is the starting structure for adding custom geoJSON shape layers to a projection.
|
|
@@ -16,7 +17,7 @@ import { MapConfig } from '../types/MapConfig'
|
|
|
16
17
|
* 3) Clean (ie. mapshaper -clean) and edit the shape as needed and export the new layer as geoJSON
|
|
17
18
|
* 4) Save the geoJSON somewhere external.
|
|
18
19
|
*/
|
|
19
|
-
export default function useMapLayers(config: MapConfig, setConfig, pathGenerator, tooltipId) {
|
|
20
|
+
export default function useMapLayers(config: MapConfig, setConfig, pathGenerator: Function, tooltipId: string) {
|
|
20
21
|
const [fetchedTopoJSON, setFetchedTopoJSON] = useState([])
|
|
21
22
|
const geoId = useId()
|
|
22
23
|
|
|
@@ -26,65 +27,43 @@ export default function useMapLayers(config: MapConfig, setConfig, pathGenerator
|
|
|
26
27
|
|
|
27
28
|
useEffect(() => {
|
|
28
29
|
fetchGeoJSONLayers()
|
|
29
|
-
}, [])
|
|
30
|
-
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
fetchGeoJSONLayers()
|
|
33
|
-
}, [config.map.layers]) //eslint-disable-line
|
|
30
|
+
}, [config.map.layers])
|
|
34
31
|
|
|
35
32
|
useEffect(() => {
|
|
36
33
|
if (pathGenerator) {
|
|
37
34
|
generateCustomLayers()
|
|
38
35
|
}
|
|
39
|
-
}, [fetchedTopoJSON])
|
|
36
|
+
}, [fetchedTopoJSON])
|
|
40
37
|
|
|
41
38
|
const fetchGeoJSONLayers = async () => {
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
try {
|
|
40
|
+
const geos = await getMapTopoJSONLayers()
|
|
41
|
+
setFetchedTopoJSON(geos)
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error('Error fetching GeoJSON layers:', e) // eslint-disable-line
|
|
44
|
+
}
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
* Removes a custom map layer from the config.
|
|
48
|
-
* @param { Event } e Remove onclick event
|
|
49
|
-
* @param { Integer } index index of layer to remove
|
|
50
|
-
*/
|
|
51
|
-
const handleRemoveLayer = (e, index) => {
|
|
47
|
+
const handleRemoveLayer = (e: MouseEvent<HTMLButtonElement>, index: number) => {
|
|
52
48
|
e.preventDefault()
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
...config.map,
|
|
58
|
-
layers: config.map.layers.filter((layer, i) => i !== index)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
setConfig(updatedState)
|
|
49
|
+
const newConfig = _.cloneDeep(config)
|
|
50
|
+
const layers = newConfig.map.layers.filter((_layer, i) => i !== index)
|
|
51
|
+
newConfig.map.layers = layers
|
|
52
|
+
setConfig(newConfig )
|
|
63
53
|
}
|
|
64
54
|
|
|
65
|
-
|
|
66
|
-
* Adds a new custom map layer to the config
|
|
67
|
-
* @param { Event } e Add onclick event
|
|
68
|
-
*/
|
|
69
|
-
const handleAddLayer = e => {
|
|
55
|
+
const handleAddLayer = (e: Event) => {
|
|
70
56
|
e.preventDefault()
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
...config.map,
|
|
75
|
-
layers: [
|
|
76
|
-
...config.map.layers,
|
|
77
|
-
{
|
|
78
|
-
name: 'New Custom Layer',
|
|
79
|
-
url: ''
|
|
80
|
-
}
|
|
81
|
-
]
|
|
82
|
-
}
|
|
57
|
+
const placeHolderLayer = {
|
|
58
|
+
name: 'New Custom Layer',
|
|
59
|
+
url: ''
|
|
83
60
|
}
|
|
84
|
-
|
|
61
|
+
const newConfig = _.cloneDeep(config)
|
|
62
|
+
newConfig.map.layers.unshift(placeHolderLayer)
|
|
63
|
+
setConfig( newConfig )
|
|
85
64
|
}
|
|
86
65
|
|
|
87
|
-
const handleMapLayer = (e
|
|
66
|
+
const handleMapLayer = (e: ChangeEvent<HTMLInputElement>, index: number, layerKey: string) => {
|
|
88
67
|
e.preventDefault()
|
|
89
68
|
|
|
90
69
|
let layerValue = e.target.value
|
|
@@ -93,17 +72,10 @@ export default function useMapLayers(config: MapConfig, setConfig, pathGenerator
|
|
|
93
72
|
layerValue = layerValue / 100
|
|
94
73
|
}
|
|
95
74
|
|
|
96
|
-
let newLayers =
|
|
97
|
-
|
|
98
|
-
newLayers[index][layerKey] = layerValue
|
|
75
|
+
let newLayers = _.cloneDeep(config.map.layers)
|
|
76
|
+
_.set(newLayers, `[${index}][${layerKey}]`, layerValue)
|
|
99
77
|
|
|
100
|
-
setConfig({
|
|
101
|
-
...config,
|
|
102
|
-
map: {
|
|
103
|
-
...config.map,
|
|
104
|
-
layers: newLayers
|
|
105
|
-
}
|
|
106
|
-
})
|
|
78
|
+
setConfig({ ...config, map: { ...config.map, layers: newLayers } })
|
|
107
79
|
}
|
|
108
80
|
|
|
109
81
|
/**
|
|
@@ -117,7 +89,7 @@ export default function useMapLayers(config: MapConfig, setConfig, pathGenerator
|
|
|
117
89
|
for (const mapLayer of config.map.layers) {
|
|
118
90
|
let newLayerItem = await fetch(mapLayer.url)
|
|
119
91
|
.then(res => res.json())
|
|
120
|
-
.catch(e => console.warn('error with newLayer item'))
|
|
92
|
+
.catch(e => console.warn('error with newLayer item', e)) // eslint-disable-line
|
|
121
93
|
if (!newLayerItem) newLayerItem = []
|
|
122
94
|
TopoJSONObjects.push(newLayerItem)
|
|
123
95
|
}
|
|
@@ -127,10 +99,9 @@ export default function useMapLayers(config: MapConfig, setConfig, pathGenerator
|
|
|
127
99
|
|
|
128
100
|
/**
|
|
129
101
|
* Updates the custom map layers based on the topojson data
|
|
130
|
-
* @returns {void} new map layers to the config
|
|
131
102
|
*/
|
|
132
|
-
const generateCustomLayers = () => {
|
|
133
|
-
if (fetchedTopoJSON.length === 0 || !fetchedTopoJSON) return
|
|
103
|
+
const generateCustomLayers = (): void => {
|
|
104
|
+
if (fetchedTopoJSON.length === 0 || !fetchedTopoJSON) return
|
|
134
105
|
let tempArr = []
|
|
135
106
|
let tempFeatureArray = []
|
|
136
107
|
|
|
@@ -149,7 +120,10 @@ export default function useMapLayers(config: MapConfig, setConfig, pathGenerator
|
|
|
149
120
|
// feature array for county maps
|
|
150
121
|
tempFeatureArray.push(item)
|
|
151
122
|
tempArr.push(
|
|
152
|
-
<Group
|
|
123
|
+
<Group
|
|
124
|
+
className={layerClasses.join(' ')}
|
|
125
|
+
key={`customMapLayer-${item.properties.name.replace(' ', '-')}-${index}`}
|
|
126
|
+
>
|
|
153
127
|
{/* prettier-ignore */}
|
|
154
128
|
<path
|
|
155
129
|
d={pathGenerator(item)}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { MapDispatchContext } from '../context'
|
|
3
|
+
|
|
4
|
+
const useModal = () => {
|
|
5
|
+
const dispatch = useContext(MapDispatchContext)
|
|
6
|
+
|
|
7
|
+
const closeModal = ({ target }, modal: Object) => {
|
|
8
|
+
if (
|
|
9
|
+
'string' === typeof target.className &&
|
|
10
|
+
(target.className.includes('modal-close') || target.className.includes('modal-background')) &&
|
|
11
|
+
null !== modal
|
|
12
|
+
) {
|
|
13
|
+
dispatch({ type: 'SET_MODAL', payload: null })
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
closeModal
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default useModal
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect } from 'react'
|
|
2
2
|
import { type ViewPort } from '@cdc/core/types/ViewPort'
|
|
3
3
|
import { type DimensionsType } from '@cdc/core/types/Dimensions'
|
|
4
|
+
import { EDITOR_WIDTH } from '@cdc/core/helpers/constants'
|
|
4
5
|
import getViewport from '@cdc/core/helpers/getViewport'
|
|
5
6
|
import ResizeObserver from 'resize-observer-polyfill'
|
|
6
7
|
|
|
@@ -12,15 +13,13 @@ export const useResizeObserver = (isEditor: boolean) => {
|
|
|
12
13
|
const resizeObserver = new ResizeObserver(entries => {
|
|
13
14
|
for (let entry of entries) {
|
|
14
15
|
let { width, height } = entry.contentRect
|
|
15
|
-
let newViewport = getViewport(entry.contentRect.width)
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
width = isEditor ? width - EDITOR_WIDTH : width
|
|
18
|
+
|
|
19
|
+
const newViewport = getViewport(width)
|
|
18
20
|
|
|
19
21
|
setCurrentViewport(newViewport)
|
|
20
22
|
|
|
21
|
-
if (isEditor) {
|
|
22
|
-
width = width - editorWidth
|
|
23
|
-
}
|
|
24
23
|
setDimensions([width, height])
|
|
25
24
|
}
|
|
26
25
|
})
|