@cdc/map 4.23.2 → 4.23.4

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 (36) hide show
  1. package/dist/cdcmap.js +24661 -24191
  2. package/examples/custom-map-layers.json +764 -0
  3. package/examples/default-county.json +169 -155
  4. package/examples/default-geocode.json +744 -744
  5. package/examples/default-hex.json +3 -5
  6. package/examples/example-city-state-no-territories.json +703 -0
  7. package/examples/example-city-state.json +26 -7
  8. package/examples/example-city-stateBAD.json +744 -0
  9. package/examples/gallery/city-state.json +478 -478
  10. package/examples/testing-layer-2.json +1 -0
  11. package/examples/testing-layer.json +96 -0
  12. package/examples/world-geocode-data.json +18 -0
  13. package/examples/world-geocode.json +108 -0
  14. package/index.html +35 -29
  15. package/package.json +6 -3
  16. package/src/CdcMap.jsx +179 -111
  17. package/src/components/CityList.jsx +35 -35
  18. package/src/components/CountyMap.jsx +309 -446
  19. package/src/components/DataTable.jsx +7 -31
  20. package/src/components/EditorPanel.jsx +468 -217
  21. package/src/components/Sidebar.jsx +2 -0
  22. package/src/components/UsaMap.jsx +34 -23
  23. package/src/components/WorldMap.jsx +40 -8
  24. package/src/data/feature-test.json +73 -0
  25. package/src/data/initial-state.js +10 -3
  26. package/src/data/supported-geos.js +7 -7
  27. package/src/hooks/useMapLayers.jsx +243 -0
  28. package/src/index.jsx +4 -8
  29. package/src/scss/editor-panel.scss +97 -97
  30. package/src/scss/filters.scss +0 -2
  31. package/src/scss/main.scss +25 -26
  32. package/src/scss/map.scss +12 -0
  33. package/src/test/CdcMap.test.jsx +19 -0
  34. package/vite.config.js +3 -3
  35. package/src/components/Filters.jsx +0 -113
  36. package/src/hooks/useColorPalette.ts +0 -88
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useCallback } from 'react'
1
+ import React, { useState, useEffect, useCallback, memo } from 'react'
2
2
 
3
3
  // Third Party
4
4
  import { Accordion, AccordionItem, AccordionItemHeading, AccordionItemPanel, AccordionItemButton } from 'react-accessible-accordion'
@@ -11,9 +11,6 @@ import { Tooltip as ReactTooltip } from 'react-tooltip'
11
11
  import colorPalettes from '@cdc/core/data/colorPalettes'
12
12
  import { supportedStatesFipsCodes } from '../data/supported-geos'
13
13
 
14
- // Hooks
15
- import { useColorPalette } from '../hooks/useColorPalette'
16
-
17
14
  // Components - Core
18
15
  import AdvancedEditor from '@cdc/core/components/AdvancedEditor'
19
16
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
@@ -30,6 +27,9 @@ import AlabamaGraphic from '@cdc/core/assets/icon-map-alabama.svg'
30
27
  import worldDefaultConfig from '../../examples/default-world.json'
31
28
  import usaDefaultConfig from '../../examples/default-usa.json'
32
29
  import countyDefaultConfig from '../../examples/default-county.json'
30
+ import useMapLayers from '../hooks/useMapLayers'
31
+
32
+ import { useFilters } from '@cdc/core/components/Filters'
33
33
 
34
34
  const TextField = ({ label, section = null, subsection = null, fieldName, updateField, value: stateValue, type = 'input', tooltip, ...attributes }) => {
35
35
  const [value, setValue] = useState(stateValue)
@@ -40,7 +40,7 @@ const TextField = ({ label, section = null, subsection = null, fieldName, update
40
40
  if ('string' === typeof debouncedValue && stateValue !== debouncedValue) {
41
41
  updateField(section, subsection, fieldName, debouncedValue)
42
42
  }
43
- }, [debouncedValue])
43
+ }, [debouncedValue]) // eslint-disable-line
44
44
 
45
45
  let name = subsection ? `${section}-${subsection}-${fieldName}` : `${section}-${subsection}-${fieldName}`
46
46
 
@@ -68,26 +68,36 @@ const TextField = ({ label, section = null, subsection = null, fieldName, update
68
68
  }
69
69
 
70
70
  const EditorPanel = props => {
71
- const { state, columnsInData = [], loadConfig, setState, isDashboard, setParentConfig, setRuntimeFilters, runtimeFilters, runtimeLegend } = props
71
+ const { state, columnsInData = [], loadConfig, setState, isDashboard, setParentConfig, runtimeFilters, runtimeLegend, changeFilterActive, isDebug, setRuntimeFilters } = props
72
72
 
73
73
  const { general, columns, legend, dataTable, tooltips } = state
74
74
 
75
75
  const [requiredColumns, setRequiredColumns] = useState(null) // Simple state so we know if we need more information before parsing the map
76
76
 
77
- const [configTextboxValue, setConfigTextbox] = useState({})
77
+ const [configTextboxValue, setConfigTextbox] = useState({}) // eslint-disable-line
78
78
 
79
79
  const [loadedDefault, setLoadedDefault] = useState(false)
80
80
 
81
81
  const [displayPanel, setDisplayPanel] = useState(true)
82
82
 
83
- const [advancedToggle, setAdvancedToggle] = useState(false)
84
-
85
83
  const [activeFilterValueForDescription, setActiveFilterValueForDescription] = useState([0, 0])
86
84
 
87
- const { filteredPallets, filteredQualitative, isPaletteReversed, paletteName } = useColorPalette(colorPalettes, state)
85
+ const { handleFilterOrder, filterOrderOptions, filterStyleOptions } = useFilters({ config: state, setConfig: setState, filteredData: runtimeFilters, setFilteredData: setRuntimeFilters })
88
86
 
89
87
  const headerColors = ['theme-blue', 'theme-purple', 'theme-brown', 'theme-teal', 'theme-pink', 'theme-orange', 'theme-slate', 'theme-indigo', 'theme-cyan', 'theme-green', 'theme-amber']
90
88
 
89
+ const {
90
+ // prettier-ignore
91
+ MapLayerHandlers: {
92
+ handleAddLayer,
93
+ handleMapLayerName,
94
+ handleMapLayerUrl,
95
+ handleRemoveLayer,
96
+ handleMapLayerNamespace,
97
+ handleMapLayerTooltip
98
+ }
99
+ } = useMapLayers(state, setState, false, true)
100
+
91
101
  const categoryMove = (idx1, idx2) => {
92
102
  let categoryValuesOrder = [...state.legend.categoryValuesOrder]
93
103
 
@@ -104,23 +114,45 @@ const EditorPanel = props => {
104
114
  })
105
115
  }
106
116
 
107
- const handleFilterOrder = (idx1, idx2, filterIndex, filter) => {
108
- let filterOrder = filter.values
109
- let [movedItem] = filterOrder.splice(idx1, 1)
110
- filterOrder.splice(idx2, 0, movedItem)
111
- let filters = [...runtimeFilters]
112
- let filterItem = { ...runtimeFilters[filterIndex] }
113
- filterItem.active = filter.values[0]
114
- filterItem.values = filterOrder
115
- filterItem.order = 'cust'
116
- filters[filterIndex] = filterItem
117
-
117
+ let specialClasses = []
118
+ if (legend.specialClasses && legend.specialClasses.length && typeof legend.specialClasses[0] === 'string') {
119
+ legend.specialClasses.forEach(specialClass => {
120
+ specialClasses.push({
121
+ key: state.columns.primary && state.columns.primary.name ? state.columns.primary.name : columnsInData[0],
122
+ value: specialClass,
123
+ label: specialClass
124
+ })
125
+ })
126
+ // DEV-3303 - since the above was a repair of bad config - need to backpopulate into the state
118
127
  setState({
119
128
  ...state,
120
- filters
129
+ legend: {
130
+ ...state.legend,
131
+ specialClasses: specialClasses
132
+ }
121
133
  })
134
+ } else {
135
+ specialClasses = legend.specialClasses || []
122
136
  }
123
137
 
138
+ const CheckBox = memo(({ label, value, fieldName, section = null, subsection = null, tooltip, updateField, ...attributes }) => (
139
+ <label className='checkbox column-heading'>
140
+ <input
141
+ type='checkbox'
142
+ name={fieldName}
143
+ checked={value}
144
+ onChange={e => {
145
+ updateField(section, subsection, fieldName, !value)
146
+ }}
147
+ {...attributes}
148
+ />
149
+ <span className='edit-label'>
150
+ {label}
151
+ {tooltip}
152
+ </span>
153
+ </label>
154
+ ))
155
+
124
156
  const DynamicDesc = ({ label, fieldName, value: stateValue, type = 'input', ...attributes }) => {
125
157
  const [value, setValue] = useState(stateValue)
126
158
 
@@ -130,7 +162,7 @@ const EditorPanel = props => {
130
162
  if ('string' === typeof debouncedValue && stateValue !== debouncedValue) {
131
163
  handleEditorChanges('changeLegendDescription', [String(activeFilterValueForDescription), debouncedValue])
132
164
  }
133
- }, [debouncedValue])
165
+ }, [debouncedValue]) // eslint-disable-line
134
166
 
135
167
  const onChange = e => setValue(e.target.value)
136
168
 
@@ -169,6 +201,17 @@ const EditorPanel = props => {
169
201
  }
170
202
  })
171
203
  break
204
+
205
+ case 'toggleDataTableLink':
206
+ setState({
207
+ ...state,
208
+ table: {
209
+ ...state.table,
210
+ showDataTableLink: value
211
+ }
212
+ })
213
+ break
214
+
172
215
  case 'toggleDataUrl':
173
216
  setState({
174
217
  ...state,
@@ -418,6 +461,15 @@ const EditorPanel = props => {
418
461
  }
419
462
  })
420
463
  break
464
+ case 'world-geocode':
465
+ setState({
466
+ ...state,
467
+ general: {
468
+ ...state.general,
469
+ type: value
470
+ }
471
+ })
472
+ break
421
473
  case 'data':
422
474
  setState({
423
475
  ...state,
@@ -457,7 +509,7 @@ const EditorPanel = props => {
457
509
  })
458
510
  break
459
511
  default:
460
- console.warn('Map type not set')
512
+ console.warn('COVE: Map type not set') // eslint-disable-line
461
513
  break
462
514
  }
463
515
  break
@@ -484,7 +536,8 @@ const EditorPanel = props => {
484
536
  ...state,
485
537
  general: {
486
538
  ...state.general,
487
- geoType: 'us'
539
+ geoType: 'us',
540
+ type: state.type === 'us-geocode' ? 'data' : state.type
488
541
  },
489
542
  dataTable: {
490
543
  ...state.dataTable,
@@ -571,6 +624,15 @@ const EditorPanel = props => {
571
624
  }
572
625
  })
573
626
  break
627
+ case 'legendShowSpecialClassesLast':
628
+ setState({
629
+ ...state,
630
+ legend: {
631
+ ...state.legend,
632
+ showSpecialClassesLast: !state.legend.showSpecialClassesLast
633
+ }
634
+ })
635
+ break
574
636
  case 'dynamicDescription':
575
637
  setState({
576
638
  ...state,
@@ -673,14 +735,28 @@ const EditorPanel = props => {
673
735
  }
674
736
  })
675
737
  break
738
+ case 'territoriesAlwaysShow':
739
+ setState({
740
+ ...state,
741
+ general: {
742
+ ...state.general,
743
+ territoriesAlwaysShow: value
744
+ }
745
+ })
746
+ break
747
+ case 'filterBehavior':
748
+ setState({
749
+ ...state,
750
+ filterBehavior: value
751
+ })
752
+ break
676
753
  default:
677
- console.warn(`Did not recognize editor property.`)
754
+ console.warn(`COVE: Did not recognize editor property.`) // eslint-disable-line
678
755
  break
679
756
  }
680
757
  }
681
758
 
682
759
  const columnsRequiredChecker = useCallback(() => {
683
- console.info('Running columns required check.')
684
760
  let columnList = []
685
761
 
686
762
  // Geo is always required
@@ -698,11 +774,11 @@ const EditorPanel = props => {
698
774
  columnList.push('Navigation')
699
775
  }
700
776
 
701
- if ('us-geocode' === state.general.type && '' === state.columns.latitude.name) {
777
+ if (('us-geocode' === state.general.type || 'world-geocode' === state.general.type) && '' === state.columns.latitude.name) {
702
778
  columnList.push('Latitude')
703
779
  }
704
780
 
705
- if ('us-geocode' === state.general.type && '' === state.columns.longitude.name) {
781
+ if (('us-geocode' === state.general.type || 'world-geocode' === state.general.type) && '' === state.columns.longitude.name) {
706
782
  columnList.push('Longitude')
707
783
  }
708
784
 
@@ -713,10 +789,9 @@ const EditorPanel = props => {
713
789
 
714
790
  const editColumn = async (columnName, editTarget, value) => {
715
791
  let newSpecialClasses
716
-
717
792
  switch (editTarget) {
718
793
  case 'specialClassEdit':
719
- newSpecialClasses = Array.from(legend.specialClasses)
794
+ newSpecialClasses = Array.from(specialClasses)
720
795
 
721
796
  newSpecialClasses[value.index][value.prop] = value.value
722
797
 
@@ -729,7 +804,7 @@ const EditorPanel = props => {
729
804
  })
730
805
  break
731
806
  case 'specialClassDelete':
732
- newSpecialClasses = Array.from(legend.specialClasses)
807
+ newSpecialClasses = Array.from(specialClasses)
733
808
 
734
809
  newSpecialClasses.splice(value, 1)
735
810
 
@@ -742,7 +817,7 @@ const EditorPanel = props => {
742
817
  })
743
818
  break
744
819
  case 'specialClassAdd':
745
- newSpecialClasses = legend.specialClasses
820
+ newSpecialClasses = specialClasses
746
821
 
747
822
  newSpecialClasses.push(value)
748
823
 
@@ -799,6 +874,10 @@ const EditorPanel = props => {
799
874
  newFilters.splice(idx, 1)
800
875
  }
801
876
  break
877
+ case 'filterStyle':
878
+ newFilters[idx] = { ...newFilters[idx] }
879
+ newFilters[idx].filterStyle = value
880
+ break
802
881
  case 'columnName':
803
882
  newFilters[idx] = { ...newFilters[idx] }
804
883
  newFilters[idx].columnName = value
@@ -849,6 +928,33 @@ const EditorPanel = props => {
849
928
  })
850
929
  }
851
930
 
931
+ const MapFilters = () => {
932
+ return (
933
+ <>
934
+ <label>
935
+ Filter Behavior
936
+ <select
937
+ value={state.filterBehavior}
938
+ onChange={e => {
939
+ setState({
940
+ ...state,
941
+ filterBehavior: e.target.value
942
+ })
943
+ }}
944
+ >
945
+ <option key='Apply Button' value='Apply Button'>
946
+ Apply Button
947
+ </option>
948
+ <option key='Filter Change' value='Filter Change'>
949
+ Filter Change
950
+ </option>
951
+ </select>
952
+ </label>
953
+ {filtersJSX}
954
+ </>
955
+ )
956
+ }
957
+
852
958
  const removeAdditionalColumn = columnName => {
853
959
  const newColumns = state.columns
854
960
 
@@ -915,11 +1021,51 @@ const EditorPanel = props => {
915
1021
  return strippedState
916
1022
  }
917
1023
 
1024
+ const isReversed = state.general.palette.isReversed
1025
+ function filterColorPalettes() {
1026
+ let sequential = []
1027
+ let nonSequential = []
1028
+ for (let paletteName in colorPalettes) {
1029
+ if (!isReversed) {
1030
+ if (paletteName.includes('qualitative') && !paletteName.endsWith('reverse')) {
1031
+ nonSequential.push(paletteName)
1032
+ }
1033
+ if (!paletteName.includes('qualitative') && !paletteName.endsWith('reverse')) {
1034
+ sequential.push(paletteName)
1035
+ }
1036
+ }
1037
+ if (isReversed) {
1038
+ if (paletteName.includes('qualitative') && paletteName.endsWith('reverse')) {
1039
+ nonSequential.push(paletteName)
1040
+ }
1041
+ if (!paletteName.includes('qualitative') && paletteName.endsWith('reverse')) {
1042
+ sequential.push(paletteName)
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ return [sequential, nonSequential]
1048
+ }
1049
+ const [sequential, nonSequential] = filterColorPalettes()
1050
+
1051
+ useEffect(() => {
1052
+ let paletteName = ''
1053
+ if (isReversed && !state.color.endsWith('reverse')) {
1054
+ paletteName = state.color + 'reverse'
1055
+ }
1056
+ if (!isReversed && state.color.endsWith('reverse')) {
1057
+ paletteName = state.color.slice(0, -7)
1058
+ }
1059
+ if (paletteName) {
1060
+ handleEditorChanges('color', paletteName)
1061
+ }
1062
+ }, [isReversed])
1063
+
918
1064
  useEffect(() => {
919
1065
  setLoadedDefault(state.defaultData)
920
1066
 
921
1067
  columnsRequiredChecker()
922
- }, [state])
1068
+ }, [state]) // eslint-disable-line
923
1069
 
924
1070
  useEffect(() => {
925
1071
  //If a categorical map is used and the order is either not defined or incorrect, fix it
@@ -931,12 +1077,12 @@ const EditorPanel = props => {
931
1077
  valid = false
932
1078
  }
933
1079
  })
934
- let runtimeLegendKeys = runtimeLegend.map(item => item.value);
1080
+ let runtimeLegendKeys = runtimeLegend.map(item => item.value)
935
1081
  state.legend.categoryValuesOrder.forEach(category => {
936
1082
  if (runtimeLegendKeys.indexOf(category) === -1) {
937
- valid = false;
1083
+ valid = false
938
1084
  }
939
- });
1085
+ })
940
1086
  } else {
941
1087
  valid = false
942
1088
  }
@@ -953,7 +1099,7 @@ const EditorPanel = props => {
953
1099
  })
954
1100
  }
955
1101
  }
956
- }, [runtimeLegend])
1102
+ }, [runtimeLegend]) // eslint-disable-line
957
1103
 
958
1104
  // if no state choice by default show alabama
959
1105
  useEffect(() => {
@@ -969,7 +1115,7 @@ const EditorPanel = props => {
969
1115
  }
970
1116
  })
971
1117
  }
972
- }, [])
1118
+ }, []) // eslint-disable-line
973
1119
 
974
1120
  const columnsOptions = [
975
1121
  <option value='' key={'Select Option'}>
@@ -978,7 +1124,7 @@ const EditorPanel = props => {
978
1124
  ]
979
1125
 
980
1126
  columnsInData.map(colName => {
981
- columnsOptions.push(
1127
+ return columnsOptions.push(
982
1128
  <option value={colName} key={colName}>
983
1129
  {colName}
984
1130
  </option>
@@ -997,19 +1143,6 @@ const EditorPanel = props => {
997
1143
  })
998
1144
  })
999
1145
 
1000
- let specialClasses = []
1001
- if (legend.specialClasses && legend.specialClasses.length && typeof legend.specialClasses[0] === 'string') {
1002
- legend.specialClasses.forEach(specialClass => {
1003
- specialClasses.push({
1004
- key: state.columns.primary && state.columns.primary.name ? state.columns.primary.name : columnsInData[0],
1005
- value: specialClass,
1006
- label: specialClass
1007
- })
1008
- })
1009
- } else {
1010
- specialClasses = legend.specialClasses || []
1011
- }
1012
-
1013
1146
  const additionalColumns = Object.keys(state.columns).filter(value => {
1014
1147
  const defaultCols = ['geo', 'navigate', 'primary', 'latitude', 'longitude']
1015
1148
 
@@ -1021,6 +1154,7 @@ const EditorPanel = props => {
1021
1154
 
1022
1155
  const updateField = (section, subsection, fieldName, newValue) => {
1023
1156
  const isArray = Array.isArray(state[section])
1157
+
1024
1158
  let sectionValue = isArray ? [...state[section], newValue] : { ...state[section], [fieldName]: newValue }
1025
1159
 
1026
1160
  if (null !== subsection) {
@@ -1054,98 +1188,106 @@ const EditorPanel = props => {
1054
1188
  usedFilterColumns[filter.columnName] = true
1055
1189
  }
1056
1190
 
1057
- const filterOptions = [
1058
- {
1059
- label: 'Ascending Alphanumeric',
1060
- value: 'asc'
1061
- },
1062
- {
1063
- label: 'Descending Alphanumeric',
1064
- value: 'desc'
1065
- },
1066
- {
1067
- label: 'Custom',
1068
- value: 'cust'
1069
- }
1070
- ]
1071
-
1072
1191
  return (
1073
- <fieldset className='edit-block' key={`filter-${index}`}>
1074
- <button
1075
- className='remove-column'
1076
- onClick={e => {
1077
- e.preventDefault()
1078
- changeFilter(index, 'remove')
1079
- }}
1080
- >
1081
- Remove
1082
- </button>
1083
- <TextField value={state.filters[index].label} section='filters' subsection={index} fieldName='label' label='Label' updateField={updateField} />
1084
- <label>
1085
- <span className='edit-label column-heading'>
1086
- Filter Column
1087
- <Tooltip style={{ textTransform: 'none' }}>
1088
- <Tooltip.Target>
1089
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
1090
- </Tooltip.Target>
1091
- <Tooltip.Content>
1092
- <p>Selecting a column will add a dropdown menu below the map legend and allow users to filter based on the values in this column.</p>
1093
- </Tooltip.Content>
1094
- </Tooltip>
1095
- </span>
1096
- <select
1097
- value={filter.columnName}
1098
- onChange={event => {
1099
- changeFilter(index, 'columnName', event.target.value)
1192
+ <>
1193
+ <fieldset className='edit-block' key={`filter-${index}`}>
1194
+ <button
1195
+ className='remove-column'
1196
+ onClick={e => {
1197
+ e.preventDefault()
1198
+ changeFilter(index, 'remove')
1100
1199
  }}
1101
1200
  >
1102
- {columnsOptions.filter(({ key }) => undefined === usedFilterColumns[key] || filter.columnName === key)}
1103
- </select>
1104
- </label>
1105
-
1106
- <label>
1107
- <span className='edit-filterOrder column-heading'>Filter Order</span>
1108
- <select
1109
- value={filter.order}
1110
- onChange={e => {
1111
- changeFilter(index, 'filterOrder', e.target.value)
1112
- }}
1113
- >
1114
- {filterOptions.map((option, index) => {
1115
- return (
1116
- <option value={option.value} key={`filter-${index}`}>
1117
- {option.label}
1118
- </option>
1119
- )
1120
- })}
1121
- </select>
1122
- </label>
1123
-
1124
- {filter.order === 'cust' && (
1125
- <DragDropContext onDragEnd={({ source, destination }) => handleFilterOrder(source.index, destination.index, index, runtimeFilters[index])}>
1126
- <Droppable droppableId='filter_order'>
1127
- {provided => (
1128
- <ul {...provided.droppableProps} className='sort-list' ref={provided.innerRef} style={{ marginTop: '1em' }}>
1129
- {runtimeFilters[index]?.values.map((value, index) => {
1130
- return (
1131
- <Draggable key={value} draggableId={`draggableFilter-${value}`} index={index}>
1132
- {(provided, snapshot) => (
1133
- <li>
1134
- <div className={snapshot.isDragging ? 'currently-dragging' : ''} style={getItemStyle(snapshot.isDragging, provided.draggableProps.style, sortableItemStyles)} ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
1135
- {value}
1136
- </div>
1137
- </li>
1138
- )}
1139
- </Draggable>
1140
- )
1141
- })}
1142
- {provided.placeholder}
1143
- </ul>
1144
- )}
1145
- </Droppable>
1146
- </DragDropContext>
1147
- )}
1148
- </fieldset>
1201
+ Remove
1202
+ </button>
1203
+ <TextField value={state.filters[index].label} section='filters' subsection={index} fieldName='label' label='Label' updateField={updateField} />
1204
+ <label>
1205
+ <span className='edit-label column-heading'>
1206
+ Filter Column
1207
+ <Tooltip style={{ textTransform: 'none' }}>
1208
+ <Tooltip.Target>
1209
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
1210
+ </Tooltip.Target>
1211
+ <Tooltip.Content>
1212
+ <p>Selecting a column will add a dropdown menu below the map legend and allow users to filter based on the values in this column.</p>
1213
+ </Tooltip.Content>
1214
+ </Tooltip>
1215
+ </span>
1216
+ <select
1217
+ value={filter.columnName}
1218
+ onChange={event => {
1219
+ changeFilter(index, 'columnName', event.target.value)
1220
+ }}
1221
+ >
1222
+ {columnsOptions.filter(({ key }) => undefined === usedFilterColumns[key] || filter.columnName === key)}
1223
+ </select>
1224
+ </label>
1225
+
1226
+ {/* COMING SOON: 4.23.5: FILTER STYLES */}
1227
+
1228
+ {/* <label>
1229
+ <span className='edit-filterOrder column-heading'>Filter Style</span>
1230
+ <select
1231
+ value={filter.filterStyle}
1232
+ onChange={e => {
1233
+ changeFilter(index, 'filterStyle', e.target.value)
1234
+ }}
1235
+ >
1236
+ {filterStyleOptions.map((option, index) => {
1237
+ return (
1238
+ <option value={option} key={`filter-${option}--${index}`}>
1239
+ {option}
1240
+ </option>
1241
+ )
1242
+ })}
1243
+ </select>
1244
+ </label> */}
1245
+
1246
+ <label>
1247
+ <span className='edit-filterOrder column-heading'>Filter Order</span>
1248
+ <select
1249
+ value={filter.order}
1250
+ onChange={e => {
1251
+ changeFilter(index, 'filterOrder', e.target.value)
1252
+ changeFilterActive(index, filter.values[0])
1253
+ }}
1254
+ >
1255
+ {filterOrderOptions.map((option, index) => {
1256
+ return (
1257
+ <option value={option.value} key={`filter-${index}`}>
1258
+ {option.label}
1259
+ </option>
1260
+ )
1261
+ })}
1262
+ </select>
1263
+ </label>
1264
+
1265
+ {filter.order === 'cust' && (
1266
+ <DragDropContext onDragEnd={({ source, destination }) => handleFilterOrder(source.index, destination.index, index, state.filters[index])}>
1267
+ <Droppable droppableId='filter_order'>
1268
+ {provided => (
1269
+ <ul {...provided.droppableProps} className='sort-list' ref={provided.innerRef} style={{ marginTop: '1em' }}>
1270
+ {state.filters[index]?.values.map((value, index) => {
1271
+ return (
1272
+ <Draggable key={value} draggableId={`draggableFilter-${value}`} index={index}>
1273
+ {(provided, snapshot) => (
1274
+ <li>
1275
+ <div className={snapshot.isDragging ? 'currently-dragging' : ''} style={getItemStyle(snapshot.isDragging, provided.draggableProps.style, sortableItemStyles)} ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
1276
+ {value}
1277
+ </div>
1278
+ </li>
1279
+ )}
1280
+ </Draggable>
1281
+ )
1282
+ })}
1283
+ {provided.placeholder}
1284
+ </ul>
1285
+ )}
1286
+ </Droppable>
1287
+ </DragDropContext>
1288
+ )}
1289
+ </fieldset>
1290
+ </>
1149
1291
  )
1150
1292
  })
1151
1293
 
@@ -1178,16 +1320,12 @@ const EditorPanel = props => {
1178
1320
  })
1179
1321
  }
1180
1322
 
1181
- useEffect(() => {
1182
- if (paletteName) handleEditorChanges('color', paletteName)
1183
- }, [paletteName]) // dont add handleEditorChanges as a dependency even if it requires
1184
-
1185
1323
  useEffect(() => {
1186
1324
  const parsedData = convertStateToConfig()
1187
1325
  const formattedData = JSON.stringify(parsedData, undefined, 2)
1188
1326
 
1189
1327
  setConfigTextbox(formattedData)
1190
- }, [state])
1328
+ }, [state]) // eslint-disable-line
1191
1329
 
1192
1330
  useEffect(() => {
1193
1331
  // Pass up to Editor if needed
@@ -1195,7 +1333,7 @@ const EditorPanel = props => {
1195
1333
  const newConfig = convertStateToConfig()
1196
1334
  setParentConfig(newConfig)
1197
1335
  }
1198
- }, [state])
1336
+ }, [state]) // eslint-disable-line
1199
1337
 
1200
1338
  let numberOfItemsLimit = 8
1201
1339
 
@@ -1232,6 +1370,24 @@ const EditorPanel = props => {
1232
1370
  )
1233
1371
  }
1234
1372
 
1373
+ const isLoadedFromUrl = state?.dataKey?.includes('http://') || state?.dataKey?.includes('https://')
1374
+
1375
+ // if isDebug = true, then try to set the Geography Col and Data col to reduce clicking
1376
+ const setGeoColumn = () => {
1377
+ // only for debug mode
1378
+ let geoColFound = columnsInData.includes(state.columns.geo.name)
1379
+ if (undefined !== isDebug && isDebug && !geoColFound) {
1380
+ // then try to set the x axis to appropriate value so we dont have to manually do it
1381
+ let mapcols = columnsInData[0]
1382
+ if (mapcols !== '') editColumn('geo', 'name', mapcols)
1383
+
1384
+ if (!state.columns.hasOwnProperty('primary') || undefined === state.columns.primary.name || '' === state.columns.primary.name || !state.columns.primary.name) {
1385
+ editColumn('primary', 'name', columnsInData[1]) // blindly picks first value col
1386
+ }
1387
+ }
1388
+ }
1389
+ if (isDebug) setGeoColumn()
1390
+
1235
1391
  return (
1236
1392
  <ErrorBoundary component='EditorPanel'>
1237
1393
  {state?.runtime?.editorErrorMessage.length > 0 && <Error />}
@@ -1349,7 +1505,8 @@ const EditorPanel = props => {
1349
1505
  }}
1350
1506
  >
1351
1507
  <option value='data'>Data</option>
1352
- <option value='us-geocode'>United States Geocode</option>
1508
+ {state.general.geoType === 'us-county' && <option value='us-geocode'>Geocode</option>}
1509
+ {state.general.geoType === 'world' && <option value='world-geocode'>Geocode</option>}
1353
1510
  <option value='navigation'>Navigation</option>
1354
1511
  {(state.general.geoType === 'world' || state.general.geoType === 'us') && <option value='bubble'>Bubble</option>}
1355
1512
  </select>
@@ -1403,9 +1560,11 @@ const EditorPanel = props => {
1403
1560
  <AccordionItemPanel>
1404
1561
  <TextField
1405
1562
  value={general.title}
1563
+ data-testid='title-input'
1406
1564
  updateField={updateField}
1407
1565
  section='general'
1408
1566
  fieldName='title'
1567
+ id='title'
1409
1568
  label='Title'
1410
1569
  placeholder='Map Title'
1411
1570
  tooltip={
@@ -1414,11 +1573,21 @@ const EditorPanel = props => {
1414
1573
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
1415
1574
  </Tooltip.Target>
1416
1575
  <Tooltip.Content>
1417
- <p>For accessibility reasons, you should enter a title even if you are not planning on displaying it.</p>
1576
+ <p>Title is required to set the name of the download file but can be hidden using the option below.</p>
1418
1577
  </Tooltip.Content>
1419
1578
  </Tooltip>
1420
1579
  }
1421
1580
  />
1581
+ <label className='checkbox'>
1582
+ <input
1583
+ type='checkbox'
1584
+ checked={state.general.showTitle || false}
1585
+ onChange={event => {
1586
+ handleEditorChanges('showTitle', event.target.checked)
1587
+ }}
1588
+ />
1589
+ <span className='edit-label'>Show Title</span>
1590
+ </label>
1422
1591
  <TextField
1423
1592
  value={general.superTitle || ''}
1424
1593
  updateField={updateField}
@@ -1491,6 +1660,18 @@ const EditorPanel = props => {
1491
1660
  }
1492
1661
  />
1493
1662
  {'us' === state.general.geoType && <TextField value={general.territoriesLabel} updateField={updateField} section='general' fieldName='territoriesLabel' label='Territories Label' placeholder='Territories' />}
1663
+ {'us' === state.general.geoType && (
1664
+ <label className='checkbox'>
1665
+ <input
1666
+ type='checkbox'
1667
+ checked={general.territoriesAlwaysShow || false}
1668
+ onChange={event => {
1669
+ handleEditorChanges('territoriesAlwaysShow', event.target.checked)
1670
+ }}
1671
+ />
1672
+ <span className='edit-label'>Show All Territories</span>
1673
+ </label>
1674
+ )}
1494
1675
  {/* <label className="checkbox mt-4">
1495
1676
  <input type="checkbox" checked={ state.general.showDownloadMediaButton } onChange={(event) => { handleEditorChanges("toggleDownloadMediaButton", event.target.checked) }} />
1496
1677
  <span className="edit-label">Enable Media Download</span>
@@ -1677,8 +1858,7 @@ const EditorPanel = props => {
1677
1858
  </label>
1678
1859
  </fieldset>
1679
1860
  )}
1680
-
1681
- {'us-geocode' === state.general.type && (
1861
+ {('us-geocode' === state.general.type || 'world-geocode' === state.general.type) && (
1682
1862
  <>
1683
1863
  <label>Latitude Column</label>
1684
1864
  <select
@@ -1905,35 +2085,42 @@ const EditorPanel = props => {
1905
2085
  </Tooltip>
1906
2086
  </span>
1907
2087
  </label>
1908
- {state.legend.additionalCategories && state.legend.additionalCategories.map((val, i) => (
1909
- <fieldset className='edit-block' key={val}>
1910
- <button
1911
- className='remove-column'
1912
- onClick={event => {
1913
- event.preventDefault()
1914
- const updatedAdditionaCategories = [...state.legend.additionalCategories];
1915
- updatedAdditionaCategories.splice(i, 1);
1916
- updateField('legend', null, 'additionalCategories', updatedAdditionaCategories);
1917
- }}
1918
- >
1919
- Remove
1920
- </button>
1921
- <label>
1922
- <span className='edit-label column-heading'>Category</span>
1923
- <TextField value={val} section="legend" subsection={null} fieldName="additionalCategories" updateField={(section, subsection, fieldName, value) => {
1924
- const updatedAdditionaCategories = [...state.legend.additionalCategories];
1925
- updatedAdditionaCategories[i] = value;
1926
- updateField(section, subsection, fieldName, updatedAdditionaCategories)
1927
- }} />
1928
- </label>
1929
- </fieldset>
1930
- ))}
2088
+ {state.legend.additionalCategories &&
2089
+ state.legend.additionalCategories.map((val, i) => (
2090
+ <fieldset className='edit-block' key={val}>
2091
+ <button
2092
+ className='remove-column'
2093
+ onClick={event => {
2094
+ event.preventDefault()
2095
+ const updatedAdditionaCategories = [...state.legend.additionalCategories]
2096
+ updatedAdditionaCategories.splice(i, 1)
2097
+ updateField('legend', null, 'additionalCategories', updatedAdditionaCategories)
2098
+ }}
2099
+ >
2100
+ Remove
2101
+ </button>
2102
+ <label>
2103
+ <span className='edit-label column-heading'>Category</span>
2104
+ <TextField
2105
+ value={val}
2106
+ section='legend'
2107
+ subsection={null}
2108
+ fieldName='additionalCategories'
2109
+ updateField={(section, subsection, fieldName, value) => {
2110
+ const updatedAdditionaCategories = [...state.legend.additionalCategories]
2111
+ updatedAdditionaCategories[i] = value
2112
+ updateField(section, subsection, fieldName, updatedAdditionaCategories)
2113
+ }}
2114
+ />
2115
+ </label>
2116
+ </fieldset>
2117
+ ))}
1931
2118
  <button
1932
2119
  className={'btn full-width'}
1933
2120
  onClick={event => {
1934
2121
  event.preventDefault()
1935
- const updatedAdditionaCategories = [...(state.legend.additionalCategories || [])];
1936
- updatedAdditionaCategories.push('');
2122
+ const updatedAdditionaCategories = [...(state.legend.additionalCategories || [])]
2123
+ updatedAdditionaCategories.push('')
1937
2124
  updateField('legend', null, 'additionalCategories', updatedAdditionaCategories)
1938
2125
  }}
1939
2126
  >
@@ -2016,6 +2203,18 @@ const EditorPanel = props => {
2016
2203
  <span className='edit-label'>Single Row Legend</span>
2017
2204
  </label>
2018
2205
  )}
2206
+ {/* always show */}
2207
+ {/*
2208
+ <label className='checkbox'>
2209
+ <input
2210
+ type='checkbox'
2211
+ checked={legend.showSpecialClassesLast}
2212
+ onChange={event => {
2213
+ handleEditorChanges('legendShowSpecialClassesLast', event.target.checked)
2214
+ }}
2215
+ />
2216
+ <span className='edit-label'>Show Special Classes Last</span>
2217
+ </label> */}
2019
2218
  {'category' !== legend.type && (
2020
2219
  <label className='checkbox'>
2021
2220
  <input type='checkbox' checked={legend.separateZero || false} onChange={event => handleEditorChanges('separateZero', event.target.checked)} />
@@ -2033,7 +2232,6 @@ const EditorPanel = props => {
2033
2232
  </label>
2034
2233
  )}
2035
2234
  {/* Temp Checkbox */}
2036
-
2037
2235
  {state.legend.type === 'equalnumber' && (
2038
2236
  <label className='checkbox mt-4'>
2039
2237
  <input
@@ -2197,7 +2395,7 @@ const EditorPanel = props => {
2197
2395
  <AccordionItemButton>Filters</AccordionItemButton>
2198
2396
  </AccordionItemHeading>
2199
2397
  <AccordionItemPanel>
2200
- {filtersJSX.length > 0 ? filtersJSX : <p style={{ textAlign: 'center' }}>There are currently no filters.</p>}
2398
+ {filtersJSX.length > 0 ? <MapFilters /> : <p style={{ textAlign: 'center' }}>There are currently no filters.</p>}
2201
2399
  <button
2202
2400
  className={'btn full-width'}
2203
2401
  onClick={event => {
@@ -2223,6 +2421,7 @@ const EditorPanel = props => {
2223
2421
  updateField={updateField}
2224
2422
  section='dataTable'
2225
2423
  fieldName='title'
2424
+ id='dataTableTitle'
2226
2425
  label='Data Table Title'
2227
2426
  placeholder='Data Table'
2228
2427
  tooltip={
@@ -2236,6 +2435,26 @@ const EditorPanel = props => {
2236
2435
  </Tooltip>
2237
2436
  }
2238
2437
  />
2438
+ <label className='checkbox'>
2439
+ <input
2440
+ type='checkbox'
2441
+ checked={state.dataTable.forceDisplay !== undefined ? state.dataTable.forceDisplay : !isDashboard}
2442
+ onChange={event => {
2443
+ handleEditorChanges('showDataTable', event.target.checked)
2444
+ }}
2445
+ />
2446
+ <span className='edit-label column-heading'>
2447
+ Show Data Table
2448
+ <Tooltip style={{ textTransform: 'none' }}>
2449
+ <Tooltip.Target>
2450
+ <Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
2451
+ </Tooltip.Target>
2452
+ <Tooltip.Content>
2453
+ <p>Data tables are required for 508 compliance. When choosing to hide this data table, replace with your own version.</p>
2454
+ </Tooltip.Content>
2455
+ </Tooltip>
2456
+ </span>
2457
+ </label>
2239
2458
  <TextField
2240
2459
  value={dataTable.indexLabel || ''}
2241
2460
  updateField={updateField}
@@ -2273,26 +2492,6 @@ const EditorPanel = props => {
2273
2492
  }
2274
2493
  type='textarea'
2275
2494
  />
2276
- <label className='checkbox'>
2277
- <input
2278
- type='checkbox'
2279
- checked={state.dataTable.forceDisplay !== undefined ? state.dataTable.forceDisplay : !isDashboard}
2280
- onChange={event => {
2281
- handleEditorChanges('showDataTable', event.target.checked)
2282
- }}
2283
- />
2284
- <span className='edit-label'>
2285
- Show Table
2286
- <Tooltip style={{ textTransform: 'none' }}>
2287
- <Tooltip.Target>
2288
- <Icon display='question' style={{ marginLeft: '0.5rem' }} />
2289
- </Tooltip.Target>
2290
- <Tooltip.Content>
2291
- <p>Data tables are required for 508 compliance. When choosing to hide this data table, replace with your own version.</p>
2292
- </Tooltip.Content>
2293
- </Tooltip>
2294
- </span>
2295
- </label>
2296
2495
  <label className='checkbox'>
2297
2496
  <input
2298
2497
  type='checkbox'
@@ -2314,16 +2513,30 @@ const EditorPanel = props => {
2314
2513
  />
2315
2514
  <span className='edit-label'>Map loads with data table expanded</span>
2316
2515
  </label>
2317
- <label className='checkbox'>
2318
- <input
2319
- type='checkbox'
2320
- checked={state.table.showDownloadUrl}
2321
- onChange={event => {
2322
- handleEditorChanges('toggleDataUrl', event.target.checked)
2323
- }}
2324
- />
2325
- <span className='edit-label'>Enable Link to Dataset</span>
2326
- </label>
2516
+ {isDashboard && (
2517
+ <label className='checkbox'>
2518
+ <input
2519
+ type='checkbox'
2520
+ checked={state.table.showDataTableLink}
2521
+ onChange={event => {
2522
+ handleEditorChanges('toggleDataTableLink', event.target.checked)
2523
+ }}
2524
+ />
2525
+ <span className='edit-label'>Show Data Table Name & Link</span>
2526
+ </label>
2527
+ )}
2528
+ {isLoadedFromUrl && (
2529
+ <label className='checkbox'>
2530
+ <input
2531
+ type='checkbox'
2532
+ checked={state.table.showDownloadUrl}
2533
+ onChange={event => {
2534
+ handleEditorChanges('toggleDataUrl', event.target.checked)
2535
+ }}
2536
+ />
2537
+ <span className='edit-label'>Show URL to Automatically Updated Data</span>
2538
+ </label>
2539
+ )}
2327
2540
  <label className='checkbox'>
2328
2541
  <input
2329
2542
  type='checkbox'
@@ -2332,7 +2545,7 @@ const EditorPanel = props => {
2332
2545
  handleEditorChanges('toggleDownloadButton', event.target.checked)
2333
2546
  }}
2334
2547
  />
2335
- <span className='edit-label'>Enable Download CSV Button</span>
2548
+ <span className='edit-label'>Show Download CSV Link</span>
2336
2549
  </label>
2337
2550
  {/* <label className='checkbox'>
2338
2551
  <input
@@ -2434,8 +2647,6 @@ const EditorPanel = props => {
2434
2647
  <span className='edit-label'>Show Title</span>
2435
2648
  </label>
2436
2649
 
2437
-
2438
-
2439
2650
  {'navigation' === state.general.type && (
2440
2651
  <label className='checkbox'>
2441
2652
  <input
@@ -2464,10 +2675,10 @@ const EditorPanel = props => {
2464
2675
  <span className='edit-label'>Map Color Palette</span>
2465
2676
  </label>
2466
2677
  {/* <InputCheckbox section="general" subsection="palette" fieldName='isReversed' size='small' label='Use selected palette in reverse order' updateField={updateField} value={isPaletteReversed} /> */}
2467
- <InputToggle type='3d' section='general' subsection='palette' fieldName='isReversed' size='small' label='Use selected palette in reverse order' updateField={updateField} value={isPaletteReversed} />
2678
+ <InputToggle type='3d' section='general' subsection='palette' fieldName='isReversed' size='small' label='Use selected palette in reverse order' updateField={updateField} value={state.general.palette.isReversed} />
2468
2679
  <span>Sequential</span>
2469
2680
  <ul className='color-palette'>
2470
- {filteredPallets.map(palette => {
2681
+ {sequential.map(palette => {
2471
2682
  const colorOne = {
2472
2683
  backgroundColor: colorPalettes[palette][2]
2473
2684
  }
@@ -2498,7 +2709,7 @@ const EditorPanel = props => {
2498
2709
  </ul>
2499
2710
  <span>Non-Sequential</span>
2500
2711
  <ul className='color-palette'>
2501
- {filteredQualitative.map(palette => {
2712
+ {nonSequential.map(palette => {
2502
2713
  const colorOne = {
2503
2714
  backgroundColor: colorPalettes[palette][2]
2504
2715
  }
@@ -2531,7 +2742,7 @@ const EditorPanel = props => {
2531
2742
  )
2532
2743
  })}
2533
2744
  </ul>
2534
- {'us-geocode' === state.general.type && (
2745
+ {('us-geocode' === state.general.type || 'world-geocode' === state.general.type) && (
2535
2746
  <label>
2536
2747
  Geocode Settings
2537
2748
  <TextField type='number' value={state.visual.geoCodeCircleSize} section='visual' max='10' fieldName='geoCodeCircleSize' label='Geocode Circle Size' updateField={updateField} />
@@ -2581,7 +2792,8 @@ const EditorPanel = props => {
2581
2792
  </label>
2582
2793
  )}
2583
2794
  {state.general.geoType === 'us' ||
2584
- (state.general.geoType === 'us-county' && (
2795
+ state.general.geoType === 'us-county' ||
2796
+ (state.general.geoType === 'world' && (
2585
2797
  <label>
2586
2798
  <span className='edit-label'>City Style</span>
2587
2799
  <select
@@ -2597,6 +2809,45 @@ const EditorPanel = props => {
2597
2809
  ))}
2598
2810
  </AccordionItemPanel>
2599
2811
  </AccordionItem>
2812
+ {/* HIDING FOR 4.23.4 */}
2813
+ {/* <AccordionItem>
2814
+ <AccordionItemHeading>
2815
+ <AccordionItemButton>Custom Map Layers</AccordionItemButton>
2816
+ </AccordionItemHeading>
2817
+ <AccordionItemPanel>
2818
+ {state.map.layers.length === 0 && <p>There are currently no layers.</p>}
2819
+
2820
+ {state.map.layers.map((layer, index) => {
2821
+ return (
2822
+ <>
2823
+ <Accordion allowZeroExpanded>
2824
+ <AccordionItem className='series-item map-layers-list'>
2825
+ <AccordionItemHeading className='series-item__title map-layers-list--title'>
2826
+ <AccordionItemButton>{`Layer ${index + 1}: ${layer.name}`}</AccordionItemButton>
2827
+ </AccordionItemHeading>
2828
+ <AccordionItemPanel>
2829
+ <div className='map-layers-panel'>
2830
+ <label htmlFor='layerName'>Layer Name:</label>
2831
+ <input type='text' name='layerName' value={layer.name} onChange={e => handleMapLayerName(e, index)} />
2832
+ <label htmlFor='layerFilename'>File:</label>
2833
+ <input type='text' name='layerFilename' value={layer.url} onChange={e => handleMapLayerUrl(e, index)} />
2834
+ <label htmlFor='layerNamespace'>TOPOJSON Namespace:</label>
2835
+ <input type='text' name='layerNamespace' value={layer.namespace} onChange={e => handleMapLayerNamespace(e, index)} />
2836
+ <label htmlFor='layerTooltip'>Tooltip:</label>
2837
+ <textarea name='layerTooltip' value={layer.tooltip} onChange={e => handleMapLayerTooltip(e, index)}></textarea>
2838
+ <button onClick={e => handleRemoveLayer(e, index)}>Remove Layer</button>
2839
+ </div>
2840
+ </AccordionItemPanel>
2841
+ </AccordionItem>
2842
+ </Accordion>
2843
+ </>
2844
+ )
2845
+ })}
2846
+ <button className={'btn full-width'} onClick={handleAddLayer}>
2847
+ Add Map Layer
2848
+ </button>
2849
+ </AccordionItemPanel>
2850
+ </AccordionItem> */}
2600
2851
  </Accordion>
2601
2852
  </form>
2602
2853
  <AdvancedEditor loadConfig={loadConfig} state={state} convertStateToConfig={convertStateToConfig} />