@cdc/map 4.25.7 → 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 (99) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/CLAUDE.local.md +0 -0
  3. package/dist/cdcmap.js +54785 -53159
  4. package/examples/private/c.json +290 -0
  5. package/examples/private/canvas-city-hover.json +787 -0
  6. package/examples/private/d.json +345 -0
  7. package/examples/private/filter-map.json +909 -0
  8. package/examples/private/g.json +1 -0
  9. package/examples/private/h.json +105911 -0
  10. package/examples/private/measles-data.json +378 -0
  11. package/examples/private/measles.json +211 -0
  12. package/examples/private/north-dakota.json +1132 -0
  13. package/examples/private/rsv-data.json +532 -0
  14. package/examples/private/state-with-pattern.json +883 -0
  15. package/examples/private/test.json +222 -640
  16. package/index.html +1 -1
  17. package/package.json +26 -5
  18. package/src/CdcMap.tsx +28 -8
  19. package/src/CdcMapComponent.tsx +230 -306
  20. package/src/_stories/CdcMap.Filters.stories.tsx +2 -2
  21. package/src/_stories/CdcMap.Legend.Gradient.stories.tsx +3 -3
  22. package/src/_stories/CdcMap.Legend.stories.tsx +7 -4
  23. package/src/_stories/CdcMap.Patterns.stories.tsx +2 -2
  24. package/src/_stories/CdcMap.Table.stories.tsx +2 -2
  25. package/src/_stories/CdcMap.stories.tsx +18 -11
  26. package/src/_stories/GoogleMap.stories.tsx +2 -2
  27. package/src/_stories/UsaMap.NoData.stories.tsx +2 -2
  28. package/src/_stories/_mock/equal-number.json +1109 -0
  29. package/src/_stories/_mock/multi-state.json +21389 -0
  30. package/src/_stories/_mock/us-bubble-cities.json +306 -0
  31. package/src/components/BubbleList.tsx +16 -12
  32. package/src/components/CityList.tsx +88 -110
  33. package/src/components/DataTable.tsx +44 -12
  34. package/src/components/EditorPanel/components/EditorPanel.tsx +201 -203
  35. package/src/components/EditorPanel/components/HexShapeSettings.tsx +3 -2
  36. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +7 -5
  37. package/src/components/Geo.tsx +2 -0
  38. package/src/components/Legend/components/Legend.tsx +117 -93
  39. package/src/components/Legend/components/LegendGroup/Legend.Group.tsx +10 -7
  40. package/src/components/MapContainer.tsx +52 -0
  41. package/src/components/MapControls.tsx +44 -0
  42. package/src/components/Modal.tsx +2 -8
  43. package/src/components/NavigationMenu.tsx +13 -1
  44. package/src/components/UsaMap/components/SingleState/SingleState.CountyOutput.tsx +24 -7
  45. package/src/components/UsaMap/components/SingleState/SingleState.StateOutput.tsx +21 -15
  46. package/src/components/UsaMap/components/TerritoriesSection.tsx +2 -2
  47. package/src/components/UsaMap/components/UsaMap.County.tsx +112 -33
  48. package/src/components/UsaMap/components/UsaMap.Region.tsx +23 -5
  49. package/src/components/UsaMap/components/UsaMap.SingleState.tsx +38 -26
  50. package/src/components/UsaMap/components/UsaMap.State.tsx +28 -10
  51. package/src/components/UsaMap/helpers/map.ts +16 -8
  52. package/src/components/WorldMap/WorldMap.tsx +116 -11
  53. package/src/components/ZoomControls.tsx +6 -9
  54. package/src/context/LegendMemoContext.tsx +30 -0
  55. package/src/context.ts +1 -39
  56. package/src/data/initial-state.js +143 -128
  57. package/src/data/supported-geos.js +202 -4
  58. package/src/helpers/addUIDs.ts +8 -8
  59. package/src/helpers/applyColorToLegend.ts +122 -45
  60. package/src/helpers/applyLegendToRow.ts +15 -13
  61. package/src/helpers/componentHelpers.ts +8 -0
  62. package/src/helpers/constants.ts +12 -0
  63. package/src/helpers/dataTableHelpers.ts +6 -0
  64. package/src/helpers/displayGeoName.ts +12 -7
  65. package/src/helpers/formatLegendLocation.ts +1 -3
  66. package/src/helpers/generateRuntimeLegend.ts +192 -340
  67. package/src/helpers/generateRuntimeLegendHash.ts +4 -2
  68. package/src/helpers/getColumnNames.ts +1 -1
  69. package/src/helpers/getPatternForRow.ts +36 -0
  70. package/src/helpers/getStatesPicked.ts +14 -0
  71. package/src/helpers/handleMapAriaLabels.ts +2 -2
  72. package/src/helpers/index.ts +11 -3
  73. package/src/helpers/isLegendItemDisabled.ts +16 -0
  74. package/src/helpers/mapObserverHelpers.ts +40 -0
  75. package/src/helpers/resetLegendToggles.ts +3 -2
  76. package/src/helpers/toggleLegendActive.ts +6 -11
  77. package/src/helpers/urlDataHelpers.ts +70 -0
  78. package/src/hooks/useGeoClickHandler.ts +35 -1
  79. package/src/hooks/useLegendMemo.ts +17 -0
  80. package/src/hooks/useMapLayers.tsx +5 -4
  81. package/src/hooks/useStateZoom.tsx +137 -88
  82. package/src/hooks/useTooltip.ts +1 -2
  83. package/src/index.jsx +6 -3
  84. package/src/scss/main.scss +23 -12
  85. package/src/store/map.actions.ts +2 -2
  86. package/src/store/map.reducer.ts +21 -10
  87. package/src/test/CdcMap.test.jsx +11 -0
  88. package/src/types/MapConfig.ts +25 -17
  89. package/src/types/MapContext.ts +2 -8
  90. package/src/types/runtimeLegend.ts +12 -10
  91. package/vite.config.js +2 -7
  92. package/vitest.config.ts +16 -0
  93. package/src/_stories/_mock/floating-point.json +0 -427
  94. package/src/coreStyles_map.scss +0 -3
  95. package/src/helpers/colorDistributions.ts +0 -12
  96. package/src/helpers/generateColorsArray.ts +0 -14
  97. package/src/helpers/getStatePicked.ts +0 -8
  98. package/src/helpers/tests/generateColorsArray.test.ts +0 -18
  99. 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'
