@cdc/map 4.23.4 → 4.23.5

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.
@@ -8,7 +8,7 @@
8
8
  "showSidebar": true,
9
9
  "showTitle": true,
10
10
  "showDownloadButton": true,
11
- "expandDataTable": false,
11
+ "expandDataTable": true,
12
12
  "backgroundColor": "#f5f5f5",
13
13
  "geoBorderColor": "darkGray",
14
14
  "territoriesLabel": "Territories",
@@ -142,13 +142,13 @@
142
142
  "data": [
143
143
  {
144
144
  "STATE": "AL",
145
- "Rate": 10,
145
+ "Rate": 1000,
146
146
  "Location": "Home",
147
147
  "URL": "https://www.cdc.gov/"
148
148
  },
149
149
  {
150
150
  "STATE": "AK",
151
- "Rate": 12,
151
+ "Rate": 1200,
152
152
  "Location": "Vehicle",
153
153
  "URL": "https://www.cdc.gov/"
154
154
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cdc/map",
3
- "version": "4.23.4",
3
+ "version": "4.23.5",
4
4
  "description": "React component for visualizing tabular data on a map of the United States or the world.",
5
5
  "moduleName": "CdcMap",
6
6
  "main": "dist/cdcmap",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "license": "Apache-2.0",
26
26
  "dependencies": {
27
- "@cdc/core": "^4.23.4",
27
+ "@cdc/core": "^4.23.5",
28
28
  "@emotion/core": "^10.0.28",
29
29
  "@emotion/react": "^11.1.5",
30
30
  "@hello-pangea/dnd": "^16.2.0",
@@ -51,5 +51,5 @@
51
51
  "react": "^18.2.0",
52
52
  "react-dom": "^18.2.0"
53
53
  },
54
- "gitHead": "dcd395d76f70b2d113f2b4c6fe50a52522655cd1"
54
+ "gitHead": "34add3436994ca3cf13e51f313add4d70377f53e"
55
55
  }
package/src/CdcMap.jsx CHANGED
@@ -8,6 +8,7 @@ import ResizeObserver from 'resize-observer-polyfill'
8
8
  // Third party
9
9
  import { Tooltip as ReactTooltip } from 'react-tooltip'
10
10
  import chroma from 'chroma-js'
11
+ import Papa from 'papaparse'
11
12
  import parse from 'html-react-parser'
12
13
  import 'react-tooltip/dist/react-tooltip.css'
13
14
 
@@ -34,6 +35,7 @@ import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
34
35
  import getViewport from '@cdc/core/helpers/getViewport'
35
36
  import Loading from '@cdc/core/components/Loading'
36
37
  import numberFromString from '@cdc/core/helpers/numberFromString'
38
+ import DataTable from '@cdc/core/components/DataTable' // Future: Lazy
37
39
 
38
40
  // Child Components
39
41
  import ConfigContext from './context'
@@ -42,7 +44,6 @@ import Modal from './components/Modal'
42
44
  import Sidebar from './components/Sidebar'
43
45
 
44
46
  import CountyMap from './components/CountyMap' // Future: Lazy
45
- import DataTable from './components/DataTable' // Future: Lazy
46
47
  import EditorPanel from './components/EditorPanel' // Future: Lazy
47
48
  import NavigationMenu from './components/NavigationMenu' // Future: Lazy
48
49
  import SingleStateMap from './components/SingleStateMap' // Future: Lazy
@@ -133,6 +134,8 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
133
134
  let legendMemo = useRef(new Map())
134
135
  let innerContainerRef = useRef()
135
136
 
137
+ if (isDebug) console.log('CdcMap state=', state) // eslint-disable-line
138
+
136
139
  useEffect(() => {
137
140
  try {
138
141
  if (filteredCountryCode) {
@@ -195,7 +198,6 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
195
198
  for (let entry of entries) {
196
199
  let { width, height } = entry.contentRect
197
200
  let newViewport = getViewport(entry.contentRect.width)
198
- let svgMarginWidth = 32
199
201
  let editorWidth = 350
200
202
 
201
203
  setCurrentViewport(newViewport)
@@ -770,9 +772,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
770
772
 
771
773
  if (hash) filters.fromHash = hash
772
774
 
773
- obj?.filters.forEach(({ columnName, label, active, values }, idx) => {
774
- if (undefined === columnName) return
775
-
775
+ obj?.filters.forEach(({ columnName, label, labels, queryParameter, orderedValues, active, values, type }, idx) => {
776
776
  let newFilter = runtimeFilters[idx]
777
777
 
778
778
  const sortAsc = (a, b) => {
@@ -783,20 +783,24 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
783
783
  return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
784
784
  }
785
785
 
786
- values = getUniqueValues(state.data, columnName)
786
+ if (type !== 'url') {
787
+ values = getUniqueValues(state.data, columnName)
787
788
 
788
- if (obj.filters[idx].order === 'asc') {
789
- values = values.sort(sortAsc)
790
- }
789
+ if (obj.filters[idx].order === 'asc') {
790
+ values = values.sort(sortAsc)
791
+ }
791
792
 
792
- if (obj.filters[idx].order === 'desc') {
793
- values = values.sort(sortDesc)
794
- }
793
+ if (obj.filters[idx].order === 'desc') {
794
+ values = values.sort(sortDesc)
795
+ }
795
796
 
796
- if (obj.filters[idx].order === 'cust') {
797
- if (obj.filters[idx]?.values.length > 0) {
798
- values = obj.filters[idx].values
797
+ if (obj.filters[idx].order === 'cust') {
798
+ if (obj.filters[idx]?.values.length > 0) {
799
+ values = obj.filters[idx].values
800
+ }
799
801
  }
802
+ } else {
803
+ values = values
800
804
  }
801
805
 
802
806
  if (undefined === newFilter) {
@@ -804,8 +808,12 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
804
808
  }
805
809
 
806
810
  newFilter.order = obj.filters[idx].order ? obj.filters[idx].order : 'asc'
811
+ newFilter.type = type
807
812
  newFilter.label = label ?? ''
808
813
  newFilter.columnName = columnName
814
+ newFilter.orderedValues = orderedValues
815
+ newFilter.queryParameter = queryParameter
816
+ newFilter.labels = labels
809
817
  newFilter.values = values
810
818
  handleSorting(newFilter)
811
819
  newFilter.active = active ?? values[0] // Default to first found value
@@ -862,8 +870,8 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
862
870
  // Filters
863
871
  if (filters?.length) {
864
872
  for (let i = 0; i < filters.length; i++) {
865
- const { columnName, active } = filters[i]
866
- if (String(row[columnName]) !== String(active)) return false // Bail out, not part of filter
873
+ const { columnName, active, type } = filters[i]
874
+ if (type !== 'url' && String(row[columnName]) !== String(active)) return false // Bail out, not part of filter
867
875
  }
868
876
  }
869
877
 
@@ -899,6 +907,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
899
907
  return ''
900
908
  }
901
909
 
910
+ // if string of letters like 'Home' then dont need to format as a number
902
911
  if (typeof value === 'string' && value.length > 0 && state.legend.type === 'equalnumber') {
903
912
  return value
904
913
  }
@@ -907,6 +916,17 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
907
916
 
908
917
  let columnObj = state.columns[columnName]
909
918
 
919
+ if (columnObj === undefined) {
920
+ // then use left axis config
921
+ columnObj = state.columns.primary
922
+ // NOTE: Left Value Axis uses different names
923
+ // so map them below so the code below works
924
+ // - copy commas to useCommas to work below
925
+ columnObj['useCommas'] = columnObj.commas
926
+ // - copy roundTo to roundToPlace to work below
927
+ columnObj['roundToPlace'] = columnObj.roundTo ? columnObj.roundTo : ''
928
+ }
929
+
910
930
  if (columnObj) {
911
931
  // If value is a number, apply specific formattings
912
932
  if (Number(value)) {
@@ -994,16 +1014,16 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
994
1014
  const column = state.columns[columnKey]
995
1015
 
996
1016
  if (true === column.tooltip) {
997
- let label = column.label.length > 0 ? column.label : ''
1017
+ let label = column.label?.length > 0 ? column.label : ''
998
1018
 
999
1019
  let value
1000
1020
 
1001
1021
  if (state.legend.specialClasses && state.legend.specialClasses.length && typeof state.legend.specialClasses[0] === 'object') {
1002
1022
  // THIS CODE SHOULD NOT ACT ON THE ENTIRE ROW OF KEYS BUT ONLY THE ONE KEY IN THE SPECIAL CLASS
1003
1023
  for (let i = 0; i < state.legend.specialClasses.length; i++) {
1004
- // DEV-3303 - Special Classes label in HOVERS should only apply to selected special class key
1024
+ // Special Classes label in HOVERS should only apply to selected special class key
1005
1025
  // - 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) {
1026
+ if (column.name === state.legend.specialClasses[i].key) {
1007
1027
  if (String(row[state.legend.specialClasses[i].key]) === state.legend.specialClasses[i].value) {
1008
1028
  value = displayDataAsText(state.legend.specialClasses[i].label, columnKey)
1009
1029
  break
@@ -1047,25 +1067,29 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1047
1067
  // - this function is used to prevent that and instead give the formatting that is wanted
1048
1068
  // Example: Desired city display in tooltip on map: "Inter-Tribal Indian Reservation"
1049
1069
  const titleCase = string => {
1050
- // if hyphen found, then split, uppercase each word, and put back together
1051
- if (string.includes('–') || string.includes('-')) {
1052
- let dashSplit = string.includes('–') ? string.split('–') : string.split('-') // determine hyphen or en dash to split on
1053
- let splitCharacter = string.includes('–') ? '–' : '-' // print hyphen or en dash later on.
1054
- let frontSplit = dashSplit[0]
1055
- .split(' ')
1056
- .map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())
1057
- .join(' ')
1058
- let backSplit = dashSplit[1]
1059
- .split(' ')
1060
- .map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())
1061
- .join(' ')
1062
- return frontSplit + splitCharacter + backSplit
1063
- } else {
1064
- // just return with each word uppercase
1065
- return string
1066
- .split(' ')
1067
- .map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())
1068
- .join(' ')
1070
+ // guard clause else error in editor
1071
+ if (!string) return
1072
+ if (string !== undefined) {
1073
+ // if hyphen found, then split, uppercase each word, and put back together
1074
+ if (string.includes('–') || string.includes('-')) {
1075
+ let dashSplit = string.includes('–') ? string.split('–') : string.split('-') // determine hyphen or en dash to split on
1076
+ let splitCharacter = string.includes('–') ? '–' : '-' // print hyphen or en dash later on.
1077
+ let frontSplit = dashSplit[0]
1078
+ .split(' ')
1079
+ .map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())
1080
+ .join(' ')
1081
+ let backSplit = dashSplit[1]
1082
+ .split(' ')
1083
+ .map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())
1084
+ .join(' ')
1085
+ return frontSplit + splitCharacter + backSplit
1086
+ } else {
1087
+ // just return with each word uppercase
1088
+ return string
1089
+ .split(' ')
1090
+ .map(word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase())
1091
+ .join(' ')
1092
+ }
1069
1093
  }
1070
1094
  }
1071
1095
 
@@ -1232,6 +1256,67 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1232
1256
  }
1233
1257
  }
1234
1258
 
1259
+ const reloadURLData = async () => {
1260
+ if (state.dataUrl) {
1261
+ const dataUrl = new URL(state.runtimeDataUrl || state.dataUrl)
1262
+ let qsParams = Object.fromEntries(new URLSearchParams(dataUrl.search))
1263
+
1264
+ let isUpdateNeeded = false
1265
+ state.filters.forEach(filter => {
1266
+ if (filter.type === 'url' && qsParams[filter.queryParameter] !== decodeURIComponent(filter.active)) {
1267
+ qsParams[filter.queryParameter] = filter.active
1268
+ isUpdateNeeded = true
1269
+ }
1270
+ })
1271
+
1272
+ if (!isUpdateNeeded) return
1273
+
1274
+ let dataUrlFinal = `${dataUrl.origin}${dataUrl.pathname}${Object.keys(qsParams)
1275
+ .map((param, i) => {
1276
+ let qs = i === 0 ? '?' : '&'
1277
+ qs += param + '='
1278
+ qs += qsParams[param]
1279
+ return qs
1280
+ })
1281
+ .join('')}`
1282
+
1283
+ let data
1284
+
1285
+ try {
1286
+ const regex = /(?:\.([^.]+))?$/
1287
+
1288
+ const ext = regex.exec(dataUrl.pathname)[1]
1289
+ if ('csv' === ext) {
1290
+ data = await fetch(dataUrlFinal)
1291
+ .then(response => response.text())
1292
+ .then(responseText => {
1293
+ const parsedCsv = Papa.parse(responseText, {
1294
+ header: true,
1295
+ dynamicTyping: true,
1296
+ skipEmptyLines: true
1297
+ })
1298
+ return parsedCsv.data
1299
+ })
1300
+ } else if ('json' === ext) {
1301
+ data = await fetch(dataUrlFinal).then(response => response.json())
1302
+ } else {
1303
+ data = []
1304
+ }
1305
+ } catch (e) {
1306
+ console.error(`Cannot parse URL: ${dataUrlFinal}`) // eslint-disable-line
1307
+ console.log(e) // eslint-disable-line
1308
+ data = []
1309
+ }
1310
+
1311
+ if (state.dataDescription) {
1312
+ data = transform.autoStandardize(data)
1313
+ data = transform.developerStandardize(data, state.dataDescription)
1314
+ }
1315
+
1316
+ setState({ ...state, runtimeDataUrl: dataUrlFinal, data })
1317
+ }
1318
+ }
1319
+
1235
1320
  const loadConfig = async configObj => {
1236
1321
  // Set loading flag
1237
1322
  if (!loading) setLoading(true)
@@ -1242,8 +1327,9 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1242
1327
  ...configObj
1243
1328
  }
1244
1329
 
1245
- // If a dataUrl property exists, always pull from that.
1246
- if (newState.dataUrl) {
1330
+ const urlFilters = newState.filters ? (newState.filters.filter(filter => filter.type === 'url').length > 0 ? true : false) : false
1331
+
1332
+ if (newState.dataUrl && !urlFilters) {
1247
1333
  if (newState.dataUrl[0] === '/') {
1248
1334
  newState.dataUrl = 'http://' + hostname + newState.dataUrl
1249
1335
  }
@@ -1282,8 +1368,8 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1282
1368
  addUIDs(newState, newState.columns.geo.name || newState.columns.geo.fips)
1283
1369
  }
1284
1370
 
1285
- if (newState.dataTable.forceDisplay === undefined) {
1286
- newState.dataTable.forceDisplay = !isDashboard
1371
+ if (newState.table.forceDisplay === undefined) {
1372
+ newState.table.forceDisplay = !isDashboard
1287
1373
  }
1288
1374
 
1289
1375
  validateFipsCodeLength(newState)
@@ -1338,16 +1424,16 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1338
1424
 
1339
1425
  // DEV-769 make "Data Table" both a required field and default value
1340
1426
  useEffect(() => {
1341
- if (state.dataTable?.title === '' || state.dataTable?.title === undefined) {
1427
+ if (state.table?.label === '' || state.table?.label === undefined) {
1342
1428
  setState({
1343
1429
  ...state,
1344
- dataTable: {
1345
- ...state.dataTable,
1430
+ table: {
1431
+ ...state.table,
1346
1432
  title: 'Data Table'
1347
1433
  }
1348
1434
  })
1349
1435
  }
1350
- }, [state.dataTable]) // eslint-disable-line
1436
+ }, [state.table]) // eslint-disable-line
1351
1437
 
1352
1438
  // When geo label override changes
1353
1439
  // - redo the tooltips
@@ -1408,6 +1494,10 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1408
1494
  }
1409
1495
  }, [runtimeData, state.legend.unified, state.legend.showSpecialClassesLast, state.legend.separateZero, state.general.equalNumberOptIn, state.legend.numberOfItems, state.legend.specialClasses]) // eslint-disable-line
1410
1496
 
1497
+ useEffect(() => {
1498
+ reloadURLData()
1499
+ }, [JSON.stringify(state.filters)])
1500
+
1411
1501
  if (config) {
1412
1502
  // eslint-disable-next-line react-hooks/rules-of-hooks
1413
1503
  useEffect(() => {
@@ -1416,14 +1506,14 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1416
1506
  }
1417
1507
 
1418
1508
  // Destructuring for more readable JSX
1419
- const { general, tooltips, dataTable } = state
1509
+ const { general, tooltips, table } = state
1420
1510
  let { title, subtext = '' } = general
1421
1511
 
1422
1512
  // if no title AND in editor then set a default
1423
1513
  if (isEditor) {
1424
1514
  if (!title || title === '') title = 'Map Title'
1425
1515
  }
1426
- if (!dataTable.title || dataTable.title === '') dataTable.title = 'Data Table'
1516
+ if (!table.label || table.label === '') table.label = 'Data Table'
1427
1517
 
1428
1518
  // Outer container classes
1429
1519
  let outerContainerClasses = ['cdc-open-viz-module', 'cdc-map-outer-container', currentViewport]
@@ -1475,7 +1565,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1475
1565
 
1476
1566
  if (!mapProps.data || !state.data) return <Loading />
1477
1567
 
1478
- const hasDataTable = state.runtime.editorErrorMessage.length === 0 && true === dataTable.forceDisplay && general.type !== 'navigation' && false === loading
1568
+ const hasDataTable = state.runtime.editorErrorMessage.length === 0 && true === table.forceDisplay && general.type !== 'navigation' && false === loading
1479
1569
 
1480
1570
  const handleMapTabbing = () => {
1481
1571
  let tabbingID
@@ -1596,7 +1686,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1596
1686
  {'navigation' === general.type && <NavigationMenu mapTabbingID={tabId} displayGeoName={displayGeoName} data={runtimeData} options={general} columns={state.columns} navigationHandler={val => navigationHandler(val)} />}
1597
1687
 
1598
1688
  {/* Link */}
1599
- {isDashboard && config.dataTable.forceDisplay && config.table.showDataTableLink ? tableLink : link && link}
1689
+ {isDashboard && config.table?.forceDisplay && config.table.showDataTableLink ? tableLink : link && link}
1600
1690
 
1601
1691
  {subtext.length > 0 && <p className='subtext'>{parse(subtext)}</p>}
1602
1692
 
@@ -1605,12 +1695,12 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1605
1695
  {state.general.showDownloadPdfButton && <CoveMediaControls.Button text='Download PDF' title='Download Chart as PDF' type='pdf' state={state} elementToCapture={imageId} />}
1606
1696
  </CoveMediaControls.Section>
1607
1697
 
1608
- {state.runtime.editorErrorMessage.length === 0 && true === dataTable.forceDisplay && general.type !== 'navigation' && false === loading && (
1698
+ {state.runtime.editorErrorMessage.length === 0 && true === table.forceDisplay && general.type !== 'navigation' && false === loading && (
1609
1699
  <DataTable
1610
- state={state}
1700
+ config={state}
1611
1701
  rawData={state.data}
1612
1702
  navigationHandler={navigationHandler}
1613
- expandDataTable={general.expandDataTable}
1703
+ expandDataTable={general.expandDataTable ? general.expandDataTable : table.expanded ? table.expanded : false}
1614
1704
  headerColor={general.headerColor}
1615
1705
  columns={state.columns}
1616
1706
  showDownloadButton={general.showDownloadButton}
@@ -1619,9 +1709,9 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1619
1709
  displayDataAsText={displayDataAsText}
1620
1710
  displayGeoName={displayGeoName}
1621
1711
  applyLegendToRow={applyLegendToRow}
1622
- tableTitle={dataTable.title}
1623
- indexTitle={dataTable.indexLabel}
1624
- mapTitle={general.title}
1712
+ tableTitle={table.label}
1713
+ indexTitle={table.indexLabel}
1714
+ vizTitle={general.title}
1625
1715
  viewport={currentViewport}
1626
1716
  formatLegendLocation={formatLegendLocation}
1627
1717
  setFilteredCountryCode={setFilteredCountryCode}
@@ -1631,6 +1721,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1631
1721
  innerContainerRef={innerContainerRef}
1632
1722
  outerContainerRef={outerContainerRef}
1633
1723
  imageRef={imageId}
1724
+ isDebug={isDebug}
1634
1725
  />
1635
1726
  )}
1636
1727
 
@@ -235,7 +235,7 @@ const CountyMap = props => {
235
235
  }
236
236
  }
237
237
 
238
- if (hoveredGeo) {
238
+ if (hoveredGeo && applyLegendToRow(hoveredGeo)) {
239
239
  tooltipRef.current.style.display = 'block'
240
240
  tooltipRef.current.style.top = e.clientY + 'px'
241
241
  tooltipRef.current.style.left = e.clientX + 'px'