@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
package/src/CdcMap.jsx CHANGED
@@ -37,7 +37,7 @@ import numberFromString from '@cdc/core/helpers/numberFromString'
37
37
 
38
38
  // Child Components
39
39
  import ConfigContext from './context'
40
- import Filters from './components/Filters'
40
+ import Filters, { useFilters } from '@cdc/core/components/Filters'
41
41
  import Modal from './components/Modal'
42
42
  import Sidebar from './components/Sidebar'
43
43
 
@@ -82,13 +82,13 @@ const hashObj = row => {
82
82
 
83
83
  return hash
84
84
  } catch (e) {
85
- console.error(e)
85
+ console.error('COVE: ', e) // eslint-disable-line
86
86
  }
87
87
  }
88
88
 
89
89
  const indexOfIgnoreType = (arr, item) => {
90
90
  for (let i = 0; i < arr.length; i++) {
91
- if (item == arr[i]) {
91
+ if (item === arr[i]) {
92
92
  return i
93
93
  }
94
94
  }
@@ -112,7 +112,7 @@ const getUniqueValues = (data, columnName) => {
112
112
  return Object.keys(result)
113
113
  }
114
114
 
115
- const CdcMap = ({ className, config, navigationHandler: customNavigationHandler, isDashboard = false, isEditor = false, configUrl, logo = null, setConfig, setSharedFilter, setSharedFilterValue, hostname = 'localhost:8080', link }) => {
115
+ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler, isDashboard = false, isEditor = false, isDebug = false, configUrl, logo = null, setConfig, setSharedFilter, setSharedFilterValue, hostname = 'localhost:8080', link }) => {
116
116
  const transform = new DataTransform()
117
117
  const [state, setState] = useState({ ...initialState })
118
118
  const [loading, setLoading] = useState(true)
@@ -127,7 +127,9 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
127
127
  const [coveLoadedHasRan, setCoveLoadedHasRan] = useState(false)
128
128
  const [container, setContainer] = useState()
129
129
  const [imageId, setImageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`) // eslint-disable-line
130
+ const [dimensions, setDimensions] = useState()
130
131
 
132
+ const { changeFilterActive, handleSorting } = useFilters({ config: state, setConfig: setState })
131
133
  let legendMemo = useRef(new Map())
132
134
  let innerContainerRef = useRef()
133
135
 
@@ -145,9 +147,9 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
145
147
  })
146
148
  }
147
149
  } catch (e) {
148
- console.error('Failed to set world map zoom.')
150
+ console.error('COVE: Failed to set world map zoom.') // eslint-disable-line
149
151
  }
150
- }, [filteredCountryCode])
152
+ }, [filteredCountryCode]) // eslint-disable-line
151
153
 
152
154
  useEffect(() => {
153
155
  setTimeout(() => {
@@ -159,7 +161,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
159
161
  setRuntimeData(tmpData)
160
162
  }
161
163
  }, 100)
162
- }, [filteredCountryCode])
164
+ }, [filteredCountryCode]) // eslint-disable-line
163
165
 
164
166
  useEffect(() => {
165
167
  if (state.mapPosition) {
@@ -167,16 +169,47 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
167
169
  }
168
170
  }, [state.mapPosition, setPosition])
169
171
 
172
+ const generateRuntimeLegendHash = () => {
173
+ return hashObj({
174
+ unified: state.legend.unified ?? false,
175
+ equalNumberOptIn: state.general.equalNumberOptIn ?? false,
176
+ specialClassesLast: state.legend.showSpecialClassesLast ?? false,
177
+ color: state.color,
178
+ customColors: state.customColors,
179
+ numberOfItems: state.legend.numberOfItems,
180
+ type: state.legend.type,
181
+ separateZero: state.legend.separateZero ?? false,
182
+ primary: state.columns.primary.name,
183
+ categoryValuesOrder: state.legend.categoryValuesOrder,
184
+ specialClasses: state.legend.specialClasses,
185
+ geoType: state.general.geoType,
186
+ data: state.data,
187
+ ...runtimeFilters,
188
+ filters: {
189
+ ...state.filters
190
+ }
191
+ })
192
+ }
193
+
170
194
  const resizeObserver = new ResizeObserver(entries => {
171
195
  for (let entry of entries) {
196
+ let { width, height } = entry.contentRect
172
197
  let newViewport = getViewport(entry.contentRect.width)
198
+ let svgMarginWidth = 32
199
+ let editorWidth = 350
173
200
 
174
201
  setCurrentViewport(newViewport)
202
+
203
+ if (isEditor) {
204
+ width = width - editorWidth
205
+ }
206
+ setDimensions([width, height])
175
207
  }
176
208
  })
177
209
 
178
210
  // Tag each row with a UID. Helps with filtering/placing geos. Not enumerable so doesn't show up in loops/console logs except when directly addressed ex row.uid
179
211
  // We are mutating state in place here (depending on where called) - but it's okay, this isn't used for rerender
212
+ // eslint-disable-next-line
180
213
  const addUIDs = useCallback((obj, fromColumn) => {
181
214
  obj.data.forEach(row => {
182
215
  let uid = null
@@ -225,6 +258,11 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
225
258
  const geoName = row[obj.columns.geo.name]
226
259
 
227
260
  uid = countryKeys.find(key => supportedCountries[key].includes(geoName))
261
+
262
+ // Cities
263
+ if (!uid && 'world-geocode' === state.general.type) {
264
+ uid = cityKeys.find(key => key === geoName?.toUpperCase())
265
+ }
228
266
  }
229
267
 
230
268
  // County Check
@@ -248,9 +286,10 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
248
286
  obj.data.fromColumn = fromColumn
249
287
  })
250
288
 
289
+ // eslint-disable-next-line
251
290
  const generateRuntimeLegend = useCallback((obj, runtimeData, hash) => {
252
291
  const newLegendMemo = new Map() // Reset memoization
253
- const primaryCol = obj.columns.primary.name,
292
+ let primaryCol = obj.columns.primary.name,
254
293
  isBubble = obj.general.type === 'bubble',
255
294
  categoricalCol = obj.columns.categorical ? obj.columns.categorical.name : undefined,
256
295
  type = obj.legend.type,
@@ -262,6 +301,8 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
262
301
  result.fromHash = hash
263
302
  }
264
303
 
304
+ result.runtimeDataHash = runtimeData.fromHash
305
+
265
306
  // Unified will based the legend off ALL of the data maps received. Otherwise, it will use
266
307
  let dataSet = obj.legend.unified ? obj.data : Object.values(runtimeData)
267
308
 
@@ -457,6 +498,11 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
457
498
  }
458
499
 
459
500
  legendMemo.current = newLegendMemo
501
+
502
+ result.forEach((row, i) => {
503
+ row.bin = i // set bin number to index
504
+ })
505
+
460
506
  return result
461
507
  }
462
508
 
@@ -536,6 +582,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
536
582
  let min = removedRows[0][primaryCol],
537
583
  max = removedRows[removedRows.length - 1][primaryCol]
538
584
 
585
+ // eslint-disable-next-line
539
586
  removedRows.forEach(row => {
540
587
  newLegendMemo.set(hashObj(row), result.length)
541
588
  })
@@ -574,6 +621,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
574
621
  breaks.unshift(0)
575
622
  }
576
623
 
624
+ // eslint-disable-next-line array-callback-return
577
625
  breaks.map((item, index) => {
578
626
  const setMin = index => {
579
627
  let min = breaks[index]
@@ -646,7 +694,6 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
646
694
  }
647
695
 
648
696
  // Equal Interval
649
-
650
697
  if (type === 'equalinterval' && dataSet?.length !== 0) {
651
698
  if (!dataSet || dataSet.length === 0) {
652
699
  setState({
@@ -695,11 +742,29 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
695
742
  })
696
743
 
697
744
  legendMemo.current = newLegendMemo
745
+
746
+ //----------
747
+ // DEV-784
748
+ // before returning the legend result
749
+ // add property for bin number and set to index location
750
+ result.forEach((row, i) => {
751
+ row.bin = i // set bin number to index
752
+ })
753
+
754
+ // Move all special legend items from "Special Classes" to the end of the legend
755
+ if (state.legend.showSpecialClassesLast) {
756
+ let specialRows = result.filter(d => d.special === true)
757
+ let otherRows = result.filter(d => !d.special)
758
+ result = [...otherRows, ...specialRows]
759
+ }
760
+ //-----------
761
+
698
762
  return result
699
763
  })
700
764
 
765
+ // eslint-disable-next-line
701
766
  const generateRuntimeFilters = useCallback((obj, hash, runtimeFilters) => {
702
- if (undefined === obj.filters || obj.filters.length === 0) return []
767
+ if (typeof obj === 'undefined' || undefined === obj.filters || obj.filters.length === 0) return []
703
768
 
704
769
  let filters = []
705
770
 
@@ -738,10 +803,13 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
738
803
  newFilter = {}
739
804
  }
740
805
 
806
+ newFilter.order = obj.filters[idx].order ? obj.filters[idx].order : 'asc'
741
807
  newFilter.label = label ?? ''
742
808
  newFilter.columnName = columnName
743
809
  newFilter.values = values
744
- newFilter.active = active || values[0] // Default to first found value
810
+ handleSorting(newFilter)
811
+ newFilter.active = active ?? values[0] // Default to first found value
812
+ newFilter.filterStyle = obj.filters[idx].filterStyle ? obj.filters[idx].filterStyle : 'dropdown'
745
813
 
746
814
  filters.push(newFilter)
747
815
  })
@@ -750,21 +818,21 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
750
818
  })
751
819
 
752
820
  // Calculates what's going to be displayed on the map and data table at render.
821
+ // eslint-disable-next-line
753
822
  const generateRuntimeData = useCallback((obj, filters, hash, test) => {
754
823
  try {
755
824
  const result = {}
756
825
 
757
- if (hash) {
758
- // Adding property this way prevents it from being enumerated
759
- Object.defineProperty(result, 'fromHash', {
760
- value: hash
761
- })
762
- }
826
+ // Adding property this way prevents it from being enumerated
827
+ Object.defineProperty(result, 'fromHash', {
828
+ value: hash
829
+ })
763
830
 
831
+ addUIDs(obj, obj.columns.geo.name)
764
832
  obj.data.forEach(row => {
765
833
  if (test) {
766
- console.log('object', obj)
767
- console.log('row', row)
834
+ console.log('object', obj) // eslint-disable-line
835
+ console.log('row', row) // eslint-disable-line
768
836
  }
769
837
 
770
838
  if (undefined === row.uid) return false // No UID for this row, we can't use for mapping
@@ -807,7 +875,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
807
875
 
808
876
  return result
809
877
  } catch (e) {
810
- console.error(e)
878
+ console.error('COVE: ', e) // eslint-disable-line
811
879
  }
812
880
  })
813
881
 
@@ -816,7 +884,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
816
884
  resizeObserver.observe(node)
817
885
  }
818
886
  setContainer(node)
819
- }, [])
887
+ }, []) // eslint-disable-line
820
888
 
821
889
  const mapSvg = useRef(null)
822
890
 
@@ -826,33 +894,6 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
826
894
  }
827
895
  }
828
896
 
829
- const changeFilterActive = async (idx, activeValue) => {
830
- // Reset active legend toggles
831
- resetLegendToggles()
832
-
833
- try {
834
- const isEmpty = obj => {
835
- return Object.keys(obj).length === 0
836
- }
837
-
838
- let filters = [...runtimeFilters]
839
-
840
- filters[idx] = { ...filters[idx] }
841
- filters[idx].active = activeValue
842
-
843
- const newData = generateRuntimeData(state, filters)
844
-
845
- // throw an error if newData is empty
846
- if (isEmpty(newData)) throw new Error('Cove Filter Error: No runtime data to set for this filter')
847
-
848
- // set the runtime filters and data
849
- setRuntimeData(newData)
850
- setRuntimeFilters(filters)
851
- } catch (e) {
852
- console.error(e.message)
853
- }
854
- }
855
-
856
897
  const displayDataAsText = (value, columnName) => {
857
898
  if (value === null || value === '' || value === undefined) {
858
899
  return ''
@@ -897,6 +938,8 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
897
938
  return formattedValue
898
939
  }
899
940
 
941
+ // this is passed DOWN into the various components
942
+ // then they do a lookup based on the bin number as index into here (TT)
900
943
  const applyLegendToRow = rowObj => {
901
944
  try {
902
945
  if (!rowObj) throw new Error('COVE: No rowObj in applyLegendToRow')
@@ -911,13 +954,17 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
911
954
  if (legendMemo.current.has(hash)) {
912
955
  let idx = legendMemo.current.get(hash)
913
956
  if (runtimeLegend[idx]?.disabled) return false
914
- return generateColorsArray(runtimeLegend[idx]?.color, runtimeLegend[idx]?.special)
957
+
958
+ // DEV-784 changed to use bin prop to get color instead of idx
959
+ // bc we re-order legend when showSpecialClassesLast is checked
960
+ let legendBinColor = runtimeLegend.find(o => o.bin === idx)?.color
961
+ return generateColorsArray(legendBinColor, runtimeLegend[idx]?.special)
915
962
  }
916
963
 
917
964
  // Fail state
918
965
  return generateColorsArray()
919
966
  } catch (e) {
920
- console.error(e)
967
+ console.error('COVE: ', e) // eslint-disable-line
921
968
  }
922
969
  }
923
970
 
@@ -927,6 +974,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
927
974
 
928
975
  // Adds geo label, ie State: Georgia
929
976
  let stateOrCounty = state.general.geoType === 'us' ? 'State: ' : state.general.geoType === 'us-county' || state.general.geoType === 'single-state' ? 'County: ' : ''
977
+
930
978
  // check the override
931
979
  stateOrCounty = state.general.geoLabelOverride !== '' ? state.general.geoLabelOverride + ': ' : stateOrCounty
932
980
 
@@ -939,7 +987,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
939
987
 
940
988
  toolTipText += !state.general.hideGeoColumnInTooltip ? `<strong>${stateOrCounty}${displayGeoName(geoName)}</strong>` : `<strong>${displayGeoName(geoName)}</strong>`
941
989
 
942
- if (('data' === state.general.type || state.general.type === 'bubble' || state.general.type === 'us-geocode') && undefined !== row) {
990
+ if (('data' === state.general.type || state.general.type === 'bubble' || state.general.type === 'us-geocode' || state.general.type === 'world-geocode') && undefined !== row) {
943
991
  toolTipText += `<dl>`
944
992
 
945
993
  Object.keys(state.columns).forEach(columnKey => {
@@ -951,10 +999,15 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
951
999
  let value
952
1000
 
953
1001
  if (state.legend.specialClasses && state.legend.specialClasses.length && typeof state.legend.specialClasses[0] === 'object') {
1002
+ // THIS CODE SHOULD NOT ACT ON THE ENTIRE ROW OF KEYS BUT ONLY THE ONE KEY IN THE SPECIAL CLASS
954
1003
  for (let i = 0; i < state.legend.specialClasses.length; i++) {
955
- if (String(row[state.legend.specialClasses[i].key]) === state.legend.specialClasses[i].value) {
956
- value = displayDataAsText(state.legend.specialClasses[i].label, columnKey)
957
- break
1004
+ // DEV-3303 - Special Classes label in HOVERS should only apply to selected special class key
1005
+ // - you have to ALSO check that the key matches - putting here otherwise the if stmt too long
1006
+ if (columnKey === state.legend.specialClasses[i].key) {
1007
+ if (String(row[state.legend.specialClasses[i].key]) === state.legend.specialClasses[i].value) {
1008
+ value = displayDataAsText(state.legend.specialClasses[i].label, columnKey)
1009
+ break
1010
+ }
958
1011
  }
959
1012
  }
960
1013
  }
@@ -1024,6 +1077,8 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1024
1077
  delete legendItem.disabled
1025
1078
  })
1026
1079
 
1080
+ newLegend.runtimeDataHash = runtimeLegend.runtimeDataHash
1081
+
1027
1082
  setRuntimeLegend(newLegend)
1028
1083
  }
1029
1084
 
@@ -1100,6 +1155,17 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1100
1155
  setSharedFilter(state.uid, value)
1101
1156
  }
1102
1157
 
1158
+ // If world-geocode map zoom to geo point
1159
+ if ('world-geocode' === state.general.type) {
1160
+ let lat = value[state.columns.latitude.name]
1161
+ let long = value[state.columns.longitude.name]
1162
+
1163
+ setState({
1164
+ ...state,
1165
+ mapPosition: { coordinates: [long, lat], zoom: 3 }
1166
+ })
1167
+ }
1168
+
1103
1169
  // If modals are set or we are on a mobile viewport, display modal
1104
1170
  if (window.matchMedia('(any-hover: none)').matches || 'click' === state.tooltips.appearanceType) {
1105
1171
  setModal({
@@ -1131,7 +1197,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1131
1197
  }
1132
1198
 
1133
1199
  const handleMapAriaLabels = (state = '', testing = false) => {
1134
- if (testing) console.log(`handleMapAriaLabels Testing On: ${state}`)
1200
+ if (testing) console.log(`handleMapAriaLabels Testing On: ${state}`) // eslint-disable-line
1135
1201
  try {
1136
1202
  if (!state.general.geoType) throw Error('handleMapAriaLabels: no geoType found in state')
1137
1203
  let ariaLabel = ''
@@ -1162,7 +1228,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1162
1228
 
1163
1229
  return ariaLabel
1164
1230
  } catch (e) {
1165
- console.error(e.message)
1231
+ console.error('COVE: ', e.message) // eslint-disable-line
1166
1232
  }
1167
1233
  }
1168
1234
 
@@ -1247,28 +1313,28 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1247
1313
  // Initial load
1248
1314
  useEffect(() => {
1249
1315
  init()
1250
- }, [])
1316
+ }, []) // eslint-disable-line
1251
1317
 
1252
1318
  useEffect(() => {
1253
1319
  if (state && !coveLoadedHasRan && container) {
1254
1320
  publish('cove_loaded', { config: state })
1255
1321
  setCoveLoadedHasRan(true)
1256
1322
  }
1257
- }, [state, container])
1323
+ }, [state, container]) // eslint-disable-line
1258
1324
 
1259
1325
  useEffect(() => {
1260
1326
  if (state.data) {
1261
1327
  let newData = generateRuntimeData(state)
1262
1328
  setRuntimeData(newData)
1263
1329
  }
1264
- }, [state.general.statePicked])
1330
+ }, [state.general.statePicked]) // eslint-disable-line
1265
1331
 
1266
1332
  useEffect(() => {
1267
1333
  // When geotype changes - add UID
1268
1334
  if (state.data && state.columns.geo.name) {
1269
1335
  addUIDs(state, state.columns.geo.name)
1270
1336
  }
1271
- }, [state])
1337
+ }, [state]) // eslint-disable-line
1272
1338
 
1273
1339
  // DEV-769 make "Data Table" both a required field and default value
1274
1340
  useEffect(() => {
@@ -1281,13 +1347,13 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1281
1347
  }
1282
1348
  })
1283
1349
  }
1284
- }, [state.dataTable])
1350
+ }, [state.dataTable]) // eslint-disable-line
1285
1351
 
1286
1352
  // When geo label override changes
1287
1353
  // - redo the tooltips
1288
1354
  useEffect(() => {
1289
1355
  applyTooltipsToGeo()
1290
- }, [state.general.geoLabelOverride])
1356
+ }, [state.general.geoLabelOverride]) // eslint-disable-line
1291
1357
 
1292
1358
  useEffect(() => {
1293
1359
  // UID
@@ -1307,76 +1373,57 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1307
1373
  }
1308
1374
  }
1309
1375
 
1310
- const hashLegend = hashObj({
1311
- color: state.color,
1312
- customColors: state.customColors,
1313
- numberOfItems: state.legend.numberOfItems,
1314
- type: state.legend.type,
1315
- separateZero: state.legend.separateZero ?? false,
1316
- categoryValuesOrder: state.legend.categoryValuesOrder,
1317
- specialClasses: state.legend.specialClasses,
1318
- geoType: state.general.geoType,
1319
- data: state.data,
1320
- ...runtimeLegend,
1321
- ...runtimeFilters
1322
- })
1376
+ const hashLegend = generateRuntimeLegendHash()
1323
1377
 
1324
1378
  const hashData = hashObj({
1379
+ data: state.data,
1325
1380
  columns: state.columns,
1326
1381
  geoType: state.general.geoType,
1327
1382
  type: state.general.type,
1328
1383
  geo: state.columns.geo.name,
1329
1384
  primary: state.columns.primary.name,
1330
- data: state.data,
1331
- ...runtimeFilters,
1332
1385
  mapPosition: state.mapPosition,
1333
1386
  ...runtimeFilters
1334
1387
  })
1335
1388
 
1336
1389
  // Data
1337
- let newRuntimeData
1338
1390
  if (hashData !== runtimeData.fromHash && state.data?.fromColumn) {
1339
1391
  const newRuntimeData = generateRuntimeData(state, filters || runtimeFilters, hashData)
1340
1392
  setRuntimeData(newRuntimeData)
1393
+ } else {
1394
+ if (hashLegend !== runtimeLegend.fromHash && undefined === runtimeData.init) {
1395
+ const legend = generateRuntimeLegend(state, runtimeData, hashLegend)
1396
+ setRuntimeLegend(legend)
1397
+ }
1341
1398
  }
1342
-
1343
- // Legend
1344
- if (hashLegend !== runtimeLegend.fromHash && (undefined === runtimeData.init || newRuntimeData)) {
1345
- const legend = generateRuntimeLegend(state, newRuntimeData || runtimeData, hashLegend)
1346
- setRuntimeLegend(legend)
1347
- }
1348
- }, [state])
1399
+ }, [state]) // eslint-disable-line
1349
1400
 
1350
1401
  useEffect(() => {
1351
- const hashLegend = hashObj({
1352
- color: state.color,
1353
- customColors: state.customColors,
1354
- numberOfItems: state.legend.numberOfItems,
1355
- type: state.legend.type,
1356
- separateZero: state.legend.separateZero ?? false,
1357
- categoryValuesOrder: state.legend.categoryValuesOrder,
1358
- specialClasses: state.legend.specialClasses,
1359
- geoType: state.general.geoType,
1360
- data: state.data
1361
- })
1402
+ const hashLegend = generateRuntimeLegendHash()
1362
1403
 
1363
1404
  // Legend - Update when runtimeData does
1364
1405
  if (hashLegend !== runtimeLegend.fromHash && undefined === runtimeData.init) {
1365
- const legend = generateRuntimeLegend(state, runtimeData)
1406
+ const legend = generateRuntimeLegend(state, runtimeData, hashLegend)
1366
1407
  setRuntimeLegend(legend)
1367
1408
  }
1368
- }, [runtimeData])
1409
+ }, [runtimeData, state.legend.unified, state.legend.showSpecialClassesLast, state.legend.separateZero, state.general.equalNumberOptIn, state.legend.numberOfItems, state.legend.specialClasses]) // eslint-disable-line
1369
1410
 
1370
1411
  if (config) {
1371
1412
  // eslint-disable-next-line react-hooks/rules-of-hooks
1372
1413
  useEffect(() => {
1373
1414
  loadConfig(config)
1374
- }, [config.data])
1415
+ }, [config.data]) // eslint-disable-line
1375
1416
  }
1376
1417
 
1377
1418
  // Destructuring for more readable JSX
1378
1419
  const { general, tooltips, dataTable } = state
1379
- const { title = '', subtext = '' } = general
1420
+ let { title, subtext = '' } = general
1421
+
1422
+ // if no title AND in editor then set a default
1423
+ if (isEditor) {
1424
+ if (!title || title === '') title = 'Map Title'
1425
+ }
1426
+ if (!dataTable.title || dataTable.title === '') dataTable.title = 'Data Table'
1380
1427
 
1381
1428
  // Outer container classes
1382
1429
  let outerContainerClasses = ['cdc-open-viz-module', 'cdc-map-outer-container', currentViewport]
@@ -1421,7 +1468,9 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1421
1468
  handleMapAriaLabels,
1422
1469
  runtimeFilters,
1423
1470
  setRuntimeFilters,
1424
- innerContainerRef
1471
+ innerContainerRef,
1472
+ currentViewport,
1473
+ isDebug
1425
1474
  }
1426
1475
 
1427
1476
  if (!mapProps.data || !state.data) return <Loading />
@@ -1452,16 +1501,35 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1452
1501
 
1453
1502
  const tabId = handleMapTabbing()
1454
1503
 
1504
+ // this only shows in Dashboard config mode and only if Show Table is also set
1505
+ const tableLink = (
1506
+ <a href={`#data-table-${state.general.dataKey}`} className='margin-left-href'>
1507
+ {state.general.dataKey} (Go to Table)
1508
+ </a>
1509
+ )
1510
+
1455
1511
  return (
1456
1512
  <ConfigContext.Provider value={mapProps}>
1457
1513
  <div className={outerContainerClasses.join(' ')} ref={outerContainerRef} data-download-id={imageId}>
1458
- {isEditor && <EditorPanel isDashboard={isDashboard} state={state} setState={setState} loadConfig={loadConfig} setParentConfig={setConfig} setRuntimeFilters={setRuntimeFilters} runtimeFilters={runtimeFilters} runtimeLegend={runtimeLegend} columnsInData={Object.keys(state.data[0])} />}
1514
+ {isEditor && (
1515
+ <EditorPanel
1516
+ isDashboard={isDashboard}
1517
+ isDebug={isDebug}
1518
+ state={state}
1519
+ setState={setState}
1520
+ loadConfig={loadConfig}
1521
+ setParentConfig={setConfig}
1522
+ setRuntimeFilters={setRuntimeFilters}
1523
+ runtimeFilters={runtimeFilters}
1524
+ runtimeLegend={runtimeLegend}
1525
+ columnsInData={Object.keys(state.data[0])}
1526
+ changeFilterActive={changeFilterActive}
1527
+ />
1528
+ )}
1459
1529
  {!runtimeData.init && (general.type === 'navigation' || runtimeLegend) && (
1460
1530
  <section className={`cdc-map-inner-container ${currentViewport}`} aria-label={'Map: ' + title} ref={innerContainerRef}>
1461
- {!window.matchMedia('(any-hover: none)').matches && 'hover' === tooltips.appearanceType &&
1462
- <ReactTooltip id="tooltip" variant="light" float={true} className={`${tooltips.capitalizeLabels ? 'capitalize tooltip' : 'tooltip'}`} />
1463
- }
1464
- {state.general.title && (
1531
+ {!window.matchMedia('(any-hover: none)').matches && 'hover' === tooltips.appearanceType && <ReactTooltip id='tooltip' variant='light' float={true} className={`${tooltips.capitalizeLabels ? 'capitalize tooltip' : 'tooltip'}`} />}
1532
+ {title && (
1465
1533
  <header className={general.showTitle === true ? 'visible' : 'hidden'} {...(!general.showTitle || !state.general.title ? { 'aria-hidden': true } : { 'aria-hidden': false })}>
1466
1534
  {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
1467
1535
  <div role='heading' className={'map-title ' + general.headerColor} tabIndex='0' aria-level='2'>
@@ -1470,11 +1538,10 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1470
1538
  </div>
1471
1539
  </header>
1472
1540
  )}
1473
- {general.introText &&
1474
- <section className='introText'>{parse(general.introText)}</section>
1475
- }
1541
+ {general.introText && <section className='introText'>{parse(general.introText)}</section>}
1476
1542
 
1477
- <Filters />
1543
+ {/* prettier-ignore */}
1544
+ {state?.filters?.length > 0 && <Filters config={state} setConfig={setState} filteredData={runtimeFilters} setFilteredData={setRuntimeFilters} dimensions={dimensions} />}
1478
1545
 
1479
1546
  <div
1480
1547
  role='button'
@@ -1528,7 +1595,8 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1528
1595
 
1529
1596
  {'navigation' === general.type && <NavigationMenu mapTabbingID={tabId} displayGeoName={displayGeoName} data={runtimeData} options={general} columns={state.columns} navigationHandler={val => navigationHandler(val)} />}
1530
1597
 
1531
- {link && link}
1598
+ {/* Link */}
1599
+ {isDashboard && config.dataTable.forceDisplay && config.table.showDataTableLink ? tableLink : link && link}
1532
1600
 
1533
1601
  {subtext.length > 0 && <p className='subtext'>{parse(subtext)}</p>}
1534
1602