@@ -48,6 +52,12 @@ import { addUIDs, HEADER_COLORS } from '../../../helpers'
48
52
  import './editorPanel.styles.css'
49
53
  import FootnotesEditor from '@cdc/core/components/EditorPanel/FootnotesEditor'
50
54
  import { Datasets } from '@cdc/core/types/DataSet'
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'
51
61
 
52
62
  type MapEditorPanelProps = {
53
63
  datasets?: Datasets
@@ -64,8 +74,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
64
74
  setConfig,
65
75
  config,
66
76
  tooltipId,
67
- runtimeData,
68
- setRuntimeData
77
+ runtimeData
69
78
  } = useContext<MapContext>(ConfigContext)
70
79
 
71
80
  const { columnsRequiredChecker } = useColumnsRequiredChecker()
@@ -76,6 +85,11 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
76
85
  const [loadedDefault, setLoadedDefault] = useState(false)
77
86
  const [displayPanel, setDisplayPanel] = useState(true)
78
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)
79
93
 
80
94
  const {
81
95
  MapLayerHandlers: { handleMapLayer, handleAddLayer, handleRemoveLayer }
@@ -656,15 +670,6 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
656
670
  }
657
671
  })
658
672
  break
659
- case 'capitalizeLabels':
660
- setConfig({
661
- ...config,
662
- tooltips: {
663
- ...config.tooltips,
664
- capitalizeLabels: value
665
- }
666
- })
667
- break
668
673
  case 'showDataTable':
669
674
  setConfig({
670
675
  ...config,
@@ -684,21 +689,22 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
684
689
  })
685
690
  break
686
691
  case 'chooseState':
687
- let fipsCode = Object.keys(supportedStatesFipsCodes).find(key => supportedStatesFipsCodes[key] === value)
688
- let stateName = value
689
- let stateData = { fipsCode, stateName }
692
+ let stateData = value.map(state => ({
693
+ fipsCode: Object.keys(supportedStatesFipsCodes).find(key => supportedStatesFipsCodes[key] === state),
694
+ stateName: state
695
+ }))
690
696
 
691
697
  setConfig({
692
698
  ...config,
693
699
  general: {
694
700
  ...config.general,
695
- statePicked: stateData
701
+ statesPicked: stateData
696
702
  }
697
703
  })
698
704
 
699
705
  if (config) {
700
706
  const newData = generateRuntimeData(config)
701
- setRuntimeData(newData)
707
+ dispatch({ type: 'SET_RUNTIME_DATA', payload: newData })
702
708
  }
703
709
  break
704
710
  case 'classificationType':
@@ -728,12 +734,12 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
728
734
  }
729
735
  })
730
736
  break
731
- case 'filterControlsStatePicked':
737
+ case 'filterControlsStatesPicked':
732
738
  setConfig({
733
739
  ...config,
734
740
  general: {
735
741
  ...config.general,
736
- filterControlsStatePicked: value
742
+ filterControlsStatesPicked: value
737
743
  }
738
744
  })
739
745
  break
@@ -875,7 +881,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
875
881
  }
876
882
 
877
883
  const convertStateToConfig = () => {
878
- let strippedState = _.cloneDeep(config) // Deep copy
884
+ let strippedState = cloneConfig(config) // Deep copy
879
885
 
880
886
  // Strip ref
881
887
  delete strippedState['']
@@ -907,46 +913,75 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
907
913
 
908
914
  const isReversed = config.general.palette.isReversed
909
915
 
910
- function filterColorPalettes() {
911
- let sequential = []
912
- let nonSequential = []
913
- let accessibleColors = []
914
- for (let paletteName in colorPalettes) {
915
- if (!isReversed) {
916
- if (paletteName.includes('qualitative') && !paletteName.endsWith('reverse')) {
917
- nonSequential.push(paletteName)
918
- }
919
- if (paletteName.includes('colorblindsafe') && !paletteName.endsWith('reverse')) {
920
- accessibleColors.push(paletteName)
921
- }
922
- if (
923
- !paletteName.includes('qualitative') &&
924
- !paletteName.includes('colorblindsafe') &&
925
- !paletteName.endsWith('reverse')
926
- ) {
927
- sequential.push(paletteName)
928
- }
929
- }
930
- if (isReversed) {
931
- if (paletteName.includes('qualitative') && paletteName.endsWith('reverse')) {
932
- nonSequential.push(paletteName)
933
- }
934
- if (paletteName.includes('colorblindsafe') && paletteName.endsWith('reverse')) {
935
- accessibleColors.push(paletteName)
936
- }
937
- if (
938
- !paletteName.includes('qualitative') &&
939
- !paletteName.includes('colorblindsafe') &&
940
- paletteName.endsWith('reverse')
941
- ) {
942
- 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'
943
939
  }
944
940
  }
941
+ setConfig(_newConfig)
942
+ }
943
+
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)
945
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 || ''
946
980
 
947
- return [sequential, nonSequential, accessibleColors]
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' : ''
948
984
  }
949
- const [sequential, nonSequential, accessibleColors] = filterColorPalettes()
950
985
 
951
986
  useEffect(() => {
952
987
  setLoadedDefault(config.defaultData)
@@ -1198,13 +1233,13 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1198
1233
  {config.general.geoType === 'single-state' && runtimeData && (
1199
1234
  <Select
1200
1235
  label='Filter Controlling State Picked'
1201
- value={config.general.filterControlsStatePicked || ''}
1236
+ value={config.general.filterControlsStatesPicked || ''}
1202
1237
  options={[
1203
1238
  { value: '', label: 'None' },
1204
1239
  ...(runtimeData && columnsInData?.map(col => ({ value: col, label: col })))
1205
1240
  ]}
1206
1241
  onChange={event => {
1207
- handleEditorChanges('filterControlsStatePicked', event.target.value)
1242
+ handleEditorChanges('filterControlsStatesPicked', event.target.value)
1208
1243
  }}
1209
1244
  />
1210
1245
  )}
@@ -1212,17 +1247,20 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1212
1247
  {/* Type */}
1213
1248
  {/* Select > Filter a state */}
1214
1249
  {config.general.geoType === 'single-state' && (
1215
- <Select
1216
- label='State Selector'
1217
- value={config.general.statePicked?.stateName || ''}
1218
- options={StateOptionList().map(option => ({
1219
- value: option.props.value,
1220
- label: option.props.children
1221
- }))}
1222
- onChange={event => {
1223
- handleEditorChanges('chooseState', event.target.value)
1224
- }}
1225
- />
1250
+ <label>
1251
+ <span>States Selector</span>
1252
+ <MultiSelect
1253
+ selected={config.general.statesPicked.map(state => state.stateName)}
1254
+ options={StateOptionList().map(option => ({
1255
+ value: option.props.value,
1256
+ label: option.props.children
1257
+ }))}
1258
+ fieldName={'statesPicked'}
1259
+ updateField={(_, __, ___, selectedOptions) => {
1260
+ handleEditorChanges('chooseState', selectedOptions)
1261
+ }}
1262
+ />
1263
+ </label>
1226
1264
  )}
1227
1265
  {/* Type */}
1228
1266
  <Select
@@ -1267,7 +1305,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1267
1305
  { value: '_blank', label: 'New Window' }
1268
1306
  ]}
1269
1307
  onChange={event => {
1270
- const _newConfig = _.cloneDeep(config)
1308
+ const _newConfig = cloneConfig(config)
1271
1309
  _newConfig.general.navigationTarget = event.target.value
1272
1310
  setConfig(_newConfig)
1273
1311
  }}
@@ -1306,7 +1344,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1306
1344
  type='checkbox'
1307
1345
  checked={config.general.displayAsHex}
1308
1346
  onChange={event => {
1309
- const _newConfig = _.cloneDeep(config)
1347
+ const _newConfig = cloneConfig(config)
1310
1348
  _newConfig.general.displayAsHex = event.target.checked
1311
1349
  setConfig(_newConfig)
1312
1350
  }}
@@ -1362,7 +1400,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1362
1400
  type='checkbox'
1363
1401
  checked={general.territoriesAlwaysShow || false}
1364
1402
  onChange={event => {
1365
- const _newConfig = _.cloneDeep(config)
1403
+ const _newConfig = cloneConfig(config)
1366
1404
  _newConfig.general.territoriesAlwaysShow = event.target.checked
1367
1405
  setConfig(_newConfig)
1368
1406
  }}
@@ -1406,7 +1444,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1406
1444
  type='checkbox'
1407
1445
  checked={config.general.showTitle || false}
1408
1446
  onChange={event => {
1409
- const _newConfig = _.cloneDeep(config)
1447
+ const _newConfig = cloneConfig(config)
1410
1448
  _newConfig.general.showTitle = event.target.checked
1411
1449
  setConfig(_newConfig)
1412
1450
  }}
@@ -1550,7 +1588,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1550
1588
  type='checkbox'
1551
1589
  checked={config.general.hideGeoColumnInTooltip || false}
1552
1590
  onChange={event => {
1553
- const _newConfig = _.cloneDeep(config)
1591
+ const _newConfig = cloneConfig(config)
1554
1592
  _newConfig.general.hideGeoColumnInTooltip = event.target.checked
1555
1593
  setConfig(_newConfig)
1556
1594
  }}
@@ -1583,7 +1621,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
1583
1621
  value={columns.primary.name}
1584
1622
  options={columnsOptions.map(c => c.key)}
1585
1623
  onChange={event => {
1586
- const _state = _.cloneDeep(config)
1624
+ const _state = cloneConfig(config)
1587
1625
  _state.columns.primary.name = event.target.value
1588
1626
  _state.columns.primary.label = event.target.value
1589
1627
  setConfig(_state)
@@ -2087,7 +2125,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2087
2125
  messages = []
2088
2126
  }
2089
2127
 
2090
- const _newConfig = _.cloneDeep(config)
2128
+ const _newConfig = cloneConfig(config)
2091
2129
  _newConfig.legend.type = event.target.value
2092
2130
  _newConfig.runtime.editorErrorMessage = messages
2093
2131
  setConfig(_newConfig)
@@ -2100,7 +2138,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2100
2138
  type='checkbox'
2101
2139
  checked={config.general.showSidebar || false}
2102
2140
  onChange={event => {
2103
- const _newConfig = _.cloneDeep(config)
2141
+ const _newConfig = cloneConfig(config)
2104
2142
  _newConfig.general.showSidebar = event.target.checked
2105
2143
  setConfig(_newConfig)
2106
2144
  }}
@@ -2240,7 +2278,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2240
2278
  type='checkbox'
2241
2279
  checked={legend.singleColumn}
2242
2280
  onChange={event => {
2243
- const _newConfig = _.cloneDeep(config)
2281
+ const _newConfig = cloneConfig(config)
2244
2282
  _newConfig.legend.singleColumn = event.target.checked
2245
2283
  _newConfig.legend.singleRow = false
2246
2284
  _newConfig.legend.verticalSorted = false
@@ -2257,7 +2295,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2257
2295
  type='checkbox'
2258
2296
  checked={legend.singleRow}
2259
2297
  onChange={event => {
2260
- const _newConfig = _.cloneDeep(config)
2298
+ const _newConfig = cloneConfig(config)
2261
2299
  _newConfig.legend.singleRow = event.target.checked
2262
2300
  _newConfig.legend.singleColumn = false
2263
2301
  _newConfig.legend.verticalSorted = false
@@ -2275,7 +2313,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2275
2313
  value={legend.groupBy || ''}
2276
2314
  options={columnsOptions.map(c => c.key)}
2277
2315
  onChange={event => {
2278
- const _newConfig = _.cloneDeep(config)
2316
+ const _newConfig = cloneConfig(config)
2279
2317
  _newConfig.legend.groupBy = event.target.value
2280
2318
  setConfig(_newConfig)
2281
2319
  }}
@@ -2287,7 +2325,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2287
2325
  type='checkbox'
2288
2326
  checked={legend.verticalSorted}
2289
2327
  onChange={event => {
2290
- const _newConfig = _.cloneDeep(config)
2328
+ const _newConfig = cloneConfig(config)
2291
2329
  _newConfig.legend.verticalSorted = event.target.checked
2292
2330
  setConfig(_newConfig)
2293
2331
  }}
@@ -2315,7 +2353,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2315
2353
  type='checkbox'
2316
2354
  checked={legend.separateZero || false}
2317
2355
  onChange={event => {
2318
- const _newConfig = _.cloneDeep(config)
2356
+ const _newConfig = cloneConfig(config)
2319
2357
  _newConfig.legend.separateZero = event.target.checked
2320
2358
  return setConfig(_newConfig)
2321
2359
  }}
@@ -2775,7 +2813,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2775
2813
  type='checkbox'
2776
2814
  checked={config.table.showDataTableLink}
2777
2815
  onChange={event => {
2778
- const _newConfig = _.cloneDeep(config)
2816
+ const _newConfig = cloneConfig(config)
2779
2817
  _newConfig.table.showDataTableLink = event.target.checked
2780
2818
  setConfig(_newConfig)
2781
2819
  }}
@@ -2789,7 +2827,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2789
2827
  type='checkbox'
2790
2828
  checked={config.table.showDownloadUrl}
2791
2829
  onChange={event => {
2792
- const _newConfig = _.cloneDeep(config)
2830
+ const _newConfig = cloneConfig(config)
2793
2831
  _newConfig.table.showDownloadUrl = event.target.checked
2794
2832
  setConfig(_newConfig)
2795
2833
  }}
@@ -2872,16 +2910,6 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2872
2910
  updateField={updateField}
2873
2911
  />
2874
2912
  )}
2875
- <label className='checkbox'>
2876
- <input
2877
- type='checkbox'
2878
- checked={config.tooltips.capitalizeLabels}
2879
- onChange={event => {
2880
- handleEditorChanges('capitalizeLabels', event.target.checked)
2881
- }}
2882
- />
2883
- <span className='edit-label'>Capitalize text inside tooltip</span>
2884
- </label>
2885
2913
  </AccordionItemPanel>
2886
2914
  </AccordionItem>
2887
2915
  <AccordionItem>
@@ -2925,7 +2953,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2925
2953
  type='checkbox'
2926
2954
  checked={config.general.fullBorder || false}
2927
2955
  onChange={event => {
2928
- const _newConfig = _.cloneDeep(config)
2956
+ const _newConfig = cloneConfig(config)
2929
2957
  _newConfig.general.fullBorder = event.target.checked
2930
2958
  setConfig(_newConfig)
2931
2959
  }}
@@ -2947,6 +2975,15 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2947
2975
  <label>
2948
2976
  <span className='edit-label'>Map Color Palette</span>
2949
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} />
2950
2987
  <InputToggle
2951
2988
  type='3d'
2952
2989
  section='general'
@@ -2955,127 +2992,71 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
2955
2992
  size='small'
2956
2993
  label='Use selected palette in reverse order'
2957
2994
  onClick={() => {
2958
- const _state = _.cloneDeep(config)
2995
+ const _state = cloneConfig(config)
2996
+ const currentPaletteName = config.general.palette?.name || ''
2959
2997
  _state.general.palette.isReversed = !_state.general.palette.isReversed
2960
2998
  let paletteName = ''
2961
- if (_state.general.palette.isReversed && !config.color.endsWith('reverse')) {
2962
- paletteName = config.color + 'reverse'
2999
+ if (_state.general.palette.isReversed && !currentPaletteName.endsWith('reverse')) {
3000
+ paletteName = currentPaletteName + 'reverse'
2963
3001
  }
2964
- if (!_state.general.palette.isReversed && config.color.endsWith('reverse')) {
2965
- paletteName = config.color.slice(0, -7)
3002
+ if (!_state.general.palette.isReversed && currentPaletteName.endsWith('reverse')) {
3003
+ paletteName = currentPaletteName.slice(0, -7)
2966
3004
  }
2967
3005
  if (paletteName) {
2968
- _state.color = paletteName
3006
+ _state.general.palette.name = paletteName
2969
3007
  }
2970
3008
  setConfig(_state)
2971
3009
  }}
2972
3010
  value={config.general.palette.isReversed}
2973
3011
  />
2974
3012
  <span>Sequential</span>
2975
- <ul className='color-palette'>
2976
- {sequential.map(palette => {
2977
- const colorOne = {
2978
- backgroundColor: colorPalettes[palette][2]
2979
- }
2980
-
2981
- const colorTwo = {
2982
- backgroundColor: colorPalettes[palette][4]
2983
- }
2984
-
2985
- const colorThree = {
2986
- backgroundColor: colorPalettes[palette][6]
2987
- }
2988
-
2989
- return (
2990
- <li
2991
- title={palette}
2992
- key={palette}
2993
- onClick={() => {
2994
- const _newConfig = _.cloneDeep(config)
2995
- _newConfig.color = palette
2996
- setConfig(_newConfig)
2997
- }}
2998
- className={config.color === palette ? 'selected' : ''}
2999
- >
3000
- <span style={colorOne}></span>
3001
- <span style={colorTwo}></span>
3002
- <span style={colorThree}></span>
3003
- </li>
3004
- )
3005
- })}
3006
- </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
+ />
3007
3024
  <span>Non-Sequential</span>
3008
- <ul className='color-palette'>
3009
- {nonSequential.map(palette => {
3010
- const colorOne = {
3011
- backgroundColor: colorPalettes[palette][2]
3012
- }
3013
-
3014
- const colorTwo = {
3015
- backgroundColor: colorPalettes[palette][4]
3016
- }
3017
-
3018
- const colorThree = {
3019
- backgroundColor: colorPalettes[palette][6]
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
3020
3038
  }
3021
-
3022
- // hide palettes with too few colors for region maps
3023
- if (colorPalettes[palette].length <= 8 && config.general.geoType === 'us-region') {
3024
- return ''
3025
- }
3026
- return (
3027
- <li
3028
- title={palette}
3029
- key={palette}
3030
- onClick={() => {
3031
- const _newConfig = _.cloneDeep(config)
3032
- _newConfig.color = palette
3033
- setConfig(_newConfig)
3034
- }}
3035
- className={config.color === palette ? 'selected' : ''}
3036
- >
3037
- <span style={colorOne}></span>
3038
- <span style={colorTwo}></span>
3039
- <span style={colorThree}></span>
3040
- </li>
3041
- )
3042
- })}
3043
- </ul>
3039
+ return true
3040
+ }}
3041
+ />
3044
3042
  <span>Colorblind Safe</span>
3045
- <ul className='color-palette'>
3046
- {accessibleColors.map(palette => {
3047
- const colorOne = {
3048
- backgroundColor: colorPalettes[palette][2]
3049
- }
3050
-
3051
- const colorTwo = {
3052
- backgroundColor: colorPalettes[palette][4]
3053
- }
3054
-
3055
- const colorThree = {
3056
- backgroundColor: colorPalettes[palette][6]
3057
- }
3058
-
3059
- // hide palettes with too few colors for region maps
3060
- if (colorPalettes[palette].length <= 8 && config.general.geoType === 'us-region') {
3061
- 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
3062
3056
  }
3063
- return (
3064
- <li
3065
- title={palette}
3066
- key={palette}
3067
- onClick={() => {
3068
- handleEditorChanges('color', palette)
3069
- }}
3070
- className={config.color === palette ? 'selected' : ''}
3071
- >
3072
- <span style={colorOne}></span>
3073
- <span style={colorTwo}></span>
3074
- <span style={colorThree}></span>
3075
- </li>
3076
- )
3077
- })}
3078
- </ul>
3057
+ return true
3058
+ }}
3059
+ />
3079
3060
  <label>
3080
3061
  Geocode Settings
3081
3062
  <TextField
@@ -3130,7 +3111,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3130
3111
  type='checkbox'
3131
3112
  checked={config.general.allowMapZoom}
3132
3113
  onChange={event => {
3133
- const _newConfig = _.cloneDeep(config)
3114
+ const _newConfig = cloneConfig(config)
3134
3115
  _newConfig.general.allowMapZoom = event.target.checked
3135
3116
  _newConfig.mapPosition.coordinates = config.general.geoType === 'world' ? [0, 30] : [0, 0]
3136
3117
  _newConfig.mapPosition.zoom = 1
@@ -3146,7 +3127,7 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3146
3127
  type='checkbox'
3147
3128
  checked={config.visual.extraBubbleBorder}
3148
3129
  onChange={event => {
3149
- const _newConfig = _.cloneDeep(config)
3130
+ const _newConfig = cloneConfig(config)
3150
3131
  _newConfig.visual.extraBubbleBorder = event.target.checked
3151
3132
  setConfig(_newConfig)
3152
3133
  }}
@@ -3385,9 +3366,26 @@ const EditorPanel: React.FC<MapEditorPanelProps> = ({ datasets }) => {
3385
3366
  </AccordionItem>
3386
3367
  {config.general.geoType === 'us' && <Panels.PatternSettings name='Pattern Settings' />}
3387
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
+ />
3388
3377
  </Accordion>
3389
3378
  <AdvancedEditor loadConfig={setConfig} config={config} convertStateToConfig={convertStateToConfig} />
3390
3379
  </Layout.Sidebar>
3380
+
3381
+ {showConversionModal && (
3382
+ <PaletteConversionModal
3383
+ onConfirm={handleConversionConfirm}
3384
+ onCancel={handleConversionCancel}
3385
+ onReturnToV1={handleReturnToV1}
3386
+ paletteName={pendingPaletteSelection?.palette}
3387
+ />
3388
+ )}
3391
3389
  </ErrorBoundary>
3392
3390
  )
3393
3391
  }