@cdc/map 4.25.8 → 4.25.10

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.
Files changed (84) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/dist/cdcmap.js +54263 -52600
  3. package/examples/private/c.json +290 -0
  4. package/examples/private/canvas-city-hover.json +787 -0
  5. package/examples/private/d.json +345 -0
  6. package/examples/private/g.json +1 -0
  7. package/examples/private/h.json +105911 -0
  8. package/examples/private/measles-data.json +378 -0
  9. package/examples/private/measles.json +211 -0
  10. package/examples/private/north-dakota.json +1132 -0
  11. package/examples/private/state-with-pattern.json +883 -0
  12. package/index.html +35 -34
  13. package/package.json +26 -5
  14. package/src/CdcMap.tsx +23 -8
  15. package/src/CdcMapComponent.tsx +215 -309
  16. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  17. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  18. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  19. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  20. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  21. package/src/_stories/CdcMap.stories.tsx +15 -5
  22. package/src/_stories/GoogleMap.stories.tsx +2 -2
  23. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  24. package/src/_stories/_mock/equal-number.json +1109 -0
  25. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  26. package/src/components/BubbleList.tsx +16 -12
  27. package/src/components/CityList.tsx +85 -107
  28. package/src/components/DataTable.tsx +37 -9
  29. package/src/components/EditorPanel/components/EditorPanel.tsx +177 -165
  30. package/src/components/EditorPanel/components/HexShapeSettings.tsx +3 -2
  31. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +7 -5
  32. package/src/components/Geo.tsx +2 -0
  33. package/src/components/Legend/components/Legend.tsx +109 -73
  34. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  35. package/src/components/MapContainer.tsx +52 -0
  36. package/src/components/MapControls.tsx +44 -0
  37. package/src/components/NavigationMenu.tsx +11 -2
  38. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +24 -7
  39. package/src/components/UsaMap/components/UsaMap.County.tsx +111 -37
  40. package/src/components/UsaMap/components/UsaMap.Region.tsx +23 -5
  41. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +6 -6
  42. package/src/components/UsaMap/components/UsaMap.State.tsx +28 -10
  43. package/src/components/UsaMap/helpers/map.ts +2 -2
  44. package/src/components/WorldMap/WorldMap.tsx +113 -25
  45. package/src/components/ZoomControls.tsx +6 -9
  46. package/src/context/LegendMemoContext.tsx +30 -0
  47. package/src/context.ts +1 -40
  48. package/src/data/initial-state.js +143 -130
  49. package/src/data/supported-geos.js +17 -2
  50. package/src/helpers/applyColorToLegend.ts +116 -20
  51. package/src/helpers/applyLegendToRow.ts +10 -6
  52. package/src/helpers/componentHelpers.ts +8 -0
  53. package/src/helpers/constants.ts +12 -0
  54. package/src/helpers/dataTableHelpers.ts +6 -0
  55. package/src/helpers/displayGeoName.ts +1 -1
  56. package/src/helpers/generateRuntimeLegend.ts +44 -8
  57. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  58. package/src/helpers/getColumnNames.ts +1 -1
  59. package/src/helpers/getPatternForRow.ts +36 -0
  60. package/src/helpers/getStatesPicked.ts +8 -5
  61. package/src/helpers/index.ts +11 -3
  62. package/src/helpers/isLegendItemDisabled.ts +16 -0
  63. package/src/helpers/mapObserverHelpers.ts +40 -0
  64. package/src/helpers/resetLegendToggles.ts +3 -2
  65. package/src/helpers/toggleLegendActive.ts +6 -11
  66. package/src/helpers/urlDataHelpers.ts +70 -0
  67. package/src/hooks/useGeoClickHandler.ts +35 -1
  68. package/src/hooks/useLegendMemo.ts +17 -0
  69. package/src/hooks/useMapLayers.tsx +5 -4
  70. package/src/hooks/useStateZoom.tsx +25 -6
  71. package/src/hooks/useTooltip.ts +1 -2
  72. package/src/index.jsx +0 -2
  73. package/src/store/map.reducer.ts +17 -6
  74. package/src/test/CdcMap.test.jsx +11 -0
  75. package/src/types/MapConfig.ts +23 -14
  76. package/src/types/MapContext.ts +0 -7
  77. package/src/types/runtimeLegend.ts +17 -1
  78. package/vite.config.js +2 -7
  79. package/vitest.config.ts +16 -0
  80. package/src/coreStyles_map.scss +0 -3
  81. package/src/helpers/colorDistributions.ts +0 -12
  82. package/src/helpers/generateColorsArray.ts +0 -14
  83. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  84. package/src/helpers/tests/generateRuntimeLegendHash.test.ts +0 -11
@@ -1,4 +1,6 @@
1
- import React, { useContext, useEffect, useState } from 'react'
1
+ import React, { useContext, useEffect, useState, useMemo } from 'react'
2
+ import { filterColorPalettes } from '@cdc/core/helpers/filterColorPalettes'
3
+ import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
2
4
 
3
5
  // Third Party
4
6
  import {
@@ -12,11 +14,12 @@ import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
12
14
  import { useDebounce } from 'use-debounce'
13
15
  import _ from 'lodash'
14
16
  import { Tooltip as ReactTooltip } from 'react-tooltip'
17
+ import 'react-tooltip/dist/react-tooltip.css'
15
18
  import Panels from './Panels'
16
19
  import Layout from '@cdc/core/components/Layout'
17
20
 
18
21
  // Data
19
- import colorPalettes from '@cdc/core/data/colorPalettes'
22
+ import { mapColorPalettes as colorPalettes } from '@cdc/core/data/colorPalettes'
20
23
  import { supportedStatesFipsCodes } from '../../../data/supported-geos.js'
21
24
 
22
25
  // Components - Core
@@ -26,6 +29,7 @@ import Icon from '@cdc/core/components/ui/Icon'
26
29
  import InputToggle from '@cdc/core/components/inputs/InputToggle'
27
30
  import Tooltip from '@cdc/core/components/ui/Tooltip'
28
31
  import VizFilterEditor from '@cdc/core/components/EditorPanel/VizFilterEditor'
32
+ import PanelMarkup from '@cdc/core/components/EditorPanel/components/PanelMarkup'
29
33
 
30
34
  // Assets
31
35
  import UsaGraphic from '@cdc/core/assets/icon-map-usa.svg'
@@ -49,6 +53,11 @@ import './editorPanel.styles.css'
49
53
  import FootnotesEditor from '@cdc/core/components/EditorPanel/FootnotesEditor'
50
54
  import { Datasets } from '@cdc/core/types/DataSet'
51
55
  import MultiSelect from '@cdc/core/components/MultiSelect'
56
+ import { paletteMigrationMap } from '@cdc/core/helpers/palettes/migratePaletteName'
57
+ import { isV1Palette, getCurrentPaletteName, migratePaletteWithMap } from '@cdc/core/helpers/palettes/utils'
58
+ import { USE_V2_MIGRATION } from '@cdc/core/helpers/constants'
59
+ import { PaletteSelector, DeveloperPaletteRollback } from '@cdc/core/components/PaletteSelector'
60
+ import PaletteConversionModal from '@cdc/core/components/PaletteConversionModal'
52
61
 
53
62
  type MapEditorPanelProps = {
54
63
  datasets?: Datasets
@@ -65,8 +74,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
65
74
  setConfig,
66
75
  config,
67
76
  tooltipId,
68
- runtimeData,
69
- setRuntimeData
77
+ runtimeData
70
78
  } = useContext<MapContext>(ConfigContext)
71
79
 
72
80
  const { columnsRequiredChecker } = useColumnsRequiredChecker()
@@ -77,6 +85,11 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
77
85
  const [loadedDefault, setLoadedDefault] = useState(false)
78
86
  const [displayPanel, setDisplayPanel] = useState(true)
79
87
  const [activeFilterValueForDescription, setActiveFilterValueForDescription] = useState([0, 0])
88
+ const [showConversionModal, setShowConversionModal] = useState(false)
89
+ const [pendingPaletteSelection, setPendingPaletteSelection] = useState<{
90
+ palette: string
91
+ action: () => void
92
+ } | null>(null)
80
93
 
81
94
  const {
82
95
  MapLayerHandlers: { handleMapLayer, handleAddLayer, handleRemoveLayer }
@@ -691,7 +704,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
691
704
 
692
705
  if (config) {
693
706
  const newData = generateRuntimeData(config)
694
- setRuntimeData(newData)
707
+ dispatch({ type: 'SET_RUNTIME_DATA', payload: newData })
695
708
  }
696
709
  break
697
710
  case 'classificationType':
@@ -868,7 +881,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
868
881
  }
869
882
 
870
883
  const convertStateToConfig = () => {
871
- let strippedState = _.cloneDeep(config) // Deep copy
884
+ let strippedState = cloneConfig(config) // Deep copy
872
885
 
873
886
  // Strip ref
874
887
  delete strippedState['']
@@ -900,46 +913,75 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
900
913
 
901
914
  const isReversed = config.general.palette.isReversed
902
915
 
903
- function filterColorPalettes() {
904
- let sequential = []
905
- let nonSequential = []
906
- let accessibleColors = []
907
- for (let paletteName in colorPalettes) {
908
- if (!isReversed) {
909
- if (paletteName.includes('qualitative') && !paletteName.endsWith('reverse')) {
910
- nonSequential.push(paletteName)
911
- }
912
- if (paletteName.includes('colorblindsafe') && !paletteName.endsWith('reverse')) {
913
- accessibleColors.push(paletteName)
914
- }
915
- if (
916
- !paletteName.includes('qualitative') &&
917
- !paletteName.includes('colorblindsafe') &&
918
- !paletteName.endsWith('reverse')
919
- ) {
920
- sequential.push(paletteName)
921
- }
922
- }
923
- if (isReversed) {
924
- if (paletteName.includes('qualitative') && paletteName.endsWith('reverse')) {
925
- nonSequential.push(paletteName)
926
- }
927
- if (paletteName.includes('colorblindsafe') && paletteName.endsWith('reverse')) {
928
- accessibleColors.push(paletteName)
929
- }
930
- if (
931
- !paletteName.includes('qualitative') &&
932
- !paletteName.includes('colorblindsafe') &&
933
- paletteName.endsWith('reverse')
934
- ) {
935
- sequential.push(paletteName)
916
+ const { sequential, nonSequential, accessibleColors } = useMemo(
917
+ () => filterColorPalettes({ config, isReversed, colorPalettes }),
918
+ [isReversed, colorPalettes, config.general.palette.version]
919
+ )
920
+
921
+ // Helper function to handle palette selection with conversion prompt
922
+ const handlePaletteSelection = (palette: string) => {
923
+ const isV1PaletteConfig = isV1Palette(config)
924
+
925
+ const executeSelection = () => {
926
+ const _newConfig = _.cloneDeep(config)
927
+
928
+ // If v2 migration is disabled, use the original palette name and keep v1 version
929
+ if (!USE_V2_MIGRATION) {
930
+ _newConfig.general.palette.name = palette
931
+ _newConfig.general.palette.version = '1.0'
932
+ } else {
933
+ // V2 migration logic
934
+ _newConfig.general.palette.name = palette
935
+ ? migratePaletteWithMap(palette, paletteMigrationMap, false)
936
+ : undefined
937
+ if (isV1PaletteConfig) {
938
+ _newConfig.general.palette.version = '2.0'
936
939
  }
937
940
  }
941
+ setConfig(_newConfig)
938
942
  }
939
943
 
940
- return [sequential, nonSequential, accessibleColors]
944
+ if (isV1PaletteConfig) {
945
+ setPendingPaletteSelection({ palette, action: executeSelection })
946
+ setShowConversionModal(true)
947
+ } else {
948
+ executeSelection()
949
+ }
950
+ }
951
+
952
+ // Modal handlers
953
+ const handleConversionConfirm = () => {
954
+ if (pendingPaletteSelection) {
955
+ pendingPaletteSelection.action()
956
+ }
957
+ setShowConversionModal(false)
958
+ setPendingPaletteSelection(null)
959
+ }
960
+
961
+ const handleConversionCancel = () => {
962
+ setShowConversionModal(false)
963
+ setPendingPaletteSelection(null)
964
+ }
965
+
966
+ const handleReturnToV1 = () => {
967
+ if (pendingPaletteSelection) {
968
+ const _newConfig = cloneConfig(config)
969
+ _newConfig.general.palette.name = pendingPaletteSelection.palette
970
+ _newConfig.general.palette.version = '1.0'
971
+ setConfig(_newConfig)
972
+ }
973
+ setShowConversionModal(false)
974
+ setPendingPaletteSelection(null)
975
+ }
976
+
977
+ // Helper function to determine if a palette should be marked as selected
978
+ const getPaletteClassName = (palette: string) => {
979
+ const currentPaletteName = config.general.palette.name || ''
980
+
981
+ // Direct comparison since the UI filters palettes by version
982
+ // When v1 is selected, UI shows v1 palettes; when v2 is selected, UI shows v2 palettes
983
+ return currentPaletteName === palette ? 'selected' : ''
941
984
  }
942
- const [sequential, nonSequential, accessibleColors] = filterColorPalettes()
943
985
 
944
986
  useEffect(() => {
945
987
  setLoadedDefault(config.defaultData)
@@ -1263,7 +1305,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1263
1305
  { value: '_blank', label: 'New Window' }
1264
1306
  ]}
1265
1307
  onChange={event => {
1266
- const _newConfig = _.cloneDeep(config)
1308
+ const _newConfig = cloneConfig(config)
1267
1309
  _newConfig.general.navigationTarget = event.target.value
1268
1310
  setConfig(_newConfig)
1269
1311
  }}
@@ -1302,7 +1344,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1302
1344
  type='checkbox'
1303
1345
  checked={config.general.displayAsHex}
1304
1346
  onChange={event => {
1305
- const _newConfig = _.cloneDeep(config)
1347
+ const _newConfig = cloneConfig(config)
1306
1348
  _newConfig.general.displayAsHex = event.target.checked
1307
1349
  setConfig(_newConfig)
1308
1350
  }}
@@ -1358,7 +1400,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1358
1400
  type='checkbox'
1359
1401
  checked={general.territoriesAlwaysShow || false}
1360
1402
  onChange={event => {
1361
- const _newConfig = _.cloneDeep(config)
1403
+ const _newConfig = cloneConfig(config)
1362
1404
  _newConfig.general.territoriesAlwaysShow = event.target.checked
1363
1405
  setConfig(_newConfig)
1364
1406
  }}
@@ -1402,7 +1444,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1402
1444
  type='checkbox'
1403
1445
  checked={config.general.showTitle || false}
1404
1446
  onChange={event => {
1405
- const _newConfig = _.cloneDeep(config)
1447
+ const _newConfig = cloneConfig(config)
1406
1448
  _newConfig.general.showTitle = event.target.checked
1407
1449
  setConfig(_newConfig)
1408
1450
  }}
@@ -1546,7 +1588,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1546
1588
  type='checkbox'
1547
1589
  checked={config.general.hideGeoColumnInTooltip || false}
1548
1590
  onChange={event => {
1549
- const _newConfig = _.cloneDeep(config)
1591
+ const _newConfig = cloneConfig(config)
1550
1592
  _newConfig.general.hideGeoColumnInTooltip = event.target.checked
1551
1593
  setConfig(_newConfig)
1552
1594
  }}
@@ -1579,7 +1621,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1579
1621
  value={columns.primary.name}
1580
1622
  options={columnsOptions.map(c => c.key)}
1581
1623
  onChange={event => {
1582
- const _state = _.cloneDeep(config)
1624
+ const _state = cloneConfig(config)
1583
1625
  _state.columns.primary.name = event.target.value
1584
1626
  _state.columns.primary.label = event.target.value
1585
1627
  setConfig(_state)
@@ -2083,7 +2125,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2083
2125
  messages = []
2084
2126
  }
2085
2127
 
2086
- const _newConfig = _.cloneDeep(config)
2128
+ const _newConfig = cloneConfig(config)
2087
2129
  _newConfig.legend.type = event.target.value
2088
2130
  _newConfig.runtime.editorErrorMessage = messages
2089
2131
  setConfig(_newConfig)
@@ -2096,7 +2138,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2096
2138
  type='checkbox'
2097
2139
  checked={config.general.showSidebar || false}
2098
2140
  onChange={event => {
2099
- const _newConfig = _.cloneDeep(config)
2141
+ const _newConfig = cloneConfig(config)
2100
2142
  _newConfig.general.showSidebar = event.target.checked
2101
2143
  setConfig(_newConfig)
2102
2144
  }}
@@ -2236,7 +2278,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2236
2278
  type='checkbox'
2237
2279
  checked={legend.singleColumn}
2238
2280
  onChange={event => {
2239
- const _newConfig = _.cloneDeep(config)
2281
+ const _newConfig = cloneConfig(config)
2240
2282
  _newConfig.legend.singleColumn = event.target.checked
2241
2283
  _newConfig.legend.singleRow = false
2242
2284
  _newConfig.legend.verticalSorted = false
@@ -2253,7 +2295,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2253
2295
  type='checkbox'
2254
2296
  checked={legend.singleRow}
2255
2297
  onChange={event => {
2256
- const _newConfig = _.cloneDeep(config)
2298
+ const _newConfig = cloneConfig(config)
2257
2299
  _newConfig.legend.singleRow = event.target.checked
2258
2300
  _newConfig.legend.singleColumn = false
2259
2301
  _newConfig.legend.verticalSorted = false
@@ -2271,7 +2313,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2271
2313
  value={legend.groupBy || ''}
2272
2314
  options={columnsOptions.map(c => c.key)}
2273
2315
  onChange={event => {
2274
- const _newConfig = _.cloneDeep(config)
2316
+ const _newConfig = cloneConfig(config)
2275
2317
  _newConfig.legend.groupBy = event.target.value
2276
2318
  setConfig(_newConfig)
2277
2319
  }}
@@ -2283,7 +2325,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2283
2325
  type='checkbox'
2284
2326
  checked={legend.verticalSorted}
2285
2327
  onChange={event => {
2286
- const _newConfig = _.cloneDeep(config)
2328
+ const _newConfig = cloneConfig(config)
2287
2329
  _newConfig.legend.verticalSorted = event.target.checked
2288
2330
  setConfig(_newConfig)
2289
2331
  }}
@@ -2311,7 +2353,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2311
2353
  type='checkbox'
2312
2354
  checked={legend.separateZero || false}
2313
2355
  onChange={event => {
2314
- const _newConfig = _.cloneDeep(config)
2356
+ const _newConfig = cloneConfig(config)
2315
2357
  _newConfig.legend.separateZero = event.target.checked
2316
2358
  return setConfig(_newConfig)
2317
2359
  }}
@@ -2771,7 +2813,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2771
2813
  type='checkbox'
2772
2814
  checked={config.table.showDataTableLink}
2773
2815
  onChange={event => {
2774
- const _newConfig = _.cloneDeep(config)
2816
+ const _newConfig = cloneConfig(config)
2775
2817
  _newConfig.table.showDataTableLink = event.target.checked
2776
2818
  setConfig(_newConfig)
2777
2819
  }}
@@ -2785,7 +2827,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2785
2827
  type='checkbox'
2786
2828
  checked={config.table.showDownloadUrl}
2787
2829
  onChange={event => {
2788
- const _newConfig = _.cloneDeep(config)
2830
+ const _newConfig = cloneConfig(config)
2789
2831
  _newConfig.table.showDownloadUrl = event.target.checked
2790
2832
  setConfig(_newConfig)
2791
2833
  }}
@@ -2911,7 +2953,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2911
2953
  type='checkbox'
2912
2954
  checked={config.general.fullBorder || false}
2913
2955
  onChange={event => {
2914
- const _newConfig = _.cloneDeep(config)
2956
+ const _newConfig = cloneConfig(config)
2915
2957
  _newConfig.general.fullBorder = event.target.checked
2916
2958
  setConfig(_newConfig)
2917
2959
  }}
@@ -2933,6 +2975,15 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2933
2975
  <label>
2934
2976
  <span className='edit-label'>Map Color Palette</span>
2935
2977
  </label>
2978
+ <div className='mb-2'>
2979
+ <small className='text-muted'>
2980
+ Review color contrasts{' '}
2981
+ <a href='https://webaim.org/resources/contrastchecker/' target='_blank' rel='noopener noreferrer'>
2982
+ here
2983
+ </a>
2984
+ </small>
2985
+ </div>
2986
+ <DeveloperPaletteRollback config={config} updateConfig={setConfig} />
2936
2987
  <InputToggle
2937
2988
  type='3d'
2938
2989
  section='general'
@@ -2941,127 +2992,71 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2941
2992
  size='small'
2942
2993
  label='Use selected palette in reverse order'
2943
2994
  onClick={() => {
2944
- const _state = _.cloneDeep(config)
2995
+ const _state = cloneConfig(config)
2996
+ const currentPaletteName = config.general.palette?.name || ''
2945
2997
  _state.general.palette.isReversed = !_state.general.palette.isReversed
2946
2998
  let paletteName = ''
2947
- if (_state.general.palette.isReversed && !config.color.endsWith('reverse')) {
2948
- paletteName = config.color + 'reverse'
2999
+ if (_state.general.palette.isReversed && !currentPaletteName.endsWith('reverse')) {
3000
+ paletteName = currentPaletteName + 'reverse'
2949
3001
  }
2950
- if (!_state.general.palette.isReversed && config.color.endsWith('reverse')) {
2951
- paletteName = config.color.slice(0, -7)
3002
+ if (!_state.general.palette.isReversed && currentPaletteName.endsWith('reverse')) {
3003
+ paletteName = currentPaletteName.slice(0, -7)
2952
3004
  }
2953
3005
  if (paletteName) {
2954
- _state.color = paletteName
3006
+ _state.general.palette.name = paletteName
2955
3007
  }
2956
3008
  setConfig(_state)
2957
3009
  }}
2958
3010
  value={config.general.palette.isReversed}
2959
3011
  />
2960
3012
  <span>Sequential</span>
2961
- <ul className='color-palette'>
2962
- {sequential.map(palette => {
2963
- const colorOne = {
2964
- backgroundColor: colorPalettes[palette][2]
2965
- }
2966
-
2967
- const colorTwo = {
2968
- backgroundColor: colorPalettes[palette][4]
2969
- }
2970
-
2971
- const colorThree = {
2972
- backgroundColor: colorPalettes[palette][6]
2973
- }
2974
-
2975
- return (
2976
- <li
2977
- title={palette}
2978
- key={palette}
2979
- onClick={() => {
2980
- const _newConfig = _.cloneDeep(config)
2981
- _newConfig.color = palette
2982
- setConfig(_newConfig)
2983
- }}
2984
- className={config.color === palette ? 'selected' : ''}
2985
- >
2986
- <span style={colorOne}></span>
2987
- <span style={colorTwo}></span>
2988
- <span style={colorThree}></span>
2989
- </li>
2990
- )
2991
- })}
2992
- </ul>
3013
+ <PaletteSelector
3014
+ palettes={sequential}
3015
+ colorPalettes={colorPalettes}
3016
+ config={config}
3017
+ onPaletteSelect={handlePaletteSelection}
3018
+ selectedPalette={getCurrentPaletteName(config)}
3019
+ colorIndices={[2, 3, 5]}
3020
+ className='color-palette'
3021
+ element='li'
3022
+ getItemClassName={getPaletteClassName}
3023
+ />
2993
3024
  <span>Non-Sequential</span>
2994
- <ul className='color-palette'>
2995
- {nonSequential.map(palette => {
2996
- const colorOne = {
2997
- backgroundColor: colorPalettes[palette][2]
2998
- }
2999
-
3000
- const colorTwo = {
3001
- backgroundColor: colorPalettes[palette][4]
3025
+ <PaletteSelector
3026
+ palettes={nonSequential}
3027
+ colorPalettes={colorPalettes}
3028
+ config={config}
3029
+ onPaletteSelect={handlePaletteSelection}
3030
+ selectedPalette={getCurrentPaletteName(config)}
3031
+ colorIndices={[2, 3, 5]}
3032
+ className='color-palette'
3033
+ element='li'
3034
+ getItemClassName={getPaletteClassName}
3035
+ minColorsForFilter={(_, paletteAccessor, config) => {
3036
+ if (paletteAccessor.length <= 8 && config.general.geoType === 'us-region') {
3037
+ return false
3002
3038
  }
3003
-
3004
- const colorThree = {
3005
- backgroundColor: colorPalettes[palette][6]
3006
- }
3007
-
3008
- // hide palettes with too few colors for region maps
3009
- if (colorPalettes[palette].length <= 8 && config.general.geoType === 'us-region') {
3010
- return ''
3011
- }
3012
- return (
3013
- <li
3014
- title={palette}
3015
- key={palette}
3016
- onClick={() => {
3017
- const _newConfig = _.cloneDeep(config)
3018
- _newConfig.color = palette
3019
- setConfig(_newConfig)
3020
- }}
3021
- className={config.color === palette ? 'selected' : ''}
3022
- >
3023
- <span style={colorOne}></span>
3024
- <span style={colorTwo}></span>
3025
- <span style={colorThree}></span>
3026
- </li>
3027
- )
3028
- })}
3029
- </ul>
3039
+ return true
3040
+ }}
3041
+ />
3030
3042
  <span>Colorblind Safe</span>
3031
- <ul className='color-palette'>
3032
- {accessibleColors.map(palette => {
3033
- const colorOne = {
3034
- backgroundColor: colorPalettes[palette][2]
3035
- }
3036
-
3037
- const colorTwo = {
3038
- backgroundColor: colorPalettes[palette][4]
3039
- }
3040
-
3041
- const colorThree = {
3042
- backgroundColor: colorPalettes[palette][6]
3043
- }
3044
-
3045
- // hide palettes with too few colors for region maps
3046
- if (colorPalettes[palette].length <= 8 && config.general.geoType === 'us-region') {
3047
- return ''
3043
+ <PaletteSelector
3044
+ palettes={accessibleColors}
3045
+ colorPalettes={colorPalettes}
3046
+ config={config}
3047
+ onPaletteSelect={handlePaletteSelection}
3048
+ selectedPalette={getCurrentPaletteName(config)}
3049
+ colorIndices={[2, 3, 5]}
3050
+ className='color-palette'
3051
+ element='li'
3052
+ getItemClassName={getPaletteClassName}
3053
+ minColorsForFilter={(_, paletteAccessor, config) => {
3054
+ if (paletteAccessor.length <= 8 && config.general.geoType === 'us-region') {
3055
+ return false
3048
3056
  }
3049
- return (
3050
- <li
3051
- title={palette}
3052
- key={palette}
3053
- onClick={() => {
3054
- handleEditorChanges('color', palette)
3055
- }}
3056
- className={config.color === palette ? 'selected' : ''}
3057
- >
3058
- <span style={colorOne}></span>
3059
- <span style={colorTwo}></span>
3060
- <span style={colorThree}></span>
3061
- </li>
3062
- )
3063
- })}
3064
- </ul>
3057
+ return true
3058
+ }}
3059
+ />
3065
3060
  <label>
3066
3061
  Geocode Settings
3067
3062
  <TextField
@@ -3116,7 +3111,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3116
3111
  type='checkbox'
3117
3112
  checked={config.general.allowMapZoom}
3118
3113
  onChange={event => {
3119
- const _newConfig = _.cloneDeep(config)
3114
+ const _newConfig = cloneConfig(config)
3120
3115
  _newConfig.general.allowMapZoom = event.target.checked
3121
3116
  _newConfig.mapPosition.coordinates = config.general.geoType === 'world' ? [0, 30] : [0, 0]
3122
3117
  _newConfig.mapPosition.zoom = 1
@@ -3132,7 +3127,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3132
3127
  type='checkbox'
3133
3128
  checked={config.visual.extraBubbleBorder}
3134
3129
  onChange={event => {
3135
- const _newConfig = _.cloneDeep(config)
3130
+ const _newConfig = cloneConfig(config)
3136
3131
  _newConfig.visual.extraBubbleBorder = event.target.checked
3137
3132
  setConfig(_newConfig)
3138
3133
  }}
@@ -3371,9 +3366,26 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3371
3366
  </AccordionItem>
3372
3367
  {config.general.geoType === 'us' && <Panels.PatternSettings name='Pattern Settings' />}
3373
3368
  {config.general.geoType !== 'us-county' && <Panels.Annotate name='Text Annotations' />}
3369
+ <PanelMarkup
3370
+ name='Markup Variables'
3371
+ markupVariables={config.markupVariables || []}
3372
+ data={config.data || []}
3373
+ enableMarkupVariables={config.enableMarkupVariables || false}
3374
+ onMarkupVariablesChange={variables => setConfig({ ...config, markupVariables: variables })}
3375
+ onToggleEnable={enabled => setConfig({ ...config, enableMarkupVariables: enabled })}
3376
+ />
3374
3377
  </Accordion>
3375
3378
  <AdvancedEditor loadConfig={setConfig} config={config} convertStateToConfig={convertStateToConfig} />
3376
3379
  </Layout.Sidebar>
3380
+
3381
+ {showConversionModal && (
3382
+ <PaletteConversionModal
3383
+ onConfirm={handleConversionConfirm}
3384
+ onCancel={handleConversionCancel}
3385
+ onReturnToV1={handleReturnToV1}
3386
+ paletteName={pendingPaletteSelection?.palette}
3387
+ />
3388
+ )}
3377
3389
  </ErrorBoundary>
3378
3390
  )
3379
3391
  }
@@ -8,6 +8,7 @@ import {
8
8
  } from 'react-accessible-accordion'
9
9
  import ConfigContext from '../../../context'
10
10
  import _ from 'lodash'
11
+ import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
11
12
 
12
13
  const shapeOptions = ['Arrow Up', 'Arrow Down', 'Arrow Right', 'Arrow Left', 'None']
13
14
 
@@ -105,7 +106,7 @@ const HexSettingShapeColumns = props => {
105
106
  type='text'
106
107
  value={shapeGroup.legendTitle || ''}
107
108
  onChange={e => {
108
- const newConfig = _.cloneDeep(config)
109
+ const newConfig = cloneConfig(config)
109
110
  newConfig.hexMap.shapeGroups[shapeGroupIndex].legendTitle = e.target.value
110
111
  setConfig(newConfig)
111
112
  }}
@@ -243,7 +244,7 @@ const HexSettingShapeColumns = props => {
243
244
  className='cove-button'
244
245
  style={{ marginTop: '15px' }}
245
246
  onClick={() => {
246
- const newConfig = _.cloneDeep(config)
247
+ const newConfig = cloneConfig(config)
247
248
  _.set(
248
249
  newConfig,
249
250
  'hexMap.shapeGroups',
@@ -7,6 +7,7 @@ import {
7
7
  AccordionItemButton
8
8
  } from 'react-accessible-accordion'
9
9
  import ConfigContext from '../../../../context'
10
+ import { useLegendMemoContext } from '../../../../context/LegendMemoContext'
10
11
  import { type MapContext } from '../../../../types/MapContext'
11
12
  import Button from '@cdc/core/components/elements/Button'
12
13
  import Tooltip from '@cdc/core/components/ui/Tooltip'
@@ -14,6 +15,7 @@ import Icon from '@cdc/core/components/ui/Icon'
14
15
  import './Panel.PatternSettings-style.css'
15
16
  import Alert from '@cdc/core/components/Alert'
16
17
  import _ from 'lodash'
18
+ import { cloneConfig } from '@cdc/core/helpers/cloneConfig'
17
19
 
18
20
  // topojson helpers for checking color contrasts
19
21
  import { feature } from 'topojson-client'
@@ -26,8 +28,8 @@ type PanelProps = {
26
28
  }
27
29
 
28
30
  const PatternSettings = ({ name }: PanelProps) => {
29
- const { config, setConfig, runtimeData, legendMemo, legendSpecialClassLastMemo, runtimeLegend } =
30
- useContext<MapContext>(ConfigContext)
31
+ const { config, setConfig, runtimeData, runtimeLegend } = useContext<MapContext>(ConfigContext)
32
+ const { legendMemo, legendSpecialClassLastMemo } = useLegendMemoContext()
31
33
  const defaultPattern = 'circles'
32
34
  const patternTypes = ['circles', 'waves', 'lines']
33
35
 
@@ -114,14 +116,14 @@ const PatternSettings = ({ name }: PanelProps) => {
114
116
  }
115
117
 
116
118
  const handlePatternFieldUpdate = (field: string, color: string, patternIndex: number) => {
117
- const _newConfig = _.cloneDeep(config)
119
+ const _newConfig = cloneConfig(config)
118
120
  _newConfig.map.patterns[patternIndex][field] = color
119
121
  reviewColorContrast(_newConfig, patternIndex)
120
122
  setConfig(_newConfig)
121
123
  }
122
124
 
123
125
  const handleRemovePattern = index => {
124
- const _newConfig = _.cloneDeep(config)
126
+ const _newConfig = cloneConfig(config)
125
127
  const updatedPatterns = config.map.patterns.filter((pattern, i) => i !== index)
126
128
  _newConfig.map.patterns = updatedPatterns
127
129
  if (checkPatternContrasts()) {
@@ -161,7 +163,7 @@ const PatternSettings = ({ name }: PanelProps) => {
161
163
  {patterns.length > 0 && (
162
164
  <Alert
163
165
  type={checkPatternContrasts() ? 'success' : 'danger'}
164
- message='Pattern colors must comply with <br /> <a href="https://www.w3.org/TR/WCAG21/">WCAG 2.1</a> 3:1 contrast ratio.'
166
+ message='Pattern colors must comply with <a href="https://www.w3.org/TR/WCAG21/">WCAG 2.1</a> 3:1 contrast ratio.'
165
167
  />
166
168
  )}
167
169
  <br />
@@ -6,6 +6,8 @@ type GeoProps = {
6
6
  strokeWidth?: number
7
7
  path?: string
8
8
  className?: string
9
+ onMouseEnter?: () => void
10
+ onClick?: () => void
9
11
  }
10
12
 
11
13
  const Geo: React.FC<GeoProps> = ({ path, styles, stroke, strokeWidth, ...props }) => {