@cdc/core 4.24.9 → 4.24.11

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 (106) hide show
  1. package/LICENSE +201 -0
  2. package/assets/icon-combo-chart.svg +1 -0
  3. package/assets/icon-epi-chart.svg +27 -0
  4. package/components/AdvancedEditor/AdvancedEditor.tsx +17 -13
  5. package/components/Alert/components/Alert.tsx +34 -8
  6. package/components/BlurStrokeText.tsx +44 -0
  7. package/components/DataTable/DataTable.tsx +62 -36
  8. package/components/DataTable/DataTableStandAlone.tsx +37 -6
  9. package/components/DataTable/components/ChartHeader.tsx +31 -26
  10. package/components/DataTable/components/MapHeader.tsx +19 -10
  11. package/components/DataTable/components/SortIcon/index.tsx +25 -0
  12. package/components/DataTable/components/SortIcon/sort-icon.css +21 -0
  13. package/{styles/_data-table.scss → components/DataTable/data-table.css} +250 -298
  14. package/components/DataTable/helpers/boxplotCellMatrix.tsx +14 -13
  15. package/components/DataTable/helpers/customSort.ts +11 -15
  16. package/components/DataTable/helpers/getChartCellValue.ts +23 -5
  17. package/components/DataTable/helpers/getDataSeriesColumns.ts +5 -1
  18. package/components/DataTable/helpers/getNewSortBy.ts +35 -0
  19. package/components/DataTable/helpers/tests/customSort.test.ts +52 -0
  20. package/components/DataTable/helpers/tests/getNewSortBy.test.ts +26 -0
  21. package/components/EditorPanel/ColumnsEditor.tsx +81 -36
  22. package/components/EditorPanel/DataTableEditor.tsx +149 -43
  23. package/components/EditorPanel/FieldSetWrapper.tsx +2 -2
  24. package/components/EditorPanel/Inputs.tsx +68 -20
  25. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +25 -7
  26. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +30 -55
  27. package/components/{Filters.tsx → Filters/Filters.tsx} +60 -43
  28. package/components/Filters/helpers/applyQueuedActive.ts +12 -0
  29. package/components/Filters/helpers/getNestedOptions.ts +29 -0
  30. package/components/Filters/helpers/handleSorting.ts +18 -0
  31. package/components/Filters/helpers/tests/applyQueuedActive.test.ts +49 -0
  32. package/components/Filters/helpers/tests/getNestedOptions.test.ts +93 -0
  33. package/components/Filters/helpers/tests/handleSorting.test.ts +68 -0
  34. package/components/Filters/index.ts +5 -0
  35. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +1 -7
  36. package/components/Layout/components/Visualization/visualizations.scss +1 -1
  37. package/components/Legend/Legend.Gradient.tsx +44 -36
  38. package/components/Loader/Loader.tsx +33 -0
  39. package/components/Loader/index.ts +1 -0
  40. package/components/Loader/loader.styles.css +13 -0
  41. package/components/MultiSelect/MultiSelect.tsx +85 -62
  42. package/components/MultiSelect/multiselect.styles.css +10 -7
  43. package/components/NestedDropdown/NestedDropdown.tsx +118 -56
  44. package/components/NestedDropdown/nestedDropdownHelpers.ts +34 -0
  45. package/components/NestedDropdown/nesteddropdown.styles.css +22 -13
  46. package/components/NestedDropdown/tests/nestedDropdownHelpers.test.ts +58 -0
  47. package/components/Table/Table.tsx +102 -34
  48. package/components/Table/components/GroupRow.tsx +1 -1
  49. package/components/_stories/BlurStrokeTest.stories.tsx +27 -0
  50. package/components/_stories/DataTable.stories.tsx +14 -0
  51. package/components/_stories/Filters.stories.tsx +57 -0
  52. package/components/_stories/NestedDropdown.stories.tsx +22 -46
  53. package/components/_stories/_mocks/DataTable/no-data.json +108 -0
  54. package/components/_stories/_mocks/nested-dropdown.json +30 -0
  55. package/components/_stories/styles.scss +0 -1
  56. package/components/ui/Icon.tsx +19 -6
  57. package/components/ui/{Tooltip.jsx → Tooltip.tsx} +38 -14
  58. package/data/colorPalettes.js +107 -10
  59. package/dist/cove-main.css +6080 -0
  60. package/dist/cove-main.css.map +1 -0
  61. package/helpers/DataTransform.ts +2 -1
  62. package/helpers/addValuesToFilters.ts +8 -3
  63. package/helpers/cove/{number.js → number.ts} +62 -27
  64. package/helpers/coveUpdateWorker.ts +6 -7
  65. package/helpers/fetchRemoteData.js +32 -37
  66. package/helpers/formatConfigBeforeSave.ts +17 -1
  67. package/helpers/gatherQueryParams.ts +12 -2
  68. package/helpers/pivotData.ts +52 -11
  69. package/helpers/queryStringUtils.ts +6 -0
  70. package/helpers/tests/gatherQueryParams.test.ts +34 -0
  71. package/helpers/tests/pivotData.test.ts +50 -0
  72. package/helpers/useDataVizClasses.ts +42 -20
  73. package/helpers/ver/4.24.10.ts +47 -0
  74. package/helpers/ver/4.24.9.ts +0 -3
  75. package/helpers/ver/tests/4.24.10.test.ts +45 -0
  76. package/helpers/viewports.ts +9 -0
  77. package/package.json +7 -3
  78. package/styles/_button-section.scss +5 -1
  79. package/styles/_global-variables.scss +20 -2
  80. package/styles/_global.scss +22 -30
  81. package/styles/_reset.scss +2 -26
  82. package/styles/base.scss +0 -1
  83. package/styles/cove-main.scss +6 -0
  84. package/styles/filters.scss +6 -26
  85. package/styles/v2/base/_reset.scss +0 -7
  86. package/styles/v2/components/editor.scss +0 -4
  87. package/styles/v2/components/icon.scss +1 -1
  88. package/styles/v2/components/ui/tooltip.scss +42 -40
  89. package/styles/v2/layout/_component.scss +0 -6
  90. package/styles/v2/layout/index.scss +0 -1
  91. package/types/Axis.ts +4 -0
  92. package/types/BoxPlot.ts +5 -3
  93. package/types/Color.ts +1 -1
  94. package/types/General.ts +1 -0
  95. package/types/Legend.ts +1 -2
  96. package/types/MarkupInclude.ts +1 -0
  97. package/types/Runtime.ts +3 -1
  98. package/types/Series.ts +8 -1
  99. package/types/Table.ts +3 -2
  100. package/types/Visualization.ts +19 -8
  101. package/types/VizFilter.ts +2 -1
  102. package/components/DataTable/components/Icons.tsx +0 -10
  103. package/components/_stories/EditorPanel.stories.tsx +0 -54
  104. package/components/_stories/Layout.Debug.stories.tsx +0 -91
  105. package/components/ui/Select.jsx +0 -30
  106. package/helpers/getGradientLegendWidth.ts +0 -15
@@ -1,5 +1,5 @@
1
1
  import { parseDate, formatDate } from '@cdc/core/helpers/cove/date'
2
- import { formatNumber } from '@cdc/core/helpers/cove/number'
2
+ import { formatNumber } from '../../../helpers/cove/number'
3
3
  import { TableConfig } from '../types/TableConfig'
4
4
 
5
5
  // if its additional column, return formatting params
@@ -32,7 +32,15 @@ export const getChartCellValue = (row: string, column: string, config: TableConf
32
32
  let labelValue = rowObj[column] // just raw X axis string
33
33
  if (column === config.xAxis?.dataKey) {
34
34
  // not the prettiest, but helper functions work nicely here.
35
- cellValue = config.xAxis?.type === 'date' ? formatDate(config.table?.dateDisplayFormat || config.xAxis?.dateDisplayFormat, parseDate(config.xAxis?.dateParseFormat, labelValue)) : labelValue
35
+ cellValue =
36
+ config.xAxis?.type === 'date'
37
+ ? formatDate(
38
+ config.table?.dateDisplayFormat || config.xAxis?.dateDisplayFormat,
39
+ parseDate(config.xAxis?.dateParseFormat, labelValue)
40
+ )
41
+ : labelValue
42
+ cellValue =
43
+ config.xAxis?.type === 'continuous' ? formatNumber(runtimeData[row][column], 'bottom', false, config) : labelValue
36
44
  } else {
37
45
  let resolvedAxis = 'left'
38
46
  let leftAxisItems = config.series ? config.series.filter(item => item?.axis === 'Left') : []
@@ -48,9 +56,13 @@ export const getChartCellValue = (row: string, column: string, config: TableConf
48
56
 
49
57
  let addColParams = isAdditionalColumn(column, config)
50
58
  if (addColParams) {
51
- cellValue = config.dataFormat ? formatNumber(runtimeData[row][column], resolvedAxis, false, config, addColParams) : runtimeData[row][column]
59
+ cellValue = config.dataFormat
60
+ ? formatNumber(runtimeData[row][column], resolvedAxis, false, config, addColParams)
61
+ : runtimeData[row][column]
52
62
  } else {
53
- cellValue = config.dataFormat ? formatNumber(runtimeData[row][column], resolvedAxis, false, config) : runtimeData[row][column]
63
+ cellValue = config.dataFormat
64
+ ? formatNumber(runtimeData[row][column], resolvedAxis, false, config)
65
+ : runtimeData[row][column]
54
66
  }
55
67
  }
56
68
 
@@ -63,7 +75,13 @@ export const getChartCellValue = (row: string, column: string, config: TableConf
63
75
  const barSeriesExist = config.runtime?.barSeriesKeys?.includes(column)
64
76
  const lineSeriesExist = config.runtime?.lineSeriesKeys?.includes(column)
65
77
  const showSymbol = config.general.showSuppressedSymbol
66
- if (isValueMatch && isColumnMatch && pd.displayTable && pd.type === 'suppression' && config.visualizationSubType !== 'stacked') {
78
+ if (
79
+ isValueMatch &&
80
+ isColumnMatch &&
81
+ pd.displayTable &&
82
+ pd.type === 'suppression' &&
83
+ config.visualizationSubType !== 'stacked'
84
+ ) {
67
85
  switch (config.visualizationType) {
68
86
  case 'Combo':
69
87
  cellValue = barSeriesExist && showSymbol ? pd.iconCode : lineSeriesExist && showSymbol ? pd.lineCode : ''
@@ -56,6 +56,10 @@ export const getDataSeriesColumns = (config: TableConfig, isVertical: boolean, r
56
56
  }
57
57
  })
58
58
 
59
- tmpSeriesColumns.sort((a, b) => columnOrderingHash[a] - columnOrderingHash[b])
59
+ tmpSeriesColumns.sort((a, b) => {
60
+ if (a === 'pivotColumn') return -1
61
+ if (b === 'pivotColumn') return 1
62
+ return columnOrderingHash[a] - columnOrderingHash[b]
63
+ })
60
64
  return tmpSeriesColumns
61
65
  }
@@ -0,0 +1,35 @@
1
+ type SortBy = {
2
+ asc: boolean
3
+ column?: string
4
+ }
5
+
6
+ export const getNewSortBy = (
7
+ sortBy: SortBy,
8
+ column: string,
9
+ index: number
10
+ ): { column: string; asc: boolean; colIndex: number } => {
11
+ let asc
12
+ let sortByCol = column
13
+ const ascending = sortBy.asc === true
14
+ const descending = sortBy.asc === false
15
+ const isInactive = sortBy.asc === undefined
16
+ const noColumnSelected = sortBy.column === undefined
17
+ if (noColumnSelected || sortBy.column !== column) {
18
+ // this is the first time a column is clicked
19
+ asc = true
20
+ } else {
21
+ // clicking the same column
22
+ if (descending) {
23
+ // reset
24
+ sortByCol = undefined
25
+ asc = undefined
26
+ }
27
+ if (isInactive) {
28
+ asc = true
29
+ }
30
+ if (ascending) {
31
+ asc = false
32
+ }
33
+ }
34
+ return { column: sortByCol, asc, colIndex: index }
35
+ }
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { customSort } from '../customSort'
3
+
4
+ describe('customSort()', () => {
5
+ it('should return positive number when a > b', () => {
6
+ const a = 3
7
+ const b = 1
8
+ const sortBy = { column: 'someColumn', asc: true, colIndex: 0 }
9
+ const config = { type: 'map' }
10
+ expect(customSort(a, b, sortBy, config)).greaterThan(0)
11
+ expect(customSort(a, b, sortBy, { type: 'notMap' })).greaterThan(0)
12
+ })
13
+ it('should return negative number when a < b', () => {
14
+ const a = 1
15
+ const b = 3
16
+ const sortBy = { column: 'someColumn', asc: true, colIndex: 0 }
17
+ const config = { type: 'map' }
18
+ expect(customSort(a, b, sortBy, config)).lessThan(0)
19
+ expect(customSort(a, b, sortBy, { type: 'notMap' })).lessThan(0)
20
+ })
21
+ it('works for dates', () => {
22
+ const a = 2000
23
+ const b = 1999
24
+ const sortBy = { column: 'someColumn', asc: true, colIndex: 0 }
25
+ expect(
26
+ customSort(a, b, sortBy, { xAxis: { dataKey: sortBy.column, dateParseFormat: '%Y', type: 'date' } })
27
+ ).greaterThan(0)
28
+ expect(
29
+ customSort(b, a, sortBy, { xAxis: { dataKey: sortBy.column, dateParseFormat: '%Y', type: 'date' } })
30
+ ).lessThan(0)
31
+ })
32
+ it('works for strings', () => {
33
+ const a = 'banana'
34
+ const b = 'apple'
35
+ const sortBy = { column: 'someColumn', asc: true, colIndex: 0 }
36
+ const config = { type: 'map' }
37
+ expect(customSort(a, b, sortBy, config)).greaterThan(0)
38
+ expect(customSort(a, b, sortBy, { type: 'notMap' })).greaterThan(0)
39
+ expect(customSort(b, a, sortBy, config)).lessThan(0)
40
+ expect(customSort(b, a, sortBy, { type: 'notMap' })).lessThan(0)
41
+ })
42
+ it('works for strings after number', () => {
43
+ const a = 'banana'
44
+ const b = '1'
45
+ const sortBy = { column: 'someColumn', asc: true, colIndex: 0 }
46
+ const config = { type: 'map' }
47
+ expect(customSort(a, b, sortBy, config)).greaterThan(0)
48
+ expect(customSort(a, b, sortBy, { type: 'notMap' })).greaterThan(0)
49
+ expect(customSort(b, a, sortBy, config)).lessThan(0)
50
+ expect(customSort(b, a, sortBy, { type: 'notMap' })).lessThan(0)
51
+ })
52
+ })
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getNewSortBy } from '../getNewSortBy'
3
+
4
+ describe('getNewSortBy()', () => {
5
+ it('should return ascending when currently undefined', () => {
6
+ const sortBy = { column: undefined, asc: undefined, colIndex: 0 }
7
+ const column = 'someColumn'
8
+ const index = 1
9
+ const result = getNewSortBy(sortBy, column, index)
10
+ expect(result).toEqual({ column, asc: true, colIndex: index })
11
+ })
12
+ it('should return ascending false when currently true', () => {
13
+ const sortBy = { column: 'someColumn', asc: true, colIndex: 0 }
14
+ const column = 'someColumn'
15
+ const index = 1
16
+ const result = getNewSortBy(sortBy, column, index)
17
+ expect(result).toEqual({ column, asc: false, colIndex: index })
18
+ })
19
+ it('should return ascending undefined when currently false', () => {
20
+ const sortBy = { column: 'someColumn', asc: false, colIndex: 0 }
21
+ const column = 'someColumn'
22
+ const index = 1
23
+ const result = getNewSortBy(sortBy, column, index)
24
+ expect(result).toEqual({ column: undefined, asc: undefined, colIndex: index })
25
+ })
26
+ })
@@ -1,6 +1,6 @@
1
1
  import Tooltip from '../ui/Tooltip'
2
2
  import Icon from '../ui/Icon'
3
- import { TextField } from './Inputs'
3
+ import { Select, TextField } from './Inputs'
4
4
  import { Visualization } from '../../types/Visualization'
5
5
  import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
6
6
  import { Column } from '../../types/Column'
@@ -16,7 +16,13 @@ interface ColumnsEditorProps {
16
16
 
17
17
  type OpenControls = [Record<string, boolean>, Function] // useState type
18
18
 
19
- const FieldSet: React.FC<ColumnsEditorProps & { colKey: string; controls: OpenControls }> = ({ config, deleteColumn, updateField, colKey, controls }) => {
19
+ const FieldSet: React.FC<ColumnsEditorProps & { colKey: string; controls: OpenControls }> = ({
20
+ config,
21
+ deleteColumn,
22
+ updateField,
23
+ colKey,
24
+ controls
25
+ }) => {
20
26
  const [openControls, setOpenControls] = controls
21
27
 
22
28
  const editColumn = (key, value) => {
@@ -63,43 +69,69 @@ const FieldSet: React.FC<ColumnsEditorProps & { colKey: string; controls: OpenCo
63
69
  const colName = config.columns[colKey]?.name
64
70
 
65
71
  return (
66
- <FieldSetWrapper fieldName={colName} fieldKey={colKey} fieldType='Column' controls={controls} deleteField={() => deleteColumn(colKey)}>
67
- <label>
68
- <span className='edit-label column-heading'>Column</span>
69
- <select
70
- value={config.columns[colKey] ? config.columns[colKey].name : undefined}
71
- onChange={event => {
72
- changeName(event.target.value)
73
- }}
74
- >
75
- {['-Select-', ...getColumns()].map(option => (
76
- <option key={option}>{option}</option>
77
- ))}
78
- </select>
79
- </label>
72
+ <FieldSetWrapper
73
+ fieldName={colName}
74
+ fieldKey={colKey}
75
+ fieldType='Column'
76
+ controls={controls}
77
+ deleteField={() => deleteColumn(colKey)}
78
+ >
79
+ <Select
80
+ label='Column'
81
+ value={config.columns[colKey]?.name}
82
+ fieldName='name'
83
+ section={'columns'}
84
+ initial={'-Select-'}
85
+ options={getColumns()}
86
+ updateField={(_section, _subsection, _fieldName, value) => changeName(value)}
87
+ />
80
88
  {config.type !== 'table' && (
81
- <label>
82
- <span className='edit-label column-heading'>Associate to Series</span>
83
- <select
84
- value={config.columns[colKey] ? config.columns[colKey].series : ''}
85
- onChange={event => {
86
- editColumn('series', event.target.value)
87
- }}
88
- >
89
- <option value=''>Select series</option>
90
- {(config.series || []).map(series => (
91
- <option key={series.dataKey}>{series.dataKey}</option>
92
- ))}
93
- </select>
94
- </label>
89
+ <Select
90
+ label='Associate to Series'
91
+ value={config.columns[colKey]?.series}
92
+ fieldName={'series'}
93
+ section='columns'
94
+ initial={'Select series'}
95
+ options={config.series?.map(series => series.dataKey) || []}
96
+ updateField={(_section, _subsection, _fieldName, value) => editColumn('series', value)}
97
+ />
95
98
  )}
96
99
 
97
- <TextField value={config.columns[colKey].label} section='columns' subsection={colKey} fieldName='label' label='Label' updateField={updateField} />
100
+ <TextField
101
+ value={config.columns[colKey].label}
102
+ section='columns'
103
+ subsection={colKey}
104
+ fieldName='label'
105
+ label='Label'
106
+ updateField={updateField}
107
+ />
98
108
  <ul className='column-edit'>
99
109
  <li className='three-col'>
100
- <TextField value={config.columns[colKey].prefix} section='columns' subsection={colKey} fieldName='prefix' label='Prefix' updateField={updateField} />
101
- <TextField value={config.columns[colKey].suffix} section='columns' subsection={colKey} fieldName='suffix' label='Suffix' updateField={updateField} />
102
- <TextField type='number' value={config.columns[colKey].roundToPlace} section='columns' subsection={colKey} fieldName='roundToPlace' label='Round' updateField={updateField} />
110
+ <TextField
111
+ value={config.columns[colKey].prefix}
112
+ section='columns'
113
+ subsection={colKey}
114
+ fieldName='prefix'
115
+ label='Prefix'
116
+ updateField={updateField}
117
+ />
118
+ <TextField
119
+ value={config.columns[colKey].suffix}
120
+ section='columns'
121
+ subsection={colKey}
122
+ fieldName='suffix'
123
+ label='Suffix'
124
+ updateField={updateField}
125
+ />
126
+ <TextField
127
+ type='number'
128
+ value={config.columns[colKey].roundToPlace}
129
+ section='columns'
130
+ subsection={colKey}
131
+ fieldName='roundToPlace'
132
+ label='Round'
133
+ updateField={updateField}
134
+ />
103
135
  </li>
104
136
  <li>
105
137
  <label className='checkbox'>
@@ -202,7 +234,13 @@ const FieldSet: React.FC<ColumnsEditorProps & { colKey: string; controls: OpenCo
202
234
  </ul>
203
235
  <label>
204
236
  <span className='edit-label column-heading'>Order</span>
205
- <input onWheel={e => e.currentTarget.blur()} type='number' min='1' value={config.columns[colKey].order} onChange={e => updateField('columns', colKey, 'order', parseInt(e.target.value))} />
237
+ <input
238
+ onWheel={e => e.currentTarget.blur()}
239
+ type='number'
240
+ min='1'
241
+ value={config.columns[colKey].order}
242
+ onChange={e => updateField('columns', colKey, 'order', parseInt(e.target.value))}
243
+ />
206
244
  </label>
207
245
  </FieldSetWrapper>
208
246
  )
@@ -252,7 +290,14 @@ const ColumnsEditor: React.FC<ColumnsEditorProps> = ({ config, updateField, dele
252
290
  </span>
253
291
  </label>
254
292
  {additionalColumns.map((val, i) => (
255
- <FieldSet key={val + i} controls={openControls} config={config} deleteColumn={deleteColumn} updateField={updateField} colKey={val} />
293
+ <FieldSet
294
+ key={val + i}
295
+ controls={openControls}
296
+ config={config}
297
+ deleteColumn={deleteColumn}
298
+ updateField={updateField}
299
+ colKey={val}
300
+ />
256
301
  ))}
257
302
  <button
258
303
  className={'btn btn-primary'}
@@ -41,6 +41,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
41
41
 
42
42
  const excludeColumns = (section, subSection, fieldName, excludedColNames: string[]) => {
43
43
  const newColumns = _.cloneDeep(config.columns)
44
+
44
45
  const colNames: string[] = []
45
46
  for (let colKey in newColumns) {
46
47
  const col = newColumns[colKey]
@@ -48,6 +49,8 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
48
49
  if (excludedColNames.includes(col.name)) {
49
50
  // ensure all excluded columns are set to false
50
51
  newColumns[colKey].dataTable = false
52
+ } else {
53
+ newColumns[colKey].dataTable = true
51
54
  }
52
55
  }
53
56
  excludedColNames.forEach(colName => {
@@ -80,7 +83,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
80
83
  </Tooltip>
81
84
  }
82
85
  />
83
- {config.type !== 'table' && (
86
+ {config.type !== 'table' ? (
84
87
  <CheckBox
85
88
  value={config.table.show}
86
89
  fieldName='show'
@@ -91,14 +94,29 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
91
94
  tooltip={
92
95
  <Tooltip style={{ textTransform: 'none' }}>
93
96
  <Tooltip.Target>
94
- <Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
97
+ <Icon
98
+ display='question'
99
+ style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }}
100
+ />
95
101
  </Tooltip.Target>
96
102
  <Tooltip.Content>
97
- <p>Hiding the data table may affect accessibility. An alternate form of accessing visualization data is a 508 requirement.</p>
103
+ <p>
104
+ Hiding the data table may affect accessibility. An alternate form of accessing visualization data is a
105
+ 508 requirement.
106
+ </p>
98
107
  </Tooltip.Content>
99
108
  </Tooltip>
100
109
  }
101
110
  />
111
+ ) : (
112
+ <CheckBox
113
+ value={config.general?.showDownloadButton}
114
+ fieldName='showDownloadButton'
115
+ label='Show Download CSV link'
116
+ section='general'
117
+ updateField={updateField}
118
+ className='column-heading'
119
+ />
102
120
  )}
103
121
 
104
122
  {config.visualizationType !== 'Box Plot' && config.type !== 'table' && (
@@ -112,7 +130,10 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
112
130
  tooltip={
113
131
  <Tooltip style={{ textTransform: 'none' }}>
114
132
  <Tooltip.Target>
115
- <Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
133
+ <Icon
134
+ display='question'
135
+ style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }}
136
+ />
116
137
  </Tooltip.Target>
117
138
  <Tooltip.Content>
118
139
  <p>This will draw the data table with vertical data instead of horizontal.</p>
@@ -135,7 +156,10 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
135
156
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
136
157
  </Tooltip.Target>
137
158
  <Tooltip.Content>
138
- <p>To comply with 508 standards, if the first column in the data table has no header, enter a brief one here.</p>
159
+ <p>
160
+ To comply with 508 standards, if the first column in the data table has no header, enter a brief one
161
+ here.
162
+ </p>
139
163
  </Tooltip.Content>
140
164
  </Tooltip>
141
165
  }
@@ -160,46 +184,124 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
160
184
  </Tooltip>
161
185
  }
162
186
  />
163
- <CheckBox value={config.table.limitHeight} section='table' fieldName='limitHeight' label=' Limit Table Height' updateField={updateField} />
164
- {config.table.limitHeight && <TextField value={config.table.height} section='table' fieldName='height' label='Data Table Height' type='number' min={0} max={500} placeholder='Height(px)' updateField={updateField} />}
165
- {config?.visualizationType !== 'Sankey' && <MultiSelect key={excludedColumns.join('') + 'excluded'} options={dataColumns.map(c => ({ label: c, value: c }))} selected={excludedColumns} fieldName='dataTable' label='Exclude Columns' section='columns' updateField={excludeColumns} />}
166
- <CheckBox value={config.table.collapsible} fieldName='collapsible' label=' Collapsible' section='table' updateField={updateField} />
167
- {config.table.collapsible !== false && <CheckBox value={config.table.expanded} fieldName='expanded' label=' Expanded by Default' section='table' updateField={updateField} />}
168
- {isDashboard && config.type !== 'table' && <CheckBox value={config.table.showDataTableLink} fieldName='showDataTableLink' label='Show Data Table Name & Link' section='table' updateField={updateField} />}
169
- {isLoadedFromUrl && <CheckBox value={config.table.showDownloadUrl} fieldName='showDownloadUrl' label='Show URL to Automatically Updated Data' section='table' updateField={updateField} />}
170
- {config.type !== 'table' && <CheckBox value={config.table.showDownloadImgButton} fieldName='showDownloadImgButton' label='Display Image Button' section='table' updateField={updateField} />}
187
+ <CheckBox
188
+ value={config.table.limitHeight}
189
+ section='table'
190
+ fieldName='limitHeight'
191
+ label=' Limit Table Height'
192
+ updateField={updateField}
193
+ />
194
+ {config.table.limitHeight && (
195
+ <TextField
196
+ value={config.table.height}
197
+ section='table'
198
+ fieldName='height'
199
+ label='Data Table Height'
200
+ type='number'
201
+ min={0}
202
+ max={500}
203
+ placeholder='Height(px)'
204
+ updateField={updateField}
205
+ />
206
+ )}
207
+ {config?.visualizationType !== 'Sankey' && (
208
+ <MultiSelect
209
+ key={excludedColumns.join('') + 'excluded'}
210
+ options={dataColumns.map(c => ({ label: c, value: c }))}
211
+ selected={excludedColumns}
212
+ fieldName='dataTable'
213
+ label='Exclude Columns'
214
+ section='columns'
215
+ updateField={excludeColumns}
216
+ />
217
+ )}
218
+ <CheckBox
219
+ value={config.table.collapsible}
220
+ fieldName='collapsible'
221
+ label=' Collapsible'
222
+ section='table'
223
+ updateField={updateField}
224
+ />
225
+ {config.table.collapsible !== false && (
226
+ <CheckBox
227
+ value={config.table.expanded}
228
+ fieldName='expanded'
229
+ label=' Expanded by Default'
230
+ section='table'
231
+ updateField={updateField}
232
+ />
233
+ )}
234
+ {isDashboard && config.type !== 'table' && (
235
+ <CheckBox
236
+ value={config.table.showDataTableLink}
237
+ fieldName='showDataTableLink'
238
+ label='Show Data Table Name & Link'
239
+ section='table'
240
+ updateField={updateField}
241
+ />
242
+ )}
243
+ {isLoadedFromUrl && (
244
+ <CheckBox
245
+ value={config.table.showDownloadUrl}
246
+ fieldName='showDownloadUrl'
247
+ label='Show URL to Automatically Updated Data'
248
+ section='table'
249
+ updateField={updateField}
250
+ />
251
+ )}
252
+ {config.type !== 'table' && (
253
+ <CheckBox
254
+ value={config.table.showDownloadImgButton}
255
+ fieldName='showDownloadImgButton'
256
+ label='Display Image Button'
257
+ section='table'
258
+ updateField={updateField}
259
+ />
260
+ )}
261
+
262
+ <CheckBox
263
+ value={config.table.showDownloadLinkBelow}
264
+ fieldName='showDownloadLinkBelow'
265
+ label='Show Download Link Below Table'
266
+ section='table'
267
+ updateField={updateField}
268
+ />
171
269
  <label>
172
270
  <span className='edit-label column-heading'>Table Cell Min Width</span>
173
- <input type='number' value={config.table.cellMinWidth ? config.table.cellMinWidth : 0} onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)} />
271
+ <input
272
+ type='number'
273
+ value={config.table.cellMinWidth ? config.table.cellMinWidth : 0}
274
+ onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)}
275
+ />
174
276
  </label>
175
277
  {config?.visualizationType !== 'Sankey' && (
176
- <label>
177
- <span className='edit-label column-heading'>
178
- Group By{' '}
278
+ <Select
279
+ value={config.table.groupBy}
280
+ fieldName={'groupBy'}
281
+ section='table'
282
+ label='Group By'
283
+ updateField={(_section, _subSection, _fieldName, value) => changeGroupBy(value)}
284
+ initial={PLACEHOLDER}
285
+ options={groupPivotColumns.filter(
286
+ col => col !== config.table.pivot?.columnName && !(config.table.pivot?.valueColumns || []).includes(col)
287
+ )}
288
+ tooltip={
179
289
  <Tooltip style={{ textTransform: 'none' }}>
180
290
  <Tooltip.Target>
181
291
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
182
292
  </Tooltip.Target>
183
293
  <Tooltip.Content>
184
- <p>Choose a column to use for grouping data rows. The selected column will not be shown in the data table. You will only be able to choose a column which does not have a column configuration.</p>
294
+ <p>
295
+ Choose a column to use for grouping data rows. The selected column will not be shown in the data
296
+ table. You will only be able to choose a column which does not have a column configuration.
297
+ </p>
185
298
  </Tooltip.Content>
186
299
  </Tooltip>
187
- </span>
188
-
189
- <select
190
- value={config.table.groupBy}
191
- onChange={event => {
192
- changeGroupBy(event.target.value)
193
- }}
194
- >
195
- {[PLACEHOLDER, ...groupPivotColumns.filter(col => col !== config.table.pivot?.columnName && col !== config.table.pivot?.valueColumn)].map(option => (
196
- <option key={option}>{option}</option>
197
- ))}
198
- </select>
199
- </label>
300
+ }
301
+ />
200
302
  )}
201
303
  <Select
202
- label='Pivot Column: '
304
+ label='Pivot Column'
203
305
  tooltip={
204
306
  <Tooltip style={{ textTransform: 'none' }}>
205
307
  <Tooltip.Target>
@@ -211,7 +313,9 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
211
313
  </Tooltip>
212
314
  }
213
315
  value={config.table.pivot?.columnName}
214
- options={groupPivotColumns.filter(col => col !== config.table.groupBy && col !== config.table.pivot?.valueColumn)}
316
+ options={groupPivotColumns.filter(
317
+ col => col !== config.table.groupBy && !(config.table.pivot?.valueColumns || []).includes(col)
318
+ )}
215
319
  initial='-Select-'
216
320
  section='table'
217
321
  subsection='pivot'
@@ -219,25 +323,27 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
219
323
  updateField={updateField}
220
324
  />
221
325
  {config.table.pivot?.columnName && (
222
- <Select
223
- label='Pivot Value Column: '
326
+ <MultiSelect
327
+ key={config.table.pivot?.columnName}
328
+ options={groupPivotColumns
329
+ .filter(col => col !== config.table.pivot?.columnName && col !== config.table.groupBy)
330
+ .map(c => ({ label: c, value: c }))}
331
+ selected={config.table.pivot?.valueColumns}
332
+ label='Pivot Value Column(s) '
333
+ section='table'
334
+ subsection='pivot'
335
+ fieldName='valueColumns'
336
+ updateField={updateField}
224
337
  tooltip={
225
338
  <Tooltip style={{ textTransform: 'none' }}>
226
339
  <Tooltip.Target>
227
340
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
228
341
  </Tooltip.Target>
229
342
  <Tooltip.Content>
230
- <p>The column whos values will be pivoted under the column selected as the Filter.</p>
343
+ <p>The column(s) whos values will be pivoted under the column selected as the Filter.</p>
231
344
  </Tooltip.Content>
232
345
  </Tooltip>
233
346
  }
234
- value={config.table.pivot?.valueColumn}
235
- initial='-Select-'
236
- section='table'
237
- options={groupPivotColumns.filter(col => col !== config.table.pivot?.columnName && col !== config.table.groupBy)}
238
- subsection='pivot'
239
- fieldName='valueColumn'
240
- updateField={updateField}
241
347
  />
242
348
  )}
243
349
  </>
@@ -21,7 +21,7 @@ const FieldSet: React.FC<FieldSetProps> = ({ fieldName, fieldKey, fieldType, con
21
21
  if (!show)
22
22
  return (
23
23
  <div className='mb-1'>
24
- <button onClick={() => setShow(fieldKey, true)}>
24
+ <button className='btn btn-light' onClick={() => setShow(fieldKey, true)}>
25
25
  <Icon display='caretDown' />
26
26
  </button>
27
27
  <span> {fieldName ? `${fieldName}` : 'New ' + fieldType}</span>
@@ -30,7 +30,7 @@ const FieldSet: React.FC<FieldSetProps> = ({ fieldName, fieldKey, fieldType, con
30
30
  return (
31
31
  <fieldset className='edit-block mb-1' key={fieldKey}>
32
32
  <div className='d-flex justify-content-between'>
33
- <button onClick={() => setShow(fieldKey, false)}>
33
+ <button className='btn btn-light' onClick={() => setShow(fieldKey, false)}>
34
34
  <Icon display='caretUp' />
35
35
  </button>
36
36
  <button