@cdc/core 4.25.6 → 4.25.8

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 (37) hide show
  1. package/components/DataTable/DataTable.tsx +52 -19
  2. package/components/DataTable/DataTableStandAlone.tsx +4 -1
  3. package/components/DataTable/components/ChartHeader.tsx +44 -13
  4. package/components/DataTable/components/ExpandCollapse.tsx +14 -2
  5. package/components/DataTable/components/MapHeader.tsx +18 -1
  6. package/components/DataTable/components/SortIcon/sort-icon.css +21 -27
  7. package/components/DataTable/data-table.css +33 -4
  8. package/components/DataTable/helpers/customSort.ts +4 -2
  9. package/components/DownloadButton.tsx +4 -1
  10. package/components/EditorPanel/DataTableEditor.tsx +10 -1
  11. package/components/Filters/Filters.tsx +26 -9
  12. package/components/Footnotes/FootnotesStandAlone.tsx +2 -1
  13. package/components/{MediaControls.jsx → MediaControls.tsx} +32 -16
  14. package/components/Table/components/Row.tsx +15 -12
  15. package/dist/cove-main.css +0 -10
  16. package/dist/cove-main.css.map +1 -1
  17. package/helpers/cove/number.ts +6 -2
  18. package/helpers/coveUpdateWorker.ts +5 -1
  19. package/helpers/events.ts +32 -0
  20. package/helpers/isRightAlignedTableValue.js +1 -1
  21. package/helpers/metrics/helpers.ts +52 -0
  22. package/helpers/metrics/types.ts +43 -0
  23. package/helpers/vegaConfig.ts +647 -0
  24. package/helpers/ver/4.25.7.ts +26 -0
  25. package/helpers/ver/4.25.8.ts +61 -0
  26. package/helpers/ver/tests/4.25.8.test.ts +86 -0
  27. package/helpers/viewports.ts +2 -0
  28. package/package.json +6 -4
  29. package/styles/_accessibility.scss +8 -0
  30. package/styles/_button-section.scss +0 -2
  31. package/styles/_global.scss +0 -4
  32. package/styles/filters.scss +0 -4
  33. package/types/ForecastingSeriesKey.ts +15 -0
  34. package/types/Runtime.ts +1 -7
  35. package/types/Series.ts +4 -0
  36. package/types/Table.ts +1 -0
  37. package/helpers/events.js +0 -14
@@ -52,9 +52,9 @@ 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
55
56
  }
56
57
 
57
- /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
58
58
  const DataTable = (props: DataTableProps) => {
59
59
  const {
60
60
  columns,
@@ -71,7 +71,8 @@ const DataTable = (props: DataTableProps) => {
71
71
  tableTitle,
72
72
  viewport,
73
73
  vizTitle,
74
- wrapColumns
74
+ wrapColumns,
75
+ interactionLabel = ''
75
76
  } = props
76
77
  const runtimeData = useMemo(() => {
77
78
  const data = removeNullColumns(parentRuntimeData)
@@ -150,7 +151,7 @@ const DataTable = (props: DataTableProps) => {
150
151
  dataA = timeParse(config.runtime.xAxis.dateParseFormat)(runtimeData[a][config.xAxis.dataKey])
151
152
  dataB = timeParse(config.runtime.xAxis.dateParseFormat)(runtimeData[b][config.xAxis.dataKey])
152
153
  }
153
- return dataA && dataB ? customSort(dataA, dataB, sortBy, config) : 0
154
+ return dataA || dataB ? customSort(dataA, dataB, sortBy, config) : 0
154
155
  })
155
156
  : rawRows
156
157
 
@@ -189,15 +190,15 @@ const DataTable = (props: DataTableProps) => {
189
190
 
190
191
  // prettier-ignore
191
192
  const tableData = useMemo(() => (
192
- config.data?.[0]?.tableData
193
- ? config.data?.[0]?.tableData
194
- : config.visualizationType === 'Sankey'
193
+ config.data?.[0]?.tableData
195
194
  ? config.data?.[0]?.tableData
196
- : config.visualizationType === 'Pie'
197
- ? [config.yAxis.dataKey]
198
- : config.visualizationType === 'Box Plot'
199
- ? config?.boxplot?.plots?.[0] ? Object.entries(config.boxplot.plots[0]) : []
200
- : config.runtime?.seriesKeys),
195
+ : config.visualizationType === 'Sankey'
196
+ ? config.data?.[0]?.tableData
197
+ : config.visualizationType === 'Pie'
198
+ ? [config.yAxis.dataKey]
199
+ : config.visualizationType === 'Box Plot'
200
+ ? config?.boxplot?.plots?.[0] ? Object.entries(config.boxplot.plots[0]) : []
201
+ : config.runtime?.seriesKeys),
201
202
  [config.runtime?.seriesKeys]) // eslint-disable-line
202
203
 
203
204
  const hasNoData = runtimeData.length === 0
@@ -279,6 +280,8 @@ const DataTable = (props: DataTableProps) => {
279
280
  ? mapCellMatrix({ ...props, rows, wrapColumns, runtimeData, viewport })
280
281
  : chartCellMatrix({ rows, ...props, runtimeData, isVertical, sortBy, hasRowType, viewport })
281
282
 
283
+ const useBottomExpandCollapse = config.table.showBottomCollapse && expanded && Array.isArray(childrenMatrix)
284
+
282
285
  // If every value in a column is a number, record the column index so the header and cells can be right-aligned
283
286
  const rightAlignedCols = childrenMatrix.length
284
287
  ? Object.fromEntries(
@@ -290,16 +293,18 @@ const DataTable = (props: DataTableProps) => {
290
293
  )
291
294
  : {}
292
295
 
296
+ const showCollapseButton = config.table.collapsible !== false && useBottomExpandCollapse
293
297
  const TableMediaControls = ({ belowTable }) => {
294
298
  const hasDownloadLink = config.table.download
295
299
  return (
296
300
  <MediaControls.Section classes={getMediaControlsClasses(belowTable, hasDownloadLink)}>
297
- <MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
301
+ <MediaControls.Link config={config} dashboardDataConfig={dataConfig} interactionLabel={interactionLabel} />
298
302
  {hasDownloadLink && (
299
303
  <DownloadButton
300
304
  rawData={getDownloadData()}
301
305
  fileName={`${vizTitle || 'data-table'}.csv`}
302
306
  headerColor={headerColor}
307
+ interactionLabel={interactionLabel}
303
308
  />
304
309
  )}
305
310
  </MediaControls.Section>
@@ -308,11 +313,21 @@ const DataTable = (props: DataTableProps) => {
308
313
 
309
314
  return (
310
315
  <ErrorBoundary component='DataTable'>
311
- {!config.table.showDownloadLinkBelow && <TableMediaControls />}
316
+ {!config.table.showDownloadLinkBelow && (
317
+ <div className='w-100 d-flex justify-content-end'>
318
+ <TableMediaControls />
319
+ </div>
320
+ )}
312
321
  <section id={tabbingId.replace('#', '')} className={getClassNames()} aria-label={accessibilityLabel}>
313
322
  <SkipTo skipId={skipId} skipMessage='Skip Data Table' />
314
323
  {config.table.collapsible !== false && (
315
- <ExpandCollapse expanded={expanded} setExpanded={setExpanded} tableTitle={tableTitle} viewport={viewport} />
324
+ <ExpandCollapse
325
+ expanded={expanded}
326
+ setExpanded={setExpanded}
327
+ tableTitle={tableTitle}
328
+ config={config}
329
+ interactionLabel={interactionLabel}
330
+ />
316
331
  )}
317
332
  <div className='table-container' style={limitHeight}>
318
333
  <Table
@@ -333,6 +348,7 @@ const DataTable = (props: DataTableProps) => {
333
348
  sortBy={sortBy}
334
349
  setSortBy={setSortBy}
335
350
  rightAlignedCols={rightAlignedCols}
351
+ interactionLabel={interactionLabel}
336
352
  />
337
353
  ) : (
338
354
  <ChartHeader
@@ -344,13 +360,14 @@ const DataTable = (props: DataTableProps) => {
344
360
  setSortBy={setSortBy}
345
361
  viewport={viewport}
346
362
  rightAlignedCols={rightAlignedCols}
363
+ interactionLabel={interactionLabel}
347
364
  />
348
365
  )
349
366
  }
350
367
  tableOptions={{
351
- className: `table table-striped ${expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'}${
352
- isVertical ? '' : ' horizontal'
353
- }`,
368
+ className: `table table-striped table-width-unset ${
369
+ expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'
370
+ }${isVertical ? '' : ' horizontal'}`,
354
371
  'aria-live': 'assertive',
355
372
  'aria-rowcount': config?.data?.length ? config.data.length : -1,
356
373
  hidden: !expanded,
@@ -383,7 +400,18 @@ const DataTable = (props: DataTableProps) => {
383
400
  )}
384
401
  </div>
385
402
  </section>
386
- {config.table.showDownloadLinkBelow && <TableMediaControls belowTable={true} />}
403
+ <div className={`w-100 d-flex ${showCollapseButton ? 'justify-content-between' : 'justify-content-end'}`}>
404
+ {showCollapseButton && (
405
+ <button
406
+ className='border-0 bg-transparent text-decoration-underline mt-2'
407
+ style={{ color: 'var(--colors-link-blue)', fontSize: '0.772rem', textUnderlineOffset: '6px' }}
408
+ onClick={() => setExpanded(false)}
409
+ >
410
+ - Collapse table
411
+ </button>
412
+ )}
413
+ {config.table.showDownloadLinkBelow && <TableMediaControls belowTable={true} />}
414
+ </div>
387
415
  <div id={skipId} className='cdcdataviz-sr-only'>
388
416
  Skipped data table.
389
417
  </div>
@@ -395,7 +423,12 @@ const DataTable = (props: DataTableProps) => {
395
423
  <ErrorBoundary component='DataTable'>
396
424
  <section id={tabbingId.replace('#', '')} className={getClassNames()} aria-label={accessibilityLabel}>
397
425
  <SkipTo skipId={skipId} skipMessage='Skip Data Table' />
398
- <ExpandCollapse expanded={expanded} setExpanded={setExpanded} tableTitle={tableTitle} />
426
+ <ExpandCollapse
427
+ expanded={expanded}
428
+ setExpanded={setExpanded}
429
+ tableTitle={tableTitle}
430
+ interactionLabel={interactionLabel}
431
+ />
399
432
  <div className='table-container' style={limitHeight}>
400
433
  <Table
401
434
  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
  </>
@@ -5,8 +5,20 @@ import ScreenReaderText from '@cdc/core/components/elements/ScreenReaderText'
5
5
  import { SortIcon } from './SortIcon'
6
6
  import { getNewSortBy } from '../helpers/getNewSortBy'
7
7
  import parse from 'html-react-parser'
8
+ import { ChartConfig } from '@cdc/chart/src/types/ChartConfig'
9
+ import { publishAnalyticsEvent } from '../../../helpers/metrics/helpers'
8
10
 
9
- type ChartHeaderProps = { data; isVertical; config; setSortBy; sortBy; hasRowType?; viewport; rightAlignedCols }
11
+ type ChartHeaderProps = {
12
+ data
13
+ isVertical
14
+ config
15
+ setSortBy
16
+ sortBy
17
+ hasRowType?
18
+ viewport
19
+ rightAlignedCols
20
+ interactionLabel: string
21
+ }
10
22
 
11
23
  const ChartHeader = ({
12
24
  data,
@@ -16,7 +28,8 @@ const ChartHeader = ({
16
28
  sortBy,
17
29
  hasRowType,
18
30
  viewport,
19
- rightAlignedCols
31
+ rightAlignedCols,
32
+ interactionLabel
20
33
  }: ChartHeaderProps) => {
21
34
  const groupBy = config.table?.groupBy
22
35
  if (!data) return
@@ -48,20 +61,30 @@ const ChartHeader = ({
48
61
  if (columnHeaderText === notApplicableText) return
49
62
 
50
63
  return (
51
- <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'
52
- } order`}</span>
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>
53
67
  )
54
68
  }
55
69
 
56
- const ColumnHeadingText = ({ column, text, config }) => {
70
+ const ColumnHeadingText = ({ text, config }: { text: string; config: ChartConfig }) => {
71
+ const notApplicableText = 'Not Applicable'
57
72
  if (text === '_pivotedFrom') return ''
58
- let notApplicableText = 'Not Applicable'
59
- if (text === '__series__' && config.table.indexLabel) return `${config.table.indexLabel} `
60
- if (text === '__series__' && !config.table.indexLabel)
61
- return <ScreenReaderText as='span'>{notApplicableText}</ScreenReaderText>
62
- return text
63
- }
73
+ if (text === '__series__') {
74
+ if (config.table.indexLabel) {
75
+ return parse(String(config.table.indexLabel))
76
+ } else {
77
+ return <ScreenReaderText as='span'>{notApplicableText}</ScreenReaderText>
78
+ }
79
+ }
64
80
 
81
+ // handle any unexpected values
82
+ if (typeof text !== 'string') {
83
+ return parse('')
84
+ }
85
+
86
+ return parse(text)
87
+ }
65
88
  if (isVertical) {
66
89
  if (hasRowType) {
67
90
  // find the row type column and place it at the beginning of the array
@@ -75,7 +98,7 @@ const ChartHeader = ({
75
98
  return (
76
99
  <tr>
77
100
  {dataSeriesColumns.map((column, index) => {
78
- const text = parse(getSeriesName(column, config))
101
+ const text = getSeriesName(column, config)
79
102
  const newSortBy = getNewSortBy(sortBy, column, index)
80
103
  const sortByAsc = sortBy.column === column ? sortBy.asc : undefined
81
104
  const isSortedCol = column === sortBy.column && !hasRowType
@@ -93,6 +116,13 @@ const ChartHeader = ({
93
116
  scope='col'
94
117
  onClick={() => {
95
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
+ )
96
126
  setSortBy(newSortBy)
97
127
  }}
98
128
  onKeyDown={e => {
@@ -124,7 +154,8 @@ const ChartHeader = ({
124
154
  const rightAxisItemsMap = new Map(rightAxisItems.map(item => [item.dataKey, item]))
125
155
 
126
156
  let column = config.xAxis?.dataKey
127
- let text = row !== '__series__' ? getChartCellValue(row, column, config, data, rightAxisItemsMap) : '__series__'
157
+ let text =
158
+ row !== '__series__' ? getChartCellValue(row, column, config, data, rightAxisItemsMap) : '__series__'
128
159
  const newSortBy = getNewSortBy(sortBy, column, index)
129
160
  const sortByAsc = sortBy.colIndex === index ? sortBy.asc : undefined
130
161
  const isSortedCol = index === sortBy.colIndex && !hasRowType
@@ -1,11 +1,23 @@
1
+ import { publishAnalyticsEvent } from '../../../helpers/metrics/helpers'
2
+ import { Visualization } from '../../../types/Visualization'
1
3
  import Icon from '../../ui/Icon'
4
+ import parse from 'html-react-parser'
2
5
 
3
- const ExpandCollapse = ({ expanded, setExpanded, tableTitle, fontSize, viewport }) => {
6
+ interface ExpandCollapseProps {
7
+ expanded: boolean
8
+ setExpanded: (expanded: boolean) => void
9
+ tableTitle: string
10
+ config?: Visualization
11
+ interactionLabel?: string
12
+ }
13
+
14
+ const ExpandCollapse = ({ expanded, setExpanded, tableTitle, config, interactionLabel = '' }: ExpandCollapseProps) => {
4
15
  return (
5
16
  <div
6
17
  role='button'
7
18
  className={expanded ? 'data-table-heading p-3' : 'collapsed data-table-heading p-3'}
8
19
  onClick={() => {
20
+ publishAnalyticsEvent('data_table_toggled', 'click', interactionLabel, config.type || 'unknown')
9
21
  setExpanded(!expanded)
10
22
  }}
11
23
  tabIndex={0}
@@ -16,7 +28,7 @@ const ExpandCollapse = ({ expanded, setExpanded, tableTitle, fontSize, viewport
16
28
  }}
17
29
  >
18
30
  <Icon display={expanded ? 'minus' : 'plus'} base />
19
- {tableTitle}
31
+ {parse(tableTitle)}
20
32
  </div>
21
33
  )
22
34
  }
@@ -2,10 +2,12 @@ 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'
5
6
 
6
7
  type MapHeaderProps = DataTableProps & {
7
8
  sortBy: { column; asc }
8
9
  setSortBy: Function
10
+ interactionLabel: string
9
11
  }
10
12
 
11
13
  const ColumnHeadingText = ({ text, config }) => {
@@ -16,7 +18,15 @@ const ColumnHeadingText = ({ text, config }) => {
16
18
  return text
17
19
  }
18
20
 
19
- const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy, rightAlignedCols }: MapHeaderProps) => {
21
+ const MapHeader = ({
22
+ columns,
23
+ config,
24
+ indexTitle,
25
+ sortBy,
26
+ setSortBy,
27
+ rightAlignedCols,
28
+ interactionLabel = ''
29
+ }: MapHeaderProps) => {
20
30
  return (
21
31
  <tr>
22
32
  {Object.keys(columns)
@@ -46,6 +56,13 @@ const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy, rightAligne
46
56
  role='columnheader'
47
57
  scope='col'
48
58
  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
+ )
49
66
  setSortBy(newSortBy)
50
67
  }}
51
68
  onKeyDown={e => {
@@ -1,36 +1,30 @@
1
- /* format the white triangle sort icon in data table headers */
2
1
  .sort-icon {
3
- fill: white;
4
- position: relative;
5
- margin: 0 0.5rem !important;
6
- :is(svg) {
7
- position: absolute;
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ justify-content: center;
6
+
7
+ position: absolute;
8
+ right: 4px;
9
+ top: 50%;
10
+ transform: translateY(-50%);
11
+ z-index: 1;
12
+
13
+ svg {
14
+ width: 0.75rem;
15
+ height: 0.75rem;
8
16
  fill: rgba(255, 255, 255, 0.5);
17
+ margin: 0;
18
+ padding: 0;
19
+ line-height: 1;
20
+
9
21
  &.active {
10
22
  fill: white;
11
23
  }
12
- width: 1rem;
13
- height: 1rem;
14
- }
15
- .up {
16
- bottom: 0.5rem;
17
- }
18
- .down {
19
- top: 0.5rem;
20
24
  }
21
- }
22
25
 
23
- @media (max-width: 576px) {
24
- .sort-icon {
25
- :is(svg) {
26
- width: 0.9rem;
27
- height: 0.9rem;
28
- }
29
- .up {
30
- bottom: 0.3rem;
31
- }
32
- .down {
33
- top: 0.3rem;
34
- }
26
+ svg.up,
27
+ svg.down {
28
+ margin: 0 !important;
35
29
  }
36
30
  }
@@ -3,9 +3,14 @@
3
3
  min-width: 100%;
4
4
  }
5
5
 
6
- .collapsed + .table-container {
6
+ .bs4 .table.table-width-unset {
7
+ width: unset;
8
+ }
9
+
10
+ .collapsed+.table-container {
7
11
  border-bottom: none;
8
12
  }
13
+
9
14
  .table-container {
10
15
  overflow-x: auto;
11
16
  border-right: 1px solid var(--lightGray);
@@ -33,6 +38,7 @@
33
38
  }
34
39
 
35
40
  table.horizontal {
41
+
36
42
  th,
37
43
  td {
38
44
  min-width: 200px;
@@ -47,6 +53,7 @@ table.data-table {
47
53
  border-collapse: collapse;
48
54
  appearance: none;
49
55
  table-layout: fixed;
56
+
50
57
  * {
51
58
  box-sizing: border-box;
52
59
  }
@@ -67,8 +74,10 @@ table.data-table {
67
74
  background: none;
68
75
  }
69
76
  }
77
+
70
78
  thead {
71
79
  color: #fff;
80
+
72
81
  .resizer {
73
82
  cursor: e-resize;
74
83
  width: 10px;
@@ -78,9 +87,11 @@ table.data-table {
78
87
  right: 0;
79
88
  touch-action: none;
80
89
  }
90
+
81
91
  tr {
82
92
  text-align: left;
83
93
  }
94
+
84
95
  th,
85
96
  td {
86
97
  padding: 0.5em 0.7em;
@@ -108,8 +119,11 @@ table.data-table {
108
119
  background-color: var(--tertiary) !important;
109
120
  font-weight: bold;
110
121
  }
122
+
111
123
  border-bottom: solid 1px #e5e5e5;
112
- min-width: 100%; /* Needed to fill content up*/
124
+ min-width: 100%;
125
+
126
+ /* Needed to fill content up*/
113
127
  &:last-child {
114
128
  border-bottom: 0;
115
129
  }
@@ -120,6 +134,7 @@ table.data-table {
120
134
  padding: 0.3em 0.7em;
121
135
  border-right: 1px solid rgba(0, 0, 0, 0.1);
122
136
  white-space: nowrap;
137
+
123
138
  &:last-child {
124
139
  border-right: 0 !important;
125
140
  }
@@ -127,6 +142,7 @@ table.data-table {
127
142
 
128
143
  td {
129
144
  position: relative;
145
+
130
146
  svg {
131
147
  margin-left: 1rem;
132
148
  }
@@ -148,6 +164,7 @@ table.data-table {
148
164
  text-decoration: underline;
149
165
  cursor: pointer;
150
166
  color: #075290;
167
+
151
168
  svg {
152
169
  max-width: 13px;
153
170
  vertical-align: baseline;
@@ -163,6 +180,7 @@ table.data-table {
163
180
 
164
181
  .no-data {
165
182
  position: relative;
183
+
166
184
  .no-data-message {
167
185
  background: rgba(255, 255, 255, 0.5);
168
186
  top: 0;
@@ -175,18 +193,22 @@ table.data-table {
175
193
  align-items: center;
176
194
  justify-content: center;
177
195
  z-index: 7;
196
+
178
197
  :is(h3) {
179
198
  font-size: 1.3rem;
180
199
  font-weight: 600;
181
200
  margin-bottom: 0.3rem;
182
201
  }
183
202
  }
203
+
184
204
  tr:hover {
185
205
  background: #fff;
186
206
  }
207
+
187
208
  th,
188
209
  td {
189
210
  width: 50%;
211
+
190
212
  &::before {
191
213
  content: '\00a0';
192
214
  }
@@ -197,15 +219,19 @@ table.data-table {
197
219
  margin: 1rem 0;
198
220
  display: flex;
199
221
  align-items: center;
222
+
200
223
  ul {
201
224
  list-style: none;
202
225
  margin: 0 1rem 0 0;
203
226
  display: flex;
204
- li + li {
227
+
228
+ li+li {
205
229
  margin-left: 0.3rem;
206
230
  }
231
+
207
232
  button {
208
233
  background: var(--mediumGray);
234
+
209
235
  &:hover {
210
236
  background: lighten(var(--mediumGray), 5%);
211
237
  }
@@ -219,6 +245,7 @@ table.data-table {
219
245
  background: var(--mediumGray);
220
246
  opacity: 0.3;
221
247
  cursor: default;
248
+
222
249
  &:hover {
223
250
  background: var(--mediumGray);
224
251
  }
@@ -232,14 +259,16 @@ table.data-table {
232
259
  text-decoration: none;
233
260
  transition: 0.3s all;
234
261
  margin: 1em 0;
262
+
235
263
  &:hover {
236
264
  transition: 0.3s all;
237
265
  }
238
266
  }
267
+
239
268
  .cove,
240
269
  .cdc-open-viz-module {
241
270
  .download-links a:not(:last-child) {
242
271
  margin-right: 10px;
243
272
  display: inline-block;
244
273
  }
245
- }
274
+ }
@@ -37,8 +37,10 @@ export const customSort = (a, b, sortBy, config) => {
37
37
  const isNumB = !isNaN(Number(valueB)) && valueB !== undefined && valueB !== null && trimmedB !== ''
38
38
 
39
39
  // Handle empty strings or spaces
40
- if (trimmedA === '' && trimmedB !== '') return sortBy.asc ? -1 : 1
41
- if (trimmedA !== '' && trimmedB === '') return sortBy.asc ? 1 : -1
40
+ // empty string should always come last no matter the asc
41
+ if (trimmedA === '' && trimmedB === '') return 0
42
+ if (trimmedA === '' && trimmedB !== '') return 1
43
+ if (trimmedA !== '' && trimmedB === '') return -1
42
44
 
43
45
  // Both are numbers: Compare numerically
44
46
  if (isNumA && isNumB) {
@@ -1,13 +1,15 @@
1
1
  import Papa from 'papaparse'
2
+ import { publishAnalyticsEvent } from '../helpers/metrics/helpers'
2
3
 
3
4
  type DownloadButtonProps = {
4
5
  rawData: Object
5
6
  fileName: string
6
7
  headerColor: string
7
8
  skipId: string | number
9
+ configUrl?: string
8
10
  }
9
11
 
10
- const DownloadButton = ({ rawData, fileName, headerColor, skipId }: DownloadButtonProps) => {
12
+ const DownloadButton = ({ rawData, fileName, headerColor, skipId, configUrl }: DownloadButtonProps) => {
11
13
  const csvData = Papa.unparse(rawData)
12
14
  // Prepend a Byte Order Mark (BOM) to the CSV data.
13
15
  // The BOM is a special marker that helps applications like Excel recognize the file as UTF-8 encoded.
@@ -22,6 +24,7 @@ const DownloadButton = ({ rawData, fileName, headerColor, skipId }: DownloadButt
22
24
  //@ts-ignore
23
25
  navigator.msSaveBlob(blob, fileName)
24
26
  }
27
+ publishAnalyticsEvent('data_downloaded', 'click', configUrl)
25
28
  }
26
29
 
27
30
  return (
@@ -216,7 +216,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
216
216
  section='table'
217
217
  updateField={updateField}
218
218
  />
219
- {config.table.collapsible !== false && (
219
+ {config.table.collapsible && (
220
220
  <CheckBox
221
221
  value={config.table.expanded}
222
222
  fieldName='expanded'
@@ -225,6 +225,15 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
225
225
  updateField={updateField}
226
226
  />
227
227
  )}
228
+ {config.table.collapsible && (
229
+ <CheckBox
230
+ value={config.table.showBottomCollapse}
231
+ fieldName='showBottomCollapse'
232
+ label='Show collapse below table'
233
+ section='table'
234
+ updateField={updateField}
235
+ />
236
+ )}
228
237
  <CheckBox
229
238
  value={config.table.download}
230
239
  fieldName='download'