@cdc/map 4.24.2 → 4.24.3

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.
package/src/CdcMap.tsx CHANGED
@@ -15,12 +15,13 @@ import 'react-tooltip/dist/react-tooltip.css'
15
15
  // Helpers
16
16
  import { publish } from '@cdc/core/helpers/events'
17
17
  import coveUpdateWorker from '@cdc/core/helpers/coveUpdateWorker'
18
+ import { getQueryStringFilterValue } from '@cdc/core/helpers/queryStringUtils'
18
19
  import Title from '@cdc/core/components/ui/Title'
19
20
 
20
21
  // Data
21
22
  import { countryCoordinates } from './data/country-coordinates'
22
23
  import { supportedStates, supportedTerritories, supportedCountries, supportedCounties, supportedCities, supportedStatesFipsCodes, stateFipsToTwoDigit, supportedRegions } from './data/supported-geos'
23
- import colorPalettes from '../../core/data/colorPalettes'
24
+ import colorPalettes from '@cdc/core/data/colorPalettes'
24
25
  import initialState from './data/initial-state'
25
26
 
26
27
  // Assets
@@ -37,6 +38,7 @@ import { DataTransform } from '@cdc/core/helpers/DataTransform'
37
38
  import MediaControls from '@cdc/core/components/MediaControls'
38
39
  import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
39
40
  import getViewport from '@cdc/core/helpers/getViewport'
41
+ import isDomainExternal from '@cdc/core/helpers/isDomainExternal'
40
42
  import Loading from '@cdc/core/components/Loading'
41
43
  import numberFromString from '@cdc/core/helpers/numberFromString'
42
44
  import DataTable from '@cdc/core/components/DataTable' // Future: Lazy
@@ -53,6 +55,7 @@ import UsaMap from './components/UsaMap' // Future: Lazy
53
55
  import WorldMap from './components/WorldMap' // Future: Lazy
54
56
  import useTooltip from './hooks/useTooltip'
55
57
  import { isSolrCsv, isSolrJson } from '@cdc/core/helpers/isSolr'
58
+ import SkipTo from '@cdc/core/components/elements/SkipTo'
56
59
 
57
60
  // Data props
58
61
  const stateKeys = Object.keys(supportedStates)
@@ -132,9 +135,11 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
132
135
  const [container, setContainer] = useState()
133
136
  const [imageId, setImageId] = useState(`cove-${Math.random().toString(16).slice(-4)}`) // eslint-disable-line
134
137
  const [dimensions, setDimensions] = useState()
138
+ const legendRef = useRef(null)
135
139
 
136
140
  const { changeFilterActive, handleSorting } = useFilters({ config: state, setConfig: setState })
137
141
  let legendMemo = useRef(new Map())
142
+ let legendSpecialClassLastMemo = useRef(new Map())
138
143
  let innerContainerRef = useRef()
139
144
 
140
145
  if (isDebug) console.log('CdcMap state=', state) // eslint-disable-line
@@ -303,6 +308,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
303
308
  // eslint-disable-next-line
304
309
  const generateRuntimeLegend = useCallback((obj, runtimeData, hash) => {
305
310
  const newLegendMemo = new Map() // Reset memoization
311
+ const newLegendSpecialClassLastMemo = new Map() // Reset bin memoization
306
312
  let primaryCol = obj.columns.primary.name,
307
313
  isBubble = obj.general.type === 'bubble',
308
314
  categoricalCol = obj.columns.categorical ? obj.columns.categorical.name : undefined,
@@ -524,6 +530,13 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
524
530
  result = [...otherRows, ...specialRows]
525
531
  }
526
532
 
533
+ const assignSpecialClassLastIndex = (value, key) => {
534
+ const newIndex = result.findIndex(d => d.bin === value)
535
+ newLegendSpecialClassLastMemo.set(key, newIndex)
536
+ }
537
+ newLegendMemo.forEach(assignSpecialClassLastIndex)
538
+ legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
539
+
527
540
  return result
528
541
  }
529
542
 
@@ -775,6 +788,13 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
775
788
  }
776
789
  //-----------
777
790
 
791
+ const assignSpecialClassLastIndex = (value, key) => {
792
+ const newIndex = result.findIndex(d => d.bin === value)
793
+ newLegendSpecialClassLastMemo.set(key, newIndex)
794
+ }
795
+ newLegendMemo.forEach(assignSpecialClassLastIndex)
796
+ legendSpecialClassLastMemo.current = newLegendSpecialClassLastMemo
797
+
778
798
  return result
779
799
  })
780
800
 
@@ -786,7 +806,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
786
806
 
787
807
  if (hash) filters.fromHash = hash
788
808
 
789
- obj?.filters.forEach(({ columnName, label, labels, queryParameter, orderedValues, active, values, type, showDropdown }, idx) => {
809
+ obj?.filters.forEach(({ columnName, label, labels, queryParameter, orderedValues, active, values, type, showDropdown, setByQueryParameter }, idx) => {
790
810
  let newFilter = runtimeFilters[idx]
791
811
 
792
812
  const sortAsc = (a, b) => {
@@ -829,6 +849,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
829
849
  newFilter.queryParameter = queryParameter
830
850
  newFilter.labels = labels
831
851
  newFilter.values = values
852
+ newFilter.setByQueryParameter = setByQueryParameter
832
853
  handleSorting(newFilter)
833
854
  newFilter.active = active ?? values[0] // Default to first found value
834
855
  newFilter.filterStyle = obj.filters[idx].filterStyle ? obj.filters[idx].filterStyle : 'dropdown'
@@ -988,7 +1009,13 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
988
1009
 
989
1010
  if (legendMemo.current.has(hash)) {
990
1011
  let idx = legendMemo.current.get(hash)
991
- if (runtimeLegend[idx]?.disabled) return false
1012
+ let disabledIdx = idx
1013
+
1014
+ if (state.legend.showSpecialClassesLast) {
1015
+ disabledIdx = legendSpecialClassLastMemo.current.get(hash)
1016
+ }
1017
+
1018
+ if (runtimeLegend[disabledIdx]?.disabled) return false
992
1019
 
993
1020
  // changed to use bin prop to get color instead of idx
994
1021
  // bc we re-order legend when showSpecialClassesLast is checked
@@ -1135,11 +1162,19 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1135
1162
 
1136
1163
  if (state.columns.hasOwnProperty('navigate') && row[state.columns.navigate.name]) {
1137
1164
  toolTipText.push(
1138
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
1139
- <ul className='navigation-link' key='modal-navigation-link' onClick={() => navigationHandler(row[state.columns.navigate.name])}>
1165
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions,jsx-a11y/anchor-is-valid
1166
+ <a
1167
+ href='#'
1168
+ className='navigation-link'
1169
+ key='modal-navigation-link'
1170
+ onClick={e => {
1171
+ e.preventDefault()
1172
+ navigationHandler(row[state.columns.navigate.name])
1173
+ }}
1174
+ >
1140
1175
  {state.tooltips.linkLabel}
1141
- <ExternalIcon className='inline-icon ml-1' />
1142
- </ul>
1176
+ {isDomainExternal(row[state.columns.navigate.name]) && <ExternalIcon className='inline-icon ml-1' />}
1177
+ </a>
1143
1178
  )
1144
1179
  }
1145
1180
  }
@@ -1305,8 +1340,6 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1305
1340
  data = transform.developerStandardize(data, state.dataDescription)
1306
1341
  }
1307
1342
 
1308
- console.log('data', data)
1309
-
1310
1343
  setState({ ...state, runtimeDataUrl: dataUrlFinal, data })
1311
1344
  }
1312
1345
  }
@@ -1449,6 +1482,12 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1449
1482
  filters = generateRuntimeFilters(state, hashFilters, runtimeFilters)
1450
1483
 
1451
1484
  if (filters) {
1485
+ filters.forEach((filter, index) => {
1486
+ const queryStringFilterValue = getQueryStringFilterValue(filter)
1487
+ if (queryStringFilterValue) {
1488
+ filters[index].active = queryStringFilterValue
1489
+ }
1490
+ })
1452
1491
  setRuntimeFilters(filters)
1453
1492
  }
1454
1493
  }
@@ -1582,21 +1621,21 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1582
1621
 
1583
1622
  // 1) skip to legend
1584
1623
  if (general.showSidebar) {
1585
- tabbingID = '#legend'
1624
+ tabbingID = 'legend'
1586
1625
  }
1587
1626
 
1588
1627
  // 2) skip to data table if it exists and not a navigation map
1589
1628
  if (hasDataTable && !general.showSidebar) {
1590
- tabbingID = `#dataTableSection__${Date.now()}`
1629
+ tabbingID = `dataTableSection__${Date.now()}`
1591
1630
  }
1592
1631
 
1593
1632
  // 3) if it's a navigation map skip to the dropdown.
1594
1633
  if (state.general.type === 'navigation') {
1595
- tabbingID = `#dropdown-${Date.now()}`
1634
+ tabbingID = `dropdown-${Date.now()}`
1596
1635
  }
1597
1636
 
1598
1637
  // 4) handle other options
1599
- return tabbingID || '#!'
1638
+ return tabbingID || '!'
1600
1639
  }
1601
1640
 
1602
1641
  const tabId = handleMapTabbing()
@@ -1614,9 +1653,6 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1614
1653
  {isEditor && <EditorPanel />}
1615
1654
  {!runtimeData.init && (general.type === 'navigation' || runtimeLegend) && (
1616
1655
  <section className={`cdc-map-inner-container ${currentViewport}`} aria-label={'Map: ' + title} ref={innerContainerRef}>
1617
- {!window.matchMedia('(any-hover: none)').matches && 'hover' === tooltips.appearanceType && (
1618
- <ReactTooltip id='tooltip' float={true} className={`${tooltips.capitalizeLabels ? 'capitalize tooltip' : 'tooltip'}`} style={{ background: `rgba(255,255,255, ${state.tooltips.opacity / 100})`, color: 'black' }} />
1619
- )}
1620
1656
  {/* prettier-ignore */}
1621
1657
  <Title
1622
1658
  title={title}
@@ -1624,16 +1660,15 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1624
1660
  config={config}
1625
1661
  classes={['map-title', general.showTitle === true ? 'visible' : 'hidden', `${general.headerColor}`]}
1626
1662
  />
1627
- <a id='skip-geo-container' className='cdcdataviz-sr-only-focusable' href={tabId}>
1628
- Skip Over Map Container
1629
- </a>
1663
+ <SkipTo skipId={tabId} skipMessage='Skip Over Map Container' />
1664
+
1630
1665
  {general.introText && <section className='introText'>{parse(general.introText)}</section>}
1631
1666
 
1632
1667
  {/* prettier-ignore */}
1633
1668
  {state?.filters?.length > 0 && <Filters config={state} setConfig={setState} filteredData={runtimeFilters} setFilteredData={setRuntimeFilters} dimensions={dimensions} />}
1634
1669
 
1635
1670
  <div
1636
- role='button'
1671
+ role='region'
1637
1672
  tabIndex='0'
1638
1673
  className={mapContainerClasses.join(' ')}
1639
1674
  onClick={e => closeModal(e)}
@@ -1658,7 +1693,7 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1658
1693
  )}
1659
1694
  </section>
1660
1695
 
1661
- {general.showSidebar && 'navigation' !== general.type && <Legend />}
1696
+ {general.showSidebar && 'navigation' !== general.type && <Legend ref={legendRef} />}
1662
1697
  </div>
1663
1698
 
1664
1699
  {'navigation' === general.type && <NavigationMenu mapTabbingID={tabId} displayGeoName={displayGeoName} data={runtimeData} options={general} columns={state.columns} navigationHandler={val => navigationHandler(val)} />}
@@ -1712,6 +1747,10 @@ const CdcMap = ({ className, config, navigationHandler: customNavigationHandler,
1712
1747
  <div aria-live='assertive' className='cdcdataviz-sr-only'>
1713
1748
  {accessibleStatus}
1714
1749
  </div>
1750
+
1751
+ {!window.matchMedia('(any-hover: none)').matches && 'hover' === tooltips.appearanceType && (
1752
+ <ReactTooltip id='tooltip' float={true} className={`${tooltips.capitalizeLabels ? 'capitalize tooltip' : 'tooltip'}`} style={{ background: `rgba(255,255,255, ${state.tooltips.opacity / 100})`, color: 'black' }} />
1753
+ )}
1715
1754
  </div>
1716
1755
  </ConfigContext.Provider>
1717
1756
  )
@@ -39,6 +39,7 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
39
39
  const circle = (
40
40
  <>
41
41
  <circle
42
+ tabIndex={-1}
42
43
  key={`circle-${countryName.replace(' ', '')}`}
43
44
  className={`bubble country--${countryName}`}
44
45
  cx={Number(projection(coordinates[1], coordinates[0])[0]) || 0} // || 0 handles error on loads where the data isn't ready
@@ -67,6 +68,7 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
67
68
 
68
69
  {state.visual.extraBubbleBorder && (
69
70
  <circle
71
+ tabIndex={-1}
70
72
  key={`circle-${countryName.replace(' ', '')}`}
71
73
  className='bubble'
72
74
  cx={Number(projection(coordinates[1], coordinates[0])[0]) || 0} // || 0 handles error on loads where the data isn't ready
@@ -95,7 +97,11 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
95
97
  </>
96
98
  )
97
99
 
98
- return <g key={`group-${countryName.replace(' ', '')}`}>{circle}</g>
100
+ return (
101
+ <g key={`group-${countryName.replace(' ', '')}`} tabIndex={-1}>
102
+ {circle}
103
+ </g>
104
+ )
99
105
  })
100
106
  return countries
101
107
  }
@@ -132,6 +138,7 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
132
138
  const circle = (
133
139
  <>
134
140
  <circle
141
+ tabIndex={-1}
135
142
  key={`circle-${stateName.replace(' ', '')}`}
136
143
  className='bubble'
137
144
  cx={projection(coordinates)[0] || 0} // || 0 handles error on loads where the data isn't ready
@@ -159,6 +166,7 @@ export const BubbleList = ({ data: dataImport, state, projection, applyLegendToR
159
166
  />
160
167
  {state.visual.extraBubbleBorder && (
161
168
  <circle
169
+ tabIndex={-1}
162
170
  key={`circle-${stateName.replace(' ', '')}`}
163
171
  className='bubble'
164
172
  cx={projection(coordinates)[0] || 0} // || 0 handles error on loads where the data isn't ready
@@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'
3
3
  import { jsx } from '@emotion/react'
4
4
  import { supportedCities } from '../data/supported-geos'
5
5
  import { scaleLinear } from 'd3-scale'
6
+ import { GlyphStar, GlyphTriangle, GlyphDiamond, GlyphSquare, GlyphCircle } from '@visx/glyph'
6
7
 
7
8
  const CityList = ({ data, state, geoClickHandler, applyTooltipsToGeo, displayGeoName, applyLegendToRow, projection, titleCase, setSharedFilterValue, isFilterValueSupported }) => {
8
9
  const [citiesData, setCitiesData] = useState({})
@@ -60,18 +61,18 @@ const CityList = ({ data, state, geoClickHandler, applyTooltipsToGeo, displayGeo
60
61
  fillOpacity: state.general.type === 'bubble' ? 0.4 : 1
61
62
  }
62
63
 
63
- const circle = <circle cx={0} cy={0} r={state.general.type === 'bubble' ? size(geoData[state.columns.primary.name]) : radius} title='Click for more information' onClick={() => geoClickHandler(cityDisplayName, geoData)} data-tooltip-id='tooltip' data-tooltip-html={toolTip} {...additionalProps} />
64
-
65
64
  const pin = (
66
65
  <path
67
66
  className='marker'
68
67
  d='M0,0l-8.8-17.7C-12.1-24.3-7.4-32,0-32h0c7.4,0,12.1,7.7,8.8,14.3L0,0z'
69
- title='Click for more information'
68
+ title='Select for more information'
70
69
  onClick={() => geoClickHandler(cityDisplayName, geoData)}
71
- strokeWidth={2}
72
- stroke={'black'}
73
70
  data-tooltip-id='tooltip'
74
71
  data-tooltip-html={toolTip}
72
+ transform={`scale(${radius / 9})`}
73
+ stroke={state.general.geoBorderColor === 'sameAsBackground' ? '#ffffff' : '#000000'}
74
+ strokeWidth={'2px'}
75
+ tabIndex='-1'
75
76
  {...additionalProps}
76
77
  />
77
78
  )
@@ -114,10 +115,61 @@ const CityList = ({ data, state, geoClickHandler, applyTooltipsToGeo, displayGeo
114
115
  styles.cursor = 'pointer'
115
116
  }
116
117
 
118
+ const shapeProps = {
119
+ onClick: () => geoClickHandler(cityDisplayName, geoData),
120
+ size: state.general.type === 'bubble' ? size(geoData[state.columns.primary.name]) : radius * 30,
121
+ title: 'Select for more information',
122
+ 'data-tooltip-id': 'tooltip',
123
+ 'data-tooltip-html': toolTip,
124
+ stroke: state.general.geoBorderColor === 'sameAsBackground' ? '#ffffff' : '#000000',
125
+ strokeWidth: '2px',
126
+ tabIndex: -1,
127
+ ...additionalProps
128
+ }
129
+
130
+ const cityStyleShapes = {
131
+ circle: <GlyphCircle {...shapeProps} />,
132
+ pin: pin,
133
+ square: <GlyphSquare {...shapeProps} />,
134
+ diamond: <GlyphDiamond {...shapeProps} />,
135
+ star: <GlyphStar {...shapeProps} />,
136
+ triangle: <GlyphTriangle {...shapeProps} />
137
+ }
138
+
139
+ const { additionalCityStyles } = state.visual || []
140
+ const cityStyle = Object.values(data)
141
+ .filter(d => additionalCityStyles.some(style => String(d[style.column]) === String(style.value)))
142
+ .map(d => {
143
+ const conditionsMatched = additionalCityStyles.find(style => String(d[style.column]) === String(style.value))
144
+ return { ...conditionsMatched, ...d }
145
+ })
146
+ .find(item => {
147
+ return Object.keys(item).find(key => item[key] === city)
148
+ })
149
+
150
+ if (cityStyle !== undefined && cityStyle.shape) {
151
+ if (!geoData?.[state.columns.longitude.name] && !geoData?.[state.columns.latitude.name] && city && supportedCities[city.toUpperCase()]) {
152
+ let translate = `translate(${projection(supportedCities[city.toUpperCase()])})`
153
+ return (
154
+ <g key={i} transform={translate} style={styles} className='geo-point' tabIndex={-1}>
155
+ {cityStyleShapes[cityStyle.shape.toLowerCase()]}
156
+ </g>
157
+ )
158
+ }
159
+
160
+ if (geoData?.[state.columns.longitude.name] && geoData?.[state.columns.latitude.name]) {
161
+ const coords = [Number(geoData?.[state.columns.longitude.name]), Number(geoData?.[state.columns.latitude.name])]
162
+ let translate = `translate(${projection(coords)})`
163
+ return (
164
+ <g key={i} transform={translate} style={styles} className='geo-point' tabIndex={-1}>
165
+ {cityStyleShapes[cityStyle.shape.toLowerCase()]}
166
+ </g>
167
+ )
168
+ }
169
+ }
117
170
  return (
118
- <g key={i} transform={transform} style={styles} className='geo-point'>
119
- {state.visual.cityStyle === 'circle' && circle}
120
- {state.visual.cityStyle === 'pin' && pin}
171
+ <g key={i} transform={transform} style={styles} className='geo-point' tabIndex={-1}>
172
+ {cityStyleShapes[state.visual.cityStyle.toLowerCase()]}
121
173
  </g>
122
174
  )
123
175
  })
@@ -7,6 +7,7 @@ import Icon from '@cdc/core/components/ui/Icon'
7
7
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
8
8
  import LegendCircle from '@cdc/core/components/LegendCircle'
9
9
  import MediaControls from '@cdc/core/components/MediaControls'
10
+ import SkipTo from '@cdc/core/components/elements/SkipTo'
10
11
 
11
12
  import Loading from '@cdc/core/components/Loading'
12
13
 
@@ -193,9 +194,7 @@ const DataTable = props => {
193
194
  {state.general.showDownloadButton && <DownloadButton />}
194
195
  </MediaControls.Section>
195
196
  <section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
196
- <a id='skip-nav' className='cdcdataviz-sr-only-focusable' href={`#${skipId}`}>
197
- Skip Navigation or Skip to Content
198
- </a>
197
+ <SkipTo skipId={skipId} skipMessage='Skip Data Table' />
199
198
  <div
200
199
  className={expanded ? 'data-table-heading' : 'collapsed data-table-heading'}
201
200
  onClick={() => {
@@ -229,7 +228,7 @@ const DataTable = props => {
229
228
  return (
230
229
  <th
231
230
  key={`col-header-${column}`}
232
- tabIndex='0'
231
+ tabIndex={0}
233
232
  title={text}
234
233
  role='columnheader'
235
234
  scope='col'
@@ -245,9 +244,7 @@ const DataTable = props => {
245
244
  {...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
246
245
  >
247
246
  {text}
248
- <button>
249
- <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
250
- </button>
247
+ <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} order`}</span>
251
248
  </th>
252
249
  )
253
250
  })}
@@ -298,6 +295,9 @@ const DataTable = props => {
298
295
  </table>
299
296
  </div>
300
297
  </section>
298
+ <div id={skipId} className='cdcdataviz-sr-only'>
299
+ Skipped data table.
300
+ </div>
301
301
  </ErrorBoundary>
302
302
  )
303
303
  }