@cdc/core 4.25.7 → 4.25.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +29 -8
  2. package/components/DataTable/DataTable.tsx +63 -11
  3. package/components/DataTable/DataTableStandAlone.tsx +4 -1
  4. package/components/DataTable/components/ChartHeader.tsx +58 -9
  5. package/components/DataTable/components/ExpandCollapse.tsx +21 -1
  6. package/components/DataTable/components/MapHeader.tsx +35 -7
  7. package/components/DataTable/data-table.css +6 -0
  8. package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
  9. package/components/DataTable/helpers/mapCellMatrix.tsx +19 -1
  10. package/components/DownloadButton.tsx +42 -13
  11. package/components/EditorPanel/DataTableEditor.tsx +10 -1
  12. package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
  13. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +411 -0
  14. package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
  15. package/components/ErrorBoundary.jsx +3 -1
  16. package/components/Filters/Filters.tsx +35 -11
  17. package/components/Filters/components/Tabs.tsx +1 -0
  18. package/components/Footnotes/FootnotesStandAlone.tsx +2 -1
  19. package/components/Legend/Legend.Gradient.tsx +3 -6
  20. package/components/LegendShape.tsx +121 -3
  21. package/components/{MediaControls.jsx → MediaControls.tsx} +80 -16
  22. package/components/PaletteConversionModal.tsx +87 -0
  23. package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
  24. package/components/PaletteSelector/PaletteSelector.css +51 -0
  25. package/components/PaletteSelector/PaletteSelector.tsx +112 -0
  26. package/components/PaletteSelector/index.ts +2 -0
  27. package/components/RichTooltip/RichTooltip.tsx +1 -0
  28. package/components/Table/Table.tsx +3 -1
  29. package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
  30. package/components/_stories/DataTable.stories.tsx +1 -1
  31. package/components/_stories/Filters.stories.tsx +1 -1
  32. package/components/_stories/Footnotes.stories.tsx +1 -1
  33. package/components/_stories/Inputs.stories.tsx +1 -1
  34. package/components/_stories/MultiSelect.stories.tsx +3 -3
  35. package/components/_stories/NestedDropdown.stories.tsx +1 -1
  36. package/components/_stories/Table.stories.tsx +1 -1
  37. package/components/elements/_stories/Button.stories.tsx +1 -1
  38. package/components/elements/_stories/Card.stories.tsx +1 -1
  39. package/components/inputs/InputToggle.tsx +2 -0
  40. package/components/managers/DataDesigner.tsx +10 -9
  41. package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
  42. package/components/ui/Tooltip.tsx +2 -1
  43. package/components/ui/_stories/Accordion.stories.tsx +1 -1
  44. package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
  45. package/components/ui/_stories/Colors.stories.tsx +330 -0
  46. package/components/ui/_stories/IconGallery.stories.tsx +316 -0
  47. package/components/ui/_stories/Title.stories.tsx +1 -1
  48. package/contexts/EditorContext.ts +18 -0
  49. package/contexts/editor.actions.ts +28 -0
  50. package/contexts/editor.reducer.ts +94 -0
  51. package/data/chartColorPalettes.ts +118 -0
  52. package/data/colorPalettes.ts +9 -0
  53. package/data/mapColorPalettes.ts +45 -0
  54. package/data/sharedPalettes.ts +50 -0
  55. package/dist/cove-main.css +14 -13
  56. package/dist/cove-main.css.map +1 -1
  57. package/generateViteConfig.js +80 -0
  58. package/helpers/addValuesToFilters.ts +2 -3
  59. package/helpers/cloneConfig.ts +31 -0
  60. package/helpers/configDataHelpers.ts +128 -0
  61. package/helpers/configHelpers.ts +27 -0
  62. package/helpers/constants.ts +5 -2
  63. package/helpers/cove/number.ts +6 -2
  64. package/helpers/coveUpdateWorker.ts +15 -3
  65. package/helpers/events.ts +32 -0
  66. package/helpers/filterColorPalettes.ts +152 -0
  67. package/helpers/generateColorsArray.ts +13 -0
  68. package/helpers/getColorPaletteVersion.ts +33 -0
  69. package/helpers/getPaletteAccessor.ts +18 -0
  70. package/helpers/markupProcessor.ts +205 -0
  71. package/helpers/metrics/helpers.ts +75 -0
  72. package/helpers/metrics/types.ts +82 -0
  73. package/helpers/metrics/utils.ts +34 -0
  74. package/helpers/palettes/colorDistributions.ts +56 -0
  75. package/helpers/palettes/migratePaletteName.ts +150 -0
  76. package/helpers/palettes/standardizePaletteNames.ts +77 -0
  77. package/helpers/palettes/utils.ts +267 -0
  78. package/helpers/queryStringUtils.ts +13 -0
  79. package/helpers/testing.ts +345 -0
  80. package/helpers/tests/addValuesToFilters.test.ts +1 -2
  81. package/helpers/tests/generateColorsArray.test.ts +24 -0
  82. package/helpers/tests/markupProcessor.test.ts +538 -0
  83. package/helpers/tests/testStandaloneBuild.ts +44 -0
  84. package/helpers/useMarkupVariables.ts +31 -0
  85. package/helpers/vegaConfig.ts +0 -1
  86. package/helpers/ver/4.24.10.ts +2 -1
  87. package/helpers/ver/4.24.11.ts +2 -1
  88. package/helpers/ver/4.24.3.ts +2 -1
  89. package/helpers/ver/4.24.4.ts +2 -1
  90. package/helpers/ver/4.24.5.ts +2 -1
  91. package/helpers/ver/4.24.7.ts +2 -1
  92. package/helpers/ver/4.24.9.ts +2 -1
  93. package/helpers/ver/4.25.1.ts +2 -1
  94. package/helpers/ver/4.25.10.ts +36 -0
  95. package/helpers/ver/4.25.3.ts +2 -1
  96. package/helpers/ver/4.25.4.ts +2 -1
  97. package/helpers/ver/4.25.6.ts +2 -1
  98. package/helpers/ver/4.25.7.ts +2 -1
  99. package/helpers/ver/4.25.8.ts +62 -0
  100. package/helpers/ver/4.25.9.ts +293 -0
  101. package/helpers/ver/tests/4.25.10.test.ts +204 -0
  102. package/helpers/ver/tests/4.25.8.test.ts +86 -0
  103. package/helpers/ver/tests/4.25.9.test.ts +51 -0
  104. package/helpers/viewports.ts +2 -0
  105. package/hooks/useColorPalette.ts +79 -0
  106. package/package.json +12 -4
  107. package/styles/_button-section.scss +0 -2
  108. package/styles/_global.scss +7 -5
  109. package/styles/base.scss +8 -5
  110. package/styles/v2/components/button.scss +4 -3
  111. package/styles/v2/components/editor.scss +2 -1
  112. package/styles/v2/layout/_data-table.scss +3 -2
  113. package/styles/v2/themes/_color-definitions.scss +18 -17
  114. package/testBuild.js +0 -0
  115. package/testing-setup.js +32 -0
  116. package/types/ForecastingSeriesKey.ts +0 -1
  117. package/types/MarkupInclude.ts +6 -1
  118. package/types/MarkupVariable.ts +19 -0
  119. package/types/Series.ts +4 -0
  120. package/types/Table.ts +1 -0
  121. package/types/VizFilter.ts +1 -0
  122. package/vitest.config.ts +16 -0
  123. package/components/ui/_stories/Colors.stories.mdx +0 -220
  124. package/components/ui/_stories/IconGallery.stories.mdx +0 -14
  125. package/data/colorPalettes.js +0 -171
  126. package/helpers/events.js +0 -14
  127. package/helpers/formatConfigBeforeSave.ts +0 -135
  128. package/helpers/tests/formatConfigBeforeSave.test.ts +0 -68
@@ -3,12 +3,17 @@ import MapIcon from '../../assets/map-folded.svg'
3
3
  import ChartIcon from '../../assets/icon-chart-bar.svg'
4
4
  import MarkupIncludeIcon from '../../assets/icon-code.svg'
5
5
  import { FilterFunction, JsonEditor, UpdateFunction } from 'json-edit-react'
6
- import { formatConfigBeforeSave as stripConfig } from '../../helpers/formatConfigBeforeSave'
7
6
  import './advanced-editor-styles.css'
8
7
  import _ from 'lodash'
9
8
  import Tooltip from '../ui/Tooltip'
10
9
 
11
- export const AdvancedEditor = ({ loadConfig, config, convertStateToConfig, onExpandCollapse = () => {} }) => {
10
+ export const AdvancedEditor = ({
11
+ loadConfig,
12
+ config,
13
+ convertStateToConfig,
14
+ stripConfig = config => config,
15
+ onExpandCollapse = () => {}
16
+ }) => {
12
17
  const [advancedToggle, _setAdvancedToggle] = useState(false)
13
18
  const [configTextboxValue, setConfigTextbox] = useState<Record<string, any>>({})
14
19
  const setAdvancedToggle = val => {
@@ -26,13 +31,29 @@ export const AdvancedEditor = ({ loadConfig, config, convertStateToConfig, onExp
26
31
  }
27
32
 
28
33
  useEffect(() => {
29
- let parsedConfig = stripConfig(config)
30
- if (config.type !== 'dashboard') {
31
- parsedConfig = convertStateToConfig()
34
+ // Only process config when advanced editor is open to improve performance
35
+ if (advancedToggle) {
36
+ let parsedConfig = stripConfig(config)
37
+ if (config.type !== 'dashboard') {
38
+ parsedConfig = convertStateToConfig()
39
+ }
40
+
41
+ setConfigTextbox(parsedConfig)
32
42
  }
43
+ }, [config, advancedToggle])
33
44
 
34
- setConfigTextbox(parsedConfig)
35
- }, [config])
45
+ // Initialize config when advanced editor is first opened
46
+ const handleToggleOpen = () => {
47
+ if (!advancedToggle) {
48
+ // Process config only when opening for the first time
49
+ let parsedConfig = stripConfig(config)
50
+ if (config.type !== 'dashboard') {
51
+ parsedConfig = convertStateToConfig()
52
+ }
53
+ setConfigTextbox(parsedConfig)
54
+ }
55
+ setAdvancedToggle(!advancedToggle)
56
+ }
36
57
 
37
58
  const typeLookup = {
38
59
  chart: ['Charts', 'https://www.cdc.gov/cove/index.html', <ChartIcon />],
@@ -58,7 +79,7 @@ export const AdvancedEditor = ({ loadConfig, config, convertStateToConfig, onExp
58
79
  </div>
59
80
  </a>
60
81
  <div className='advanced'>
61
- <span className='advanced-toggle-link' onClick={() => setAdvancedToggle(!advancedToggle)}>
82
+ <span className='advanced-toggle-link' onClick={handleToggleOpen}>
62
83
  <span>{advancedToggle ? `— ` : `+ `}</span>Advanced Options
63
84
  </span>
64
85
  {advancedToggle && (
@@ -1,4 +1,4 @@
1
- import { useEffect, useState, useMemo } from 'react'
1
+ import { useEffect, useState, useMemo, useRef } from 'react'
2
2
  import { timeParse } from 'd3-time-format'
3
3
 
4
4
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
@@ -52,9 +52,13 @@ export type DataTableProps = {
52
52
  vizTitle?: string
53
53
  // determines if columns should be wrapped in the table
54
54
  wrapColumns?: boolean
55
+ interactionLabel?: string
56
+ // Map-specific props (optional)
57
+ legendMemo?: React.MutableRefObject<Map<any, any>>
58
+ legendSpecialClassLastMemo?: React.MutableRefObject<Map<any, any>>
59
+ runtimeLegend?: any
55
60
  }
56
61
 
57
- /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
58
62
  const DataTable = (props: DataTableProps) => {
59
63
  const {
60
64
  columns,
@@ -71,7 +75,8 @@ const DataTable = (props: DataTableProps) => {
71
75
  tableTitle,
72
76
  viewport,
73
77
  vizTitle,
74
- wrapColumns
78
+ wrapColumns,
79
+ interactionLabel = ''
75
80
  } = props
76
81
  const runtimeData = useMemo(() => {
77
82
  const data = removeNullColumns(parentRuntimeData)
@@ -94,6 +99,11 @@ const DataTable = (props: DataTableProps) => {
94
99
 
95
100
  const [accessibilityLabel, setAccessibilityLabel] = useState('')
96
101
 
102
+ // Create default refs for map-specific props when not provided
103
+ const defaultLegendMemo = useRef(new Map())
104
+ const defaultLegendSpecialClassLastMemo = useRef(new Map())
105
+ const defaultRuntimeLegend = null
106
+
97
107
  const isVertical = !(config.type === 'chart' && !config.table?.showVertical)
98
108
 
99
109
  const rand = Math.random().toString(16).substr(2, 8)
@@ -276,9 +286,20 @@ const DataTable = (props: DataTableProps) => {
276
286
 
277
287
  const childrenMatrix =
278
288
  config.type === 'map'
279
- ? mapCellMatrix({ ...props, rows, wrapColumns, runtimeData, viewport })
289
+ ? mapCellMatrix({
290
+ ...props,
291
+ rows,
292
+ wrapColumns,
293
+ runtimeData,
294
+ viewport,
295
+ legendMemo: props.legendMemo || defaultLegendMemo,
296
+ legendSpecialClassLastMemo: props.legendSpecialClassLastMemo || defaultLegendSpecialClassLastMemo,
297
+ runtimeLegend: props.runtimeLegend || defaultRuntimeLegend
298
+ })
280
299
  : chartCellMatrix({ rows, ...props, runtimeData, isVertical, sortBy, hasRowType, viewport })
281
300
 
301
+ const useBottomExpandCollapse = config.table.showBottomCollapse && expanded && Array.isArray(childrenMatrix)
302
+
282
303
  // If every value in a column is a number, record the column index so the header and cells can be right-aligned
283
304
  const rightAlignedCols = childrenMatrix.length
284
305
  ? Object.fromEntries(
@@ -290,16 +311,19 @@ const DataTable = (props: DataTableProps) => {
290
311
  )
291
312
  : {}
292
313
 
314
+ const showCollapseButton = config.table.collapsible !== false && useBottomExpandCollapse
293
315
  const TableMediaControls = ({ belowTable }) => {
294
316
  const hasDownloadLink = config.table.download
295
317
  return (
296
318
  <MediaControls.Section classes={getMediaControlsClasses(belowTable, hasDownloadLink)}>
297
- <MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
319
+ <MediaControls.Link config={config} dashboardDataConfig={dataConfig} interactionLabel={interactionLabel} />
298
320
  {hasDownloadLink && (
299
321
  <DownloadButton
300
322
  rawData={getDownloadData()}
301
323
  fileName={`${vizTitle || 'data-table'}.csv`}
302
324
  headerColor={headerColor}
325
+ interactionLabel={interactionLabel}
326
+ config={config}
303
327
  />
304
328
  )}
305
329
  </MediaControls.Section>
@@ -308,11 +332,21 @@ const DataTable = (props: DataTableProps) => {
308
332
 
309
333
  return (
310
334
  <ErrorBoundary component='DataTable'>
311
- {!config.table.showDownloadLinkBelow && <TableMediaControls />}
335
+ {!config.table.showDownloadLinkBelow && (
336
+ <div className='w-100 d-flex justify-content-end'>
337
+ <TableMediaControls />
338
+ </div>
339
+ )}
312
340
  <section id={tabbingId.replace('#', '')} className={getClassNames()} aria-label={accessibilityLabel}>
313
341
  <SkipTo skipId={skipId} skipMessage='Skip Data Table' />
314
342
  {config.table.collapsible !== false && (
315
- <ExpandCollapse expanded={expanded} setExpanded={setExpanded} tableTitle={tableTitle} viewport={viewport} />
343
+ <ExpandCollapse
344
+ expanded={expanded}
345
+ setExpanded={setExpanded}
346
+ tableTitle={tableTitle}
347
+ config={config}
348
+ interactionLabel={interactionLabel}
349
+ />
316
350
  )}
317
351
  <div className='table-container' style={limitHeight}>
318
352
  <Table
@@ -333,6 +367,7 @@ const DataTable = (props: DataTableProps) => {
333
367
  sortBy={sortBy}
334
368
  setSortBy={setSortBy}
335
369
  rightAlignedCols={rightAlignedCols}
370
+ interactionLabel={interactionLabel}
336
371
  />
337
372
  ) : (
338
373
  <ChartHeader
@@ -344,12 +379,13 @@ const DataTable = (props: DataTableProps) => {
344
379
  setSortBy={setSortBy}
345
380
  viewport={viewport}
346
381
  rightAlignedCols={rightAlignedCols}
382
+ interactionLabel={interactionLabel}
347
383
  />
348
384
  )
349
385
  }
350
386
  tableOptions={{
351
- className: `table table-striped table-width-unset ${expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'}${isVertical ? '' : ' horizontal'
352
- }`,
387
+ className: `table table-striped table-width-unset ${expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'
388
+ }${isVertical ? '' : ' horizontal'}`,
353
389
  'aria-live': 'assertive',
354
390
  'aria-rowcount': config?.data?.length ? config.data.length : -1,
355
391
  hidden: !expanded,
@@ -382,7 +418,18 @@ const DataTable = (props: DataTableProps) => {
382
418
  )}
383
419
  </div>
384
420
  </section>
385
- {config.table.showDownloadLinkBelow && <TableMediaControls belowTable={true} />}
421
+ <div className={`w-100 d-flex ${showCollapseButton ? 'justify-content-between' : 'justify-content-end'}`}>
422
+ {showCollapseButton && (
423
+ <button
424
+ className='border-0 bg-transparent text-decoration-underline mt-2'
425
+ style={{ color: 'var(--colors-link-blue)', fontSize: '0.772rem', textUnderlineOffset: '6px' }}
426
+ onClick={() => setExpanded(false)}
427
+ >
428
+ - Collapse table
429
+ </button>
430
+ )}
431
+ {config.table.showDownloadLinkBelow && <TableMediaControls belowTable={true} />}
432
+ </div>
386
433
  <div id={skipId} className='cdcdataviz-sr-only'>
387
434
  Skipped data table.
388
435
  </div>
@@ -394,7 +441,12 @@ const DataTable = (props: DataTableProps) => {
394
441
  <ErrorBoundary component='DataTable'>
395
442
  <section id={tabbingId.replace('#', '')} className={getClassNames()} aria-label={accessibilityLabel}>
396
443
  <SkipTo skipId={skipId} skipMessage='Skip Data Table' />
397
- <ExpandCollapse expanded={expanded} setExpanded={setExpanded} tableTitle={tableTitle} />
444
+ <ExpandCollapse
445
+ expanded={expanded}
446
+ setExpanded={setExpanded}
447
+ tableTitle={tableTitle}
448
+ interactionLabel={interactionLabel}
449
+ />
398
450
  <div className='table-container' style={limitHeight}>
399
451
  <Table
400
452
  viewport={viewport}
@@ -18,6 +18,7 @@ type StandAloneProps = {
18
18
  isEditor?: boolean
19
19
  updateConfig?: (Visualization) => void
20
20
  datasets?: Datasets
21
+ interactionLabel?: string
21
22
  }
22
23
 
23
24
  const DataTableStandAlone: React.FC<StandAloneProps> = ({
@@ -26,7 +27,8 @@ const DataTableStandAlone: React.FC<StandAloneProps> = ({
26
27
  updateConfig,
27
28
  viewport,
28
29
  isEditor,
29
- datasets
30
+ datasets,
31
+ interactionLabel = ''
30
32
  }) => {
31
33
  const [filteredData, setFilteredData] = useState<Record<string, any>[]>(
32
34
  filterVizData(config.filters, config.formattedData || config.data)
@@ -69,6 +71,7 @@ const DataTableStandAlone: React.FC<StandAloneProps> = ({
69
71
  tabbingId={visualizationKey}
70
72
  tableTitle={config.table.label}
71
73
  viewport={viewport || 'lg'}
74
+ interactionLabel={interactionLabel}
72
75
  />
73
76
  <FootnotesStandAlone config={config.footnotes} filters={config.filters?.filter(f => f.filterFootnotes)} />
74
77
  </>
@@ -6,8 +6,20 @@ import { SortIcon } from './SortIcon'
6
6
  import { getNewSortBy } from '../helpers/getNewSortBy'
7
7
  import parse from 'html-react-parser'
8
8
  import { ChartConfig } from '@cdc/chart/src/types/ChartConfig'
9
+ import { publishAnalyticsEvent } from '../../../helpers/metrics/helpers'
10
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
9
11
 
10
- type ChartHeaderProps = { data; isVertical; config; setSortBy; sortBy; hasRowType?; viewport; rightAlignedCols }
12
+ type ChartHeaderProps = {
13
+ data
14
+ isVertical
15
+ config
16
+ setSortBy
17
+ sortBy
18
+ hasRowType?
19
+ viewport
20
+ rightAlignedCols
21
+ interactionLabel: string
22
+ }
11
23
 
12
24
  const ChartHeader = ({
13
25
  data,
@@ -17,7 +29,8 @@ const ChartHeader = ({
17
29
  sortBy,
18
30
  hasRowType,
19
31
  viewport,
20
- rightAlignedCols
32
+ rightAlignedCols,
33
+ interactionLabel
21
34
  }: ChartHeaderProps) => {
22
35
  const groupBy = config.table?.groupBy
23
36
  if (!data) return
@@ -49,9 +62,8 @@ const ChartHeader = ({
49
62
  if (columnHeaderText === notApplicableText) return
50
63
 
51
64
  return (
52
- <span className='cdcdataviz-sr-only'>{`Press command, modifier, or enter key to sort by ${columnHeaderText} in ${
53
- sortBy.column !== columnHeaderText ? 'ascending' : sortBy.column === 'desc' ? 'descending' : 'ascending'
54
- } order`}</span>
65
+ <span className='cdcdataviz-sr-only'>{`Press command, modifier, or enter key to sort by ${columnHeaderText} in ${sortBy.column !== columnHeaderText ? 'ascending' : sortBy.column === 'desc' ? 'descending' : 'ascending'
66
+ } order`}</span>
55
67
  )
56
68
  }
57
69
 
@@ -104,11 +116,29 @@ const ChartHeader = ({
104
116
  scope='col'
105
117
  onClick={() => {
106
118
  if (hasRowType) return
119
+ publishAnalyticsEvent({
120
+ vizType: config.type,
121
+ vizSubType: getVizSubType(config),
122
+ eventType: `data_table_sort`,
123
+ eventAction: 'click',
124
+ eventLabel: interactionLabel,
125
+ vizTitle: getVizTitle(config),
126
+ specifics: `column: ${newSortBy.column || 'none'}, order: ${newSortBy.asc === true ? 'asc' : newSortBy.asc === false ? 'desc' : 'none'}`
127
+ })
107
128
  setSortBy(newSortBy)
108
129
  }}
109
130
  onKeyDown={e => {
110
131
  if (hasRowType) return
111
- if (e.keyCode === 13) {
132
+ if (e.key === 'Enter') {
133
+ publishAnalyticsEvent({
134
+ vizType: config.type,
135
+ vizSubType: getVizSubType(config),
136
+ eventType: `data_table_sort`,
137
+ eventAction: 'keyboard',
138
+ eventLabel: interactionLabel,
139
+ vizTitle: getVizTitle(config),
140
+ specifics: `column: ${newSortBy.column || 'none'}, order: ${newSortBy.asc === true ? 'asc' : newSortBy.asc === false ? 'desc' : 'none'}`
141
+ })
112
142
  setSortBy(newSortBy)
113
143
  }
114
144
  }}
@@ -118,7 +148,7 @@ const ChartHeader = ({
118
148
  : { 'aria-sort': 'descending' }
119
149
  : null)}
120
150
  >
121
- <ColumnHeadingText text={text} column={column} config={config} />
151
+ <ColumnHeadingText text={text} config={config} />
122
152
  {isSortedCol && <SortIcon ascending={sortByAsc} />}
123
153
  <ScreenReaderSortByText sortBy={sortBy} config={config} text={text} />
124
154
  </th>
@@ -152,10 +182,29 @@ const ChartHeader = ({
152
182
  role='columnheader'
153
183
  scope='col'
154
184
  onClick={() => {
185
+ if (hasRowType) return
186
+ publishAnalyticsEvent({
187
+ vizType: config.type,
188
+ vizSubType: getVizSubType(config),
189
+ eventType: `data_table_sort`,
190
+ eventAction: 'click',
191
+ eventLabel: interactionLabel,
192
+ vizTitle: getVizTitle(config),
193
+ specifics: `column: ${newSortBy.column || 'none'}, order: ${newSortBy.asc === true ? 'asc' : newSortBy.asc === false ? 'desc' : 'none'}`
194
+ })
155
195
  setSortBy(newSortBy)
156
196
  }}
157
197
  onKeyDown={e => {
158
- if (e.keyCode === 13) {
198
+ if (e.key === 'Enter') {
199
+ publishAnalyticsEvent({
200
+ vizType: config.type,
201
+ vizSubType: getVizSubType(config),
202
+ eventType: `data_table_sort`,
203
+ eventAction: 'keyboard',
204
+ eventLabel: interactionLabel,
205
+ vizTitle: getVizTitle(config),
206
+ specifics: `column: ${newSortBy.column || 'none'}, order: ${newSortBy.asc === true ? 'asc' : newSortBy.asc === false ? 'desc' : 'none'}`
207
+ })
159
208
  setSortBy(newSortBy)
160
209
  }
161
210
  }}
@@ -165,7 +214,7 @@ const ChartHeader = ({
165
214
  : { 'aria-sort': 'descending' }
166
215
  : null)}
167
216
  >
168
- <ColumnHeadingText text={text} column={column} config={config} />
217
+ <ColumnHeadingText text={text} config={config} />
169
218
  {isSortedCol && <SortIcon ascending={sortByAsc} />}
170
219
 
171
220
  <ScreenReaderSortByText text={text} config={config} sortBy={sortBy} />
@@ -1,12 +1,32 @@
1
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
2
+ import { publishAnalyticsEvent } from '../../../helpers/metrics/helpers'
3
+ import { Visualization } from '../../../types/Visualization'
1
4
  import Icon from '../../ui/Icon'
2
5
  import parse from 'html-react-parser'
3
6
 
4
- const ExpandCollapse = ({ expanded, setExpanded, tableTitle, fontSize, viewport }) => {
7
+ interface ExpandCollapseProps {
8
+ expanded: boolean
9
+ setExpanded: (expanded: boolean) => void
10
+ tableTitle: string
11
+ config?: Visualization
12
+ interactionLabel?: string
13
+ }
14
+
15
+ const ExpandCollapse = ({ expanded, setExpanded, tableTitle, config, interactionLabel = '' }: ExpandCollapseProps) => {
5
16
  return (
6
17
  <div
7
18
  role='button'
8
19
  className={expanded ? 'data-table-heading p-3' : 'collapsed data-table-heading p-3'}
9
20
  onClick={() => {
21
+ publishAnalyticsEvent({
22
+ vizType: config?.type,
23
+ vizSubType: getVizSubType(config),
24
+ eventType: 'expand_collapse_toggled',
25
+ eventAction: 'click',
26
+ eventLabel: interactionLabel,
27
+ vizTitle: getVizTitle(config),
28
+ specifics: expanded ? 'collapsed' : 'expanded'
29
+ })
10
30
  setExpanded(!expanded)
11
31
  }}
12
32
  tabIndex={0}
@@ -2,10 +2,13 @@ import { DataTableProps } from '../DataTable'
2
2
  import ScreenReaderText from '../../elements/ScreenReaderText'
3
3
  import { SortIcon } from './SortIcon'
4
4
  import { getNewSortBy } from '../helpers/getNewSortBy'
5
+ import { publishAnalyticsEvent } from '../../../helpers/metrics/helpers'
6
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
5
7
 
6
8
  type MapHeaderProps = DataTableProps & {
7
9
  sortBy: { column; asc }
8
10
  setSortBy: Function
11
+ interactionLabel: string
9
12
  }
10
13
 
11
14
  const ColumnHeadingText = ({ text, config }) => {
@@ -16,7 +19,15 @@ const ColumnHeadingText = ({ text, config }) => {
16
19
  return text
17
20
  }
18
21
 
19
- const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy, rightAlignedCols }: MapHeaderProps) => {
22
+ const MapHeader = ({
23
+ columns,
24
+ config,
25
+ indexTitle,
26
+ sortBy,
27
+ setSortBy,
28
+ rightAlignedCols,
29
+ interactionLabel = ''
30
+ }: MapHeaderProps) => {
20
31
  return (
21
32
  <tr>
22
33
  {Object.keys(columns)
@@ -46,10 +57,28 @@ const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy, rightAligne
46
57
  role='columnheader'
47
58
  scope='col'
48
59
  onClick={() => {
60
+ publishAnalyticsEvent({
61
+ vizType: config.type,
62
+ vizSubType: getVizSubType(config),
63
+ eventType: `data_table_sort`,
64
+ eventAction: 'click',
65
+ eventLabel: interactionLabel,
66
+ vizTitle: getVizTitle(config),
67
+ specifics: `column: ${newSortBy.column || 'none'}, order: ${newSortBy.asc === true ? 'asc' : newSortBy.asc === false ? 'desc' : 'none'}`
68
+ })
49
69
  setSortBy(newSortBy)
50
70
  }}
51
71
  onKeyDown={e => {
52
- if (e.keyCode === 13) {
72
+ if (e.key === 'Enter') {
73
+ publishAnalyticsEvent({
74
+ vizType: config.type,
75
+ vizSubType: getVizSubType(config),
76
+ eventType: `data_table_sort`,
77
+ eventAction: 'keyboard',
78
+ eventLabel: interactionLabel,
79
+ vizTitle: getVizTitle(config),
80
+ specifics: `column: ${newSortBy.column || 'none'}, order: ${newSortBy.asc === true ? 'asc' : newSortBy.asc === false ? 'desc' : 'none'}`
81
+ })
53
82
  setSortBy(newSortBy)
54
83
  }
55
84
  }}
@@ -60,15 +89,14 @@ const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy, rightAligne
60
89
  : { 'aria-sort': 'descending' }
61
90
  : null)}
62
91
  >
63
- <ColumnHeadingText text={text} config={config} column={column} />
92
+ <ColumnHeadingText text={text} config={config} />
64
93
  <SortIcon ascending={sortByAsc} />
65
- <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${
66
- sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'
67
- } order`}</span>
94
+ <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'
95
+ } order`}</span>
68
96
  </th>
69
97
  )
70
98
  })}
71
- </tr>
99
+ </tr >
72
100
  )
73
101
  }
74
102
 
@@ -145,7 +145,13 @@ table.data-table {
145
145
 
146
146
  svg {
147
147
  margin-left: 1rem;
148
+
149
+ &.legend-shape-svg {
150
+ display: flex;
151
+ margin-left: 0 !important;
152
+ }
148
153
  }
154
+
149
155
  }
150
156
 
151
157
  td a {
@@ -31,7 +31,6 @@ const chartCellArray = ({
31
31
  const groupBy = config.table?.groupBy
32
32
  const dataSeriesColumns = getDataSeriesColumns(config, isVertical, runtimeData)
33
33
 
34
-
35
34
  const dataSeriesColumnsSorted = () => {
36
35
  if (!sortBy && sortBy.colIndex === null) return dataSeriesColumns
37
36
  return dataSeriesColumns.sort((a, b) => {
@@ -73,10 +72,14 @@ const chartCellArray = ({
73
72
  return rows.map(row => {
74
73
  if (hasRowType) {
75
74
  const rowType = getRowType(runtimeData[row])
76
- const rowValues = dataSeriesColumns.map(column => getChartCellValue(row, column, config, runtimeData, rightAxisItemsMap))
75
+ const rowValues = dataSeriesColumns.map(column =>
76
+ getChartCellValue(row, column, config, runtimeData, rightAxisItemsMap)
77
+ )
77
78
  return [rowType, ...rowValues]
78
79
  } else {
79
- return dataSeriesColumns.map((column, j) => getChartCellValue(row, column, config, runtimeData, rightAxisItemsMap))
80
+ return dataSeriesColumns.map((column, j) =>
81
+ getChartCellValue(row, column, config, runtimeData, rightAxisItemsMap)
82
+ )
80
83
  }
81
84
  })
82
85
  }
@@ -86,11 +89,11 @@ const chartCellArray = ({
86
89
  let nodes: ReactNode[] =
87
90
  config.visualizationType !== 'Pie'
88
91
  ? [
89
- <>
90
- {colorScale && colorScale(seriesName) && <LegendShape fill={colorScale(seriesName)} />}
91
- {parse(seriesName)}
92
- </>
93
- ]
92
+ <>
93
+ {colorScale && colorScale(seriesName) && <LegendShape fill={colorScale(seriesName)} />}
94
+ {parse(String(seriesName))}
95
+ </>
96
+ ]
94
97
  : []
95
98
  return nodes.concat(rows.map((row, i) => getChartCellValue(row, column, config, runtimeData, rightAxisItemsMap)))
96
99
  })
@@ -6,6 +6,7 @@ import { displayDataAsText } from '@cdc/core/helpers/displayDataAsText'
6
6
  import _ from 'lodash'
7
7
  import { applyLegendToRow } from '@cdc/map/src/helpers/applyLegendToRow'
8
8
  import { hashObj } from '@cdc/map/src/helpers'
9
+ import { getPatternForRow } from '@cdc/map/src/helpers/getPatternForRow'
9
10
 
10
11
  type MapRowsProps = DataTableProps & {
11
12
  rows: string[]
@@ -104,10 +105,27 @@ const mapCellArray = ({
104
105
  type === 'bubble' && allowMapZoom && geoType === 'world' ? () => setFilteredCountryCode(row) : undefined
105
106
 
106
107
  const validColor = legendColor && legendColor.length > 0 && !noColor
108
+
109
+ // Check for pattern information
110
+ const patternInfo = getPatternForRow(rowObj, config)
111
+ const mapId = config.runtime?.uniqueId || 'map'
112
+
107
113
  return (
108
114
  <div className='col-12'>
109
115
  {validColor ? (
110
- <LegendShape fill={legendColor[0]} />
116
+ patternInfo ? (
117
+ <LegendShape
118
+ fill={legendColor[0]}
119
+ patternInfo={{
120
+ pattern: patternInfo.pattern,
121
+ patternId: `${mapId}--${patternInfo.dataKey}--${patternInfo.patternIndex}--table`,
122
+ size: patternInfo.size,
123
+ color: patternInfo.color
124
+ }}
125
+ />
126
+ ) : (
127
+ <LegendShape fill={legendColor[0]} />
128
+ )
111
129
  ) : (
112
130
  <div className='d-inline-block me-2' style={{ width: '1rem', height: '1rem' }} />
113
131
  )}
@@ -1,40 +1,69 @@
1
+ import { useRef } from 'react'
1
2
  import Papa from 'papaparse'
3
+ import { publishAnalyticsEvent } from '../helpers/metrics/helpers'
4
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
2
5
 
3
6
  type DownloadButtonProps = {
4
- rawData: Object
7
+ rawData: any[]
5
8
  fileName: string
6
9
  headerColor: string
7
10
  skipId: string | number
11
+ configUrl?: string
12
+ interactionLabel?: string
13
+ title?: string
8
14
  }
9
15
 
10
- const DownloadButton = ({ rawData, fileName, headerColor, skipId }: DownloadButtonProps) => {
11
- const csvData = Papa.unparse(rawData)
12
- // Prepend a Byte Order Mark (BOM) to the CSV data.
13
- // The BOM is a special marker that helps applications like Excel recognize the file as UTF-8 encoded.
14
- // Adding the BOM ensures that Excel interprets special characters correctly.
15
- const bom = '\uFEFF'
16
- const utf8EncodedCsvData = new TextEncoder().encode(bom + csvData)
17
- const blob = new Blob([utf8EncodedCsvData], { type: 'text/csv;charset=utf-8;' })
16
+ const DownloadButton = ({ rawData, fileName, headerColor, skipId, interactionLabel, configUrl, title, config }: DownloadButtonProps) => {
17
+ const linkRef = useRef<HTMLAnchorElement>(null)
18
+
19
+ const handleDownload = (event: React.MouseEvent<HTMLAnchorElement>) => {
20
+ event.preventDefault()
21
+
22
+ const csvData = Papa.unparse(rawData)
23
+
24
+ // Prepend a Byte Order Mark (BOM) to the CSV data.
25
+ // The BOM is a special marker that helps applications like Excel recognize the file as UTF-8 encoded.
26
+ // Adding the BOM ensures that Excel interprets special characters correctly.
27
+ const bom = '\uFEFF'
28
+ const utf8EncodedCsvData = new TextEncoder().encode(bom + csvData)
29
+ const blob = new Blob([utf8EncodedCsvData], { type: 'text/csv;charset=utf-8;' })
30
+
31
+ const url = URL.createObjectURL(blob)
18
32
 
19
- const saveBlob = () => {
20
33
  //@ts-ignore
21
34
  if (typeof window.navigator.msSaveBlob === 'function') {
22
35
  //@ts-ignore
23
36
  navigator.msSaveBlob(blob, fileName)
37
+ } else {
38
+ const downloadLink = document.createElement('a')
39
+ downloadLink.href = url
40
+ downloadLink.download = fileName
41
+ document.body.appendChild(downloadLink)
42
+ downloadLink.click()
43
+ document.body.removeChild(downloadLink)
24
44
  }
45
+ URL.revokeObjectURL(url)
46
+ publishAnalyticsEvent({
47
+ vizType: config.type,
48
+ vizSubType: getVizSubType(config),
49
+ eventType: 'data_downloaded',
50
+ eventAction: 'click',
51
+ eventLabel: interactionLabel || configUrl,
52
+ vizTitle: getVizTitle(config)
53
+ })
25
54
  }
26
55
 
27
56
  return (
28
57
  <a
29
- download={fileName}
58
+ ref={linkRef}
30
59
  type='button'
31
- onClick={saveBlob}
32
- href={URL.createObjectURL(blob)}
60
+ onClick={handleDownload}
33
61
  aria-label='Download this data in a CSV file format.'
34
62
  className={`${headerColor} no-border`}
35
63
  id={`${skipId}`}
36
64
  data-html2canvas-ignore
37
65
  role='button'
66
+ style={{ cursor: 'pointer' }}
38
67
  >
39
68
  Download Data (CSV)
40
69
  </a>