@cdc/core 4.25.8 → 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 (117) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +29 -8
  2. package/components/DataTable/DataTable.tsx +56 -38
  3. package/components/DataTable/components/ChartHeader.tsx +44 -14
  4. package/components/DataTable/components/ExpandCollapse.tsx +10 -1
  5. package/components/DataTable/components/MapHeader.tsx +24 -13
  6. package/components/DataTable/data-table.css +6 -0
  7. package/components/DataTable/helpers/chartCellMatrix.tsx +11 -8
  8. package/components/DataTable/helpers/mapCellMatrix.tsx +19 -1
  9. package/components/DownloadButton.tsx +40 -14
  10. package/components/EditorPanel/components/MarkupHighlightedTextField.tsx +227 -0
  11. package/components/EditorPanel/components/MarkupVariablesEditor.tsx +411 -0
  12. package/components/EditorPanel/components/PanelMarkup.tsx +59 -0
  13. package/components/ErrorBoundary.jsx +3 -1
  14. package/components/Filters/Filters.tsx +27 -20
  15. package/components/Filters/components/Tabs.tsx +1 -0
  16. package/components/Legend/Legend.Gradient.tsx +3 -6
  17. package/components/LegendShape.tsx +121 -3
  18. package/components/MediaControls.tsx +51 -3
  19. package/components/PaletteConversionModal.tsx +87 -0
  20. package/components/PaletteSelector/DeveloperPaletteRollback.tsx +114 -0
  21. package/components/PaletteSelector/PaletteSelector.css +51 -0
  22. package/components/PaletteSelector/PaletteSelector.tsx +112 -0
  23. package/components/PaletteSelector/index.ts +2 -0
  24. package/components/RichTooltip/RichTooltip.tsx +1 -0
  25. package/components/Table/Table.tsx +3 -1
  26. package/components/_stories/BlurStrokeTest.stories.tsx +1 -1
  27. package/components/_stories/DataTable.stories.tsx +1 -1
  28. package/components/_stories/Filters.stories.tsx +1 -1
  29. package/components/_stories/Footnotes.stories.tsx +1 -1
  30. package/components/_stories/Inputs.stories.tsx +1 -1
  31. package/components/_stories/MultiSelect.stories.tsx +3 -3
  32. package/components/_stories/NestedDropdown.stories.tsx +1 -1
  33. package/components/_stories/Table.stories.tsx +1 -1
  34. package/components/elements/_stories/Button.stories.tsx +1 -1
  35. package/components/elements/_stories/Card.stories.tsx +1 -1
  36. package/components/inputs/InputToggle.tsx +2 -0
  37. package/components/managers/DataDesigner.tsx +10 -9
  38. package/components/managers/_stories/DataDesigner.stories.tsx +1 -1
  39. package/components/ui/Tooltip.tsx +2 -1
  40. package/components/ui/_stories/Accordion.stories.tsx +1 -1
  41. package/components/ui/_stories/ColorPaletteMigration.stories.mdx +275 -0
  42. package/components/ui/_stories/Colors.stories.tsx +330 -0
  43. package/components/ui/_stories/IconGallery.stories.tsx +316 -0
  44. package/components/ui/_stories/Title.stories.tsx +1 -1
  45. package/contexts/EditorContext.ts +18 -0
  46. package/contexts/editor.actions.ts +28 -0
  47. package/contexts/editor.reducer.ts +94 -0
  48. package/data/chartColorPalettes.ts +118 -0
  49. package/data/colorPalettes.ts +9 -0
  50. package/data/mapColorPalettes.ts +45 -0
  51. package/data/sharedPalettes.ts +50 -0
  52. package/dist/cove-main.css +14 -11
  53. package/dist/cove-main.css.map +1 -1
  54. package/generateViteConfig.js +80 -0
  55. package/helpers/addValuesToFilters.ts +2 -3
  56. package/helpers/cloneConfig.ts +31 -0
  57. package/helpers/configDataHelpers.ts +128 -0
  58. package/helpers/configHelpers.ts +27 -0
  59. package/helpers/constants.ts +5 -2
  60. package/helpers/coveUpdateWorker.ts +13 -3
  61. package/helpers/filterColorPalettes.ts +152 -0
  62. package/helpers/generateColorsArray.ts +13 -0
  63. package/helpers/getColorPaletteVersion.ts +33 -0
  64. package/helpers/getPaletteAccessor.ts +18 -0
  65. package/helpers/markupProcessor.ts +205 -0
  66. package/helpers/metrics/helpers.ts +42 -19
  67. package/helpers/metrics/types.ts +48 -9
  68. package/helpers/metrics/utils.ts +34 -0
  69. package/helpers/palettes/colorDistributions.ts +56 -0
  70. package/helpers/palettes/migratePaletteName.ts +150 -0
  71. package/helpers/palettes/standardizePaletteNames.ts +77 -0
  72. package/helpers/palettes/utils.ts +267 -0
  73. package/helpers/queryStringUtils.ts +13 -0
  74. package/helpers/testing.ts +345 -0
  75. package/helpers/tests/addValuesToFilters.test.ts +1 -2
  76. package/helpers/tests/generateColorsArray.test.ts +24 -0
  77. package/helpers/tests/markupProcessor.test.ts +538 -0
  78. package/helpers/tests/testStandaloneBuild.ts +44 -0
  79. package/helpers/useMarkupVariables.ts +31 -0
  80. package/helpers/vegaConfig.ts +0 -1
  81. package/helpers/ver/4.24.10.ts +2 -1
  82. package/helpers/ver/4.24.11.ts +2 -1
  83. package/helpers/ver/4.24.3.ts +2 -1
  84. package/helpers/ver/4.24.4.ts +2 -1
  85. package/helpers/ver/4.24.5.ts +2 -1
  86. package/helpers/ver/4.24.7.ts +2 -1
  87. package/helpers/ver/4.24.9.ts +2 -1
  88. package/helpers/ver/4.25.1.ts +2 -1
  89. package/helpers/ver/4.25.10.ts +36 -0
  90. package/helpers/ver/4.25.3.ts +2 -1
  91. package/helpers/ver/4.25.4.ts +2 -1
  92. package/helpers/ver/4.25.6.ts +2 -1
  93. package/helpers/ver/4.25.7.ts +2 -1
  94. package/helpers/ver/4.25.8.ts +2 -1
  95. package/helpers/ver/4.25.9.ts +293 -0
  96. package/helpers/ver/tests/4.25.10.test.ts +204 -0
  97. package/helpers/ver/tests/4.25.8.test.ts +1 -1
  98. package/helpers/ver/tests/4.25.9.test.ts +51 -0
  99. package/hooks/useColorPalette.ts +79 -0
  100. package/package.json +12 -4
  101. package/styles/_global.scss +7 -5
  102. package/styles/base.scss +8 -5
  103. package/styles/v2/components/button.scss +4 -3
  104. package/styles/v2/components/editor.scss +2 -1
  105. package/styles/v2/layout/_data-table.scss +3 -2
  106. package/styles/v2/themes/_color-definitions.scss +18 -17
  107. package/testBuild.js +0 -0
  108. package/testing-setup.js +32 -0
  109. package/types/MarkupInclude.ts +6 -1
  110. package/types/MarkupVariable.ts +19 -0
  111. package/types/VizFilter.ts +1 -0
  112. package/vitest.config.ts +16 -0
  113. package/components/ui/_stories/Colors.stories.mdx +0 -220
  114. package/components/ui/_stories/IconGallery.stories.mdx +0 -14
  115. package/data/colorPalettes.js +0 -171
  116. package/helpers/formatConfigBeforeSave.ts +0 -135
  117. 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'
@@ -53,6 +53,10 @@ export type DataTableProps = {
53
53
  // determines if columns should be wrapped in the table
54
54
  wrapColumns?: boolean
55
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
56
60
  }
57
61
 
58
62
  const DataTable = (props: DataTableProps) => {
@@ -95,6 +99,11 @@ const DataTable = (props: DataTableProps) => {
95
99
 
96
100
  const [accessibilityLabel, setAccessibilityLabel] = useState('')
97
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
+
98
107
  const isVertical = !(config.type === 'chart' && !config.table?.showVertical)
99
108
 
100
109
  const rand = Math.random().toString(16).substr(2, 8)
@@ -136,23 +145,23 @@ const DataTable = (props: DataTableProps) => {
136
145
  const rows =
137
146
  isVertical && sortBy.column
138
147
  ? rawRows.sort((a, b) => {
139
- let dataA
140
- let dataB
141
- if (config.type === 'map' && config.columns) {
142
- const sortByColName = config.columns[sortBy.column].name
143
- dataA = runtimeData[a][sortByColName]
144
- dataB = runtimeData[b][sortByColName]
145
- }
146
- if (['chart', 'dashboard', 'table'].includes(config.type)) {
147
- dataA = runtimeData[a][sortBy.column]
148
- dataB = runtimeData[b][sortBy.column]
149
- }
150
- if (!dataA && !dataB && config.type === 'chart' && config.xAxis && config.xAxis.type === 'date-time') {
151
- dataA = timeParse(config.runtime.xAxis.dateParseFormat)(runtimeData[a][config.xAxis.dataKey])
152
- dataB = timeParse(config.runtime.xAxis.dateParseFormat)(runtimeData[b][config.xAxis.dataKey])
153
- }
154
- return dataA || dataB ? customSort(dataA, dataB, sortBy, config) : 0
155
- })
148
+ let dataA
149
+ let dataB
150
+ if (config.type === 'map' && config.columns) {
151
+ const sortByColName = config.columns[sortBy.column].name
152
+ dataA = runtimeData[a][sortByColName]
153
+ dataB = runtimeData[b][sortByColName]
154
+ }
155
+ if (['chart', 'dashboard', 'table'].includes(config.type)) {
156
+ dataA = runtimeData[a][sortBy.column]
157
+ dataB = runtimeData[b][sortBy.column]
158
+ }
159
+ if (!dataA && !dataB && config.type === 'chart' && config.xAxis && config.xAxis.type === 'date-time') {
160
+ dataA = timeParse(config.runtime.xAxis.dateParseFormat)(runtimeData[a][config.xAxis.dataKey])
161
+ dataB = timeParse(config.runtime.xAxis.dateParseFormat)(runtimeData[b][config.xAxis.dataKey])
162
+ }
163
+ return dataA || dataB ? customSort(dataA, dataB, sortBy, config) : 0
164
+ })
156
165
  : rawRows
157
166
 
158
167
  const limitHeight = {
@@ -231,17 +240,17 @@ const DataTable = (props: DataTableProps) => {
231
240
  const visibleData =
232
241
  config.type === 'map'
233
242
  ? getMapRowData(
234
- rows,
235
- columns,
236
- config,
237
- formatLegendLocation,
238
- runtimeData as Record<string, Object>,
239
- displayGeoName,
240
- filterColumns
241
- )
243
+ rows,
244
+ columns,
245
+ config,
246
+ formatLegendLocation,
247
+ runtimeData as Record<string, Object>,
248
+ displayGeoName,
249
+ filterColumns
250
+ )
242
251
  : runtimeData.map(d => {
243
- return _.pick(d, [...filterColumns, ...dataSeriesColumns])
244
- })
252
+ return _.pick(d, [...filterColumns, ...dataSeriesColumns])
253
+ })
245
254
  const csvData = config.table?.downloadVisibleDataOnly ? visibleData : rawData
246
255
 
247
256
  // only use fullGeoName on County maps and no other
@@ -277,7 +286,16 @@ const DataTable = (props: DataTableProps) => {
277
286
 
278
287
  const childrenMatrix =
279
288
  config.type === 'map'
280
- ? 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
+ })
281
299
  : chartCellMatrix({ rows, ...props, runtimeData, isVertical, sortBy, hasRowType, viewport })
282
300
 
283
301
  const useBottomExpandCollapse = config.table.showBottomCollapse && expanded && Array.isArray(childrenMatrix)
@@ -285,12 +303,12 @@ const DataTable = (props: DataTableProps) => {
285
303
  // If every value in a column is a number, record the column index so the header and cells can be right-aligned
286
304
  const rightAlignedCols = childrenMatrix.length
287
305
  ? Object.fromEntries(
288
- Object.keys(childrenMatrix[0])
289
- .filter(
290
- i => childrenMatrix.filter(row => isRightAlignedTableValue(row[i])).length === childrenMatrix.length
291
- )
292
- .map(x => [x, true])
293
- )
306
+ Object.keys(childrenMatrix[0])
307
+ .filter(
308
+ i => childrenMatrix.filter(row => isRightAlignedTableValue(row[i])).length === childrenMatrix.length
309
+ )
310
+ .map(x => [x, true])
311
+ )
294
312
  : {}
295
313
 
296
314
  const showCollapseButton = config.table.collapsible !== false && useBottomExpandCollapse
@@ -305,6 +323,7 @@ const DataTable = (props: DataTableProps) => {
305
323
  fileName={`${vizTitle || 'data-table'}.csv`}
306
324
  headerColor={headerColor}
307
325
  interactionLabel={interactionLabel}
326
+ config={config}
308
327
  />
309
328
  )}
310
329
  </MediaControls.Section>
@@ -365,9 +384,8 @@ const DataTable = (props: DataTableProps) => {
365
384
  )
366
385
  }
367
386
  tableOptions={{
368
- className: `table table-striped table-width-unset ${
369
- expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'
370
- }${isVertical ? '' : ' horizontal'}`,
387
+ className: `table table-striped table-width-unset ${expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'
388
+ }${isVertical ? '' : ' horizontal'}`,
371
389
  'aria-live': 'assertive',
372
390
  'aria-rowcount': config?.data?.length ? config.data.length : -1,
373
391
  hidden: !expanded,
@@ -7,6 +7,7 @@ import { getNewSortBy } from '../helpers/getNewSortBy'
7
7
  import parse from 'html-react-parser'
8
8
  import { ChartConfig } from '@cdc/chart/src/types/ChartConfig'
9
9
  import { publishAnalyticsEvent } from '../../../helpers/metrics/helpers'
10
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
10
11
 
11
12
  type ChartHeaderProps = {
12
13
  data
@@ -61,9 +62,8 @@ const ChartHeader = ({
61
62
  if (columnHeaderText === notApplicableText) return
62
63
 
63
64
  return (
64
- <span className='cdcdataviz-sr-only'>{`Press command, modifier, or enter key to sort by ${columnHeaderText} in ${
65
- sortBy.column !== columnHeaderText ? 'ascending' : sortBy.column === 'desc' ? 'descending' : 'ascending'
66
- } 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>
67
67
  )
68
68
  }
69
69
 
@@ -116,18 +116,29 @@ const ChartHeader = ({
116
116
  scope='col'
117
117
  onClick={() => {
118
118
  if (hasRowType) return
119
- publishAnalyticsEvent(
120
- `data_table_sort_by|${newSortBy.column}|${
121
- newSortBy.asc === true ? 'asc' : newSortBy.asc === false ? 'desc' : 'undefined'
122
- }`,
123
- 'click',
124
- interactionLabel
125
- )
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
+ })
126
128
  setSortBy(newSortBy)
127
129
  }}
128
130
  onKeyDown={e => {
129
131
  if (hasRowType) return
130
- 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
+ })
131
142
  setSortBy(newSortBy)
132
143
  }
133
144
  }}
@@ -137,7 +148,7 @@ const ChartHeader = ({
137
148
  : { 'aria-sort': 'descending' }
138
149
  : null)}
139
150
  >
140
- <ColumnHeadingText text={text} column={column} config={config} />
151
+ <ColumnHeadingText text={text} config={config} />
141
152
  {isSortedCol && <SortIcon ascending={sortByAsc} />}
142
153
  <ScreenReaderSortByText sortBy={sortBy} config={config} text={text} />
143
154
  </th>
@@ -171,10 +182,29 @@ const ChartHeader = ({
171
182
  role='columnheader'
172
183
  scope='col'
173
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
+ })
174
195
  setSortBy(newSortBy)
175
196
  }}
176
197
  onKeyDown={e => {
177
- 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
+ })
178
208
  setSortBy(newSortBy)
179
209
  }
180
210
  }}
@@ -184,7 +214,7 @@ const ChartHeader = ({
184
214
  : { 'aria-sort': 'descending' }
185
215
  : null)}
186
216
  >
187
- <ColumnHeadingText text={text} column={column} config={config} />
217
+ <ColumnHeadingText text={text} config={config} />
188
218
  {isSortedCol && <SortIcon ascending={sortByAsc} />}
189
219
 
190
220
  <ScreenReaderSortByText text={text} config={config} sortBy={sortBy} />
@@ -1,3 +1,4 @@
1
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
1
2
  import { publishAnalyticsEvent } from '../../../helpers/metrics/helpers'
2
3
  import { Visualization } from '../../../types/Visualization'
3
4
  import Icon from '../../ui/Icon'
@@ -17,7 +18,15 @@ const ExpandCollapse = ({ expanded, setExpanded, tableTitle, config, interaction
17
18
  role='button'
18
19
  className={expanded ? 'data-table-heading p-3' : 'collapsed data-table-heading p-3'}
19
20
  onClick={() => {
20
- publishAnalyticsEvent('data_table_toggled', 'click', interactionLabel, config.type || 'unknown')
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
+ })
21
30
  setExpanded(!expanded)
22
31
  }}
23
32
  tabIndex={0}
@@ -3,6 +3,7 @@ import ScreenReaderText from '../../elements/ScreenReaderText'
3
3
  import { SortIcon } from './SortIcon'
4
4
  import { getNewSortBy } from '../helpers/getNewSortBy'
5
5
  import { publishAnalyticsEvent } from '../../../helpers/metrics/helpers'
6
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
6
7
 
7
8
  type MapHeaderProps = DataTableProps & {
8
9
  sortBy: { column; asc }
@@ -56,17 +57,28 @@ const MapHeader = ({
56
57
  role='columnheader'
57
58
  scope='col'
58
59
  onClick={() => {
59
- publishAnalyticsEvent(
60
- `data_table_sort_by|${newSortBy.column}|${
61
- newSortBy.asc === true ? 'asc' : newSortBy.asc === false ? 'desc' : 'undefined'
62
- }`,
63
- 'click',
64
- interactionLabel
65
- )
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
+ })
66
69
  setSortBy(newSortBy)
67
70
  }}
68
71
  onKeyDown={e => {
69
- 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
+ })
70
82
  setSortBy(newSortBy)
71
83
  }
72
84
  }}
@@ -77,15 +89,14 @@ const MapHeader = ({
77
89
  : { 'aria-sort': 'descending' }
78
90
  : null)}
79
91
  >
80
- <ColumnHeadingText text={text} config={config} column={column} />
92
+ <ColumnHeadingText text={text} config={config} />
81
93
  <SortIcon ascending={sortByAsc} />
82
- <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${
83
- sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'
84
- } order`}</span>
94
+ <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'
95
+ } order`}</span>
85
96
  </th>
86
97
  )
87
98
  })}
88
- </tr>
99
+ </tr >
89
100
  )
90
101
  }
91
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,43 +1,69 @@
1
+ import { useRef } from 'react'
1
2
  import Papa from 'papaparse'
2
3
  import { publishAnalyticsEvent } from '../helpers/metrics/helpers'
4
+ import { getVizTitle, getVizSubType } from '@cdc/core/helpers/metrics/utils'
3
5
 
4
6
  type DownloadButtonProps = {
5
- rawData: Object
7
+ rawData: any[]
6
8
  fileName: string
7
9
  headerColor: string
8
10
  skipId: string | number
9
11
  configUrl?: string
12
+ interactionLabel?: string
13
+ title?: string
10
14
  }
11
15
 
12
- const DownloadButton = ({ rawData, fileName, headerColor, skipId, configUrl }: DownloadButtonProps) => {
13
- const csvData = Papa.unparse(rawData)
14
- // Prepend a Byte Order Mark (BOM) to the CSV data.
15
- // The BOM is a special marker that helps applications like Excel recognize the file as UTF-8 encoded.
16
- // Adding the BOM ensures that Excel interprets special characters correctly.
17
- const bom = '\uFEFF'
18
- const utf8EncodedCsvData = new TextEncoder().encode(bom + csvData)
19
- 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)
20
32
 
21
- const saveBlob = () => {
22
33
  //@ts-ignore
23
34
  if (typeof window.navigator.msSaveBlob === 'function') {
24
35
  //@ts-ignore
25
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)
26
44
  }
27
- publishAnalyticsEvent('data_downloaded', 'click', configUrl)
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
+ })
28
54
  }
29
55
 
30
56
  return (
31
57
  <a
32
- download={fileName}
58
+ ref={linkRef}
33
59
  type='button'
34
- onClick={saveBlob}
35
- href={URL.createObjectURL(blob)}
60
+ onClick={handleDownload}
36
61
  aria-label='Download this data in a CSV file format.'
37
62
  className={`${headerColor} no-border`}
38
63
  id={`${skipId}`}
39
64
  data-html2canvas-ignore
40
65
  role='button'
66
+ style={{ cursor: 'pointer' }}
41
67
  >
42
68
  Download Data (CSV)
43
69
  </a>