@cdc/core 4.24.10 → 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 (52) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +17 -13
  2. package/components/Alert/components/Alert.tsx +34 -8
  3. package/components/DataTable/DataTable.tsx +12 -2
  4. package/components/DataTable/data-table.css +4 -22
  5. package/components/DataTable/helpers/boxplotCellMatrix.tsx +14 -13
  6. package/components/DataTable/helpers/getChartCellValue.ts +23 -5
  7. package/components/EditorPanel/ColumnsEditor.tsx +81 -36
  8. package/components/EditorPanel/DataTableEditor.tsx +33 -33
  9. package/components/EditorPanel/FieldSetWrapper.tsx +2 -2
  10. package/components/EditorPanel/Inputs.tsx +26 -16
  11. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +30 -55
  12. package/components/Filters/Filters.tsx +12 -4
  13. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +0 -4
  14. package/components/Layout/components/Visualization/visualizations.scss +1 -1
  15. package/components/Legend/Legend.Gradient.tsx +49 -34
  16. package/components/MultiSelect/MultiSelect.tsx +85 -62
  17. package/components/MultiSelect/multiselect.styles.css +10 -7
  18. package/components/NestedDropdown/NestedDropdown.tsx +40 -20
  19. package/components/NestedDropdown/nesteddropdown.styles.css +15 -13
  20. package/components/Table/Table.tsx +102 -34
  21. package/components/_stories/DataTable.stories.tsx +14 -0
  22. package/components/_stories/Filters.stories.tsx +57 -0
  23. package/components/_stories/_mocks/DataTable/no-data.json +108 -0
  24. package/components/ui/Icon.tsx +19 -6
  25. package/dist/cove-main.css +20 -54
  26. package/dist/cove-main.css.map +1 -1
  27. package/helpers/DataTransform.ts +2 -1
  28. package/helpers/cove/{number.js → number.ts} +25 -11
  29. package/helpers/fetchRemoteData.js +32 -37
  30. package/helpers/formatConfigBeforeSave.ts +1 -0
  31. package/helpers/queryStringUtils.ts +6 -0
  32. package/helpers/useDataVizClasses.ts +42 -20
  33. package/package.json +2 -2
  34. package/styles/_button-section.scss +1 -1
  35. package/styles/_global-variables.scss +3 -3
  36. package/styles/_global.scss +21 -22
  37. package/styles/_reset.scss +0 -11
  38. package/styles/filters.scss +0 -22
  39. package/styles/v2/base/_reset.scss +0 -7
  40. package/styles/v2/components/editor.scss +0 -4
  41. package/styles/v2/components/icon.scss +1 -1
  42. package/types/Axis.ts +2 -0
  43. package/types/BoxPlot.ts +5 -3
  44. package/types/Color.ts +1 -1
  45. package/types/Legend.ts +1 -2
  46. package/types/MarkupInclude.ts +1 -0
  47. package/types/Runtime.ts +3 -1
  48. package/types/Series.ts +8 -1
  49. package/types/Table.ts +1 -1
  50. package/types/Visualization.ts +7 -8
  51. package/components/ui/Select.jsx +0 -30
  52. package/helpers/getGradientLegendWidth.ts +0 -15
@@ -6,6 +6,7 @@ import { FilterFunction, JsonEditor, UpdateFunction } from 'json-edit-react'
6
6
  import { formatConfigBeforeSave as stripConfig } from '../../helpers/formatConfigBeforeSave'
7
7
  import './advanced-editor-styles.css'
8
8
  import _ from 'lodash'
9
+ import Tooltip from '../ui/Tooltip'
9
10
 
10
11
  export const AdvancedEditor = ({ loadConfig, config, convertStateToConfig, onExpandCollapse = () => {} }) => {
11
12
  const [advancedToggle, _setAdvancedToggle] = useState(false)
@@ -37,7 +38,11 @@ export const AdvancedEditor = ({ loadConfig, config, convertStateToConfig, onExp
37
38
  chart: ['Charts', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/bar-chart.html', <ChartIcon />],
38
39
  dashboard: ['Dashboard', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/bar-chart.html', <ChartIcon />],
39
40
  map: ['Maps', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/data-map.html', <MapIcon />],
40
- 'markup-include': ['Markup Include', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/Markup-Include.html', <MarkupIncludeIcon />]
41
+ 'markup-include': [
42
+ 'Markup Include',
43
+ 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/Markup-Include.html',
44
+ <MarkupIncludeIcon />
45
+ ]
41
46
  }
42
47
 
43
48
  if (!config.type) return <></>
@@ -63,25 +68,24 @@ export const AdvancedEditor = ({ loadConfig, config, convertStateToConfig, onExp
63
68
  </div>
64
69
  </section>
65
70
  <p className='pb-2'>
66
- This tool displays the actual <acronym title='JavaScript Object Notation'>JSON</acronym> configuration that is generated by this editor and allows you to edit properties directly and apply them.
71
+ This tool displays the actual <acronym title='JavaScript Object Notation'>JSON</acronym> configuration
72
+ that is generated by this editor and allows you to edit properties directly and apply them.
67
73
  </p>
74
+ <JsonEditor
75
+ className='advanced-json-editor'
76
+ data={configTextboxValue}
77
+ onUpdate={onUpdate}
78
+ rootName=''
79
+ collapse={collapseFields}
80
+ />
68
81
  <button
69
- className='btn '
70
- onClick={() => {
71
- navigator.clipboard.writeText(JSON.stringify(configTextboxValue))
72
- }}
73
- >
74
- Copy to Clipboard
75
- </button>
76
- <JsonEditor className='advanced-json-editor' data={configTextboxValue} onUpdate={onUpdate} rootName='' collapse={collapseFields} />
77
- <button
78
- className='btn full-width'
82
+ className='btn btn-success m-2 p-2'
79
83
  onClick={() => {
80
84
  loadConfig(configTextboxValue)
81
85
  setAdvancedToggle(!advancedToggle)
82
86
  }}
83
87
  >
84
- Apply
88
+ Apply Configuration Changes
85
89
  </button>
86
90
  </React.Fragment>
87
91
  )}
@@ -1,7 +1,7 @@
1
1
  import Icon from '../../ui/Icon'
2
2
 
3
3
  import DOMPurify from 'dompurify'
4
- import React from 'react'
4
+ import React, { useEffect } from 'react'
5
5
 
6
6
  import './Alert.styles.css'
7
7
 
@@ -14,10 +14,31 @@ type AlertProps = {
14
14
  iconSize?: number
15
15
  // heading for the alert box
16
16
  heading?: string
17
+ // dismiss function
18
+ onDismiss?: Function
19
+ // make the alert dismiss on it's own
20
+ autoDismiss?: boolean
21
+ // set seconds until autoDismiss, default is 5 seconds
22
+ secondsBeforeDismiss?: number
17
23
  }
18
24
 
19
- const Alert: React.FC<AlertProps> = ({ type = 'info', message = '', iconSize = 21, heading }) => {
25
+ const Alert: React.FC<AlertProps> = ({
26
+ type = 'info',
27
+ message = '',
28
+ iconSize = 21,
29
+ heading,
30
+ onDismiss,
31
+ autoDismiss,
32
+ secondsBeforeDismiss = 5
33
+ }) => {
20
34
  // sanitize the text for setting dangerouslySetInnerHTML
35
+
36
+ useEffect(() => {
37
+ if (autoDismiss) {
38
+ setTimeout(() => onDismiss(), secondsBeforeDismiss * 1000)
39
+ }
40
+ }, [])
41
+
21
42
  const sanitizedData = () => ({
22
43
  __html: DOMPurify.sanitize(message)
23
44
  })
@@ -26,12 +47,17 @@ const Alert: React.FC<AlertProps> = ({ type = 'info', message = '', iconSize = 2
26
47
  const styleResets = { width: 'unset', height: 'unset', paddingRight: '5px' }
27
48
 
28
49
  return (
29
- <div className={`alert alert-${type} p-1`} role='alert'>
30
- {heading && <h4 className='alert-heading'>{heading}</h4>}
31
- {type === 'success' && <Icon display='check' size={iconSize} />}
32
- {type === 'danger' && <Icon display='warningCircle' size={iconSize} />}
33
- {type === 'info' && <Icon display='info' size={iconSize} />}
34
- <span dangerouslySetInnerHTML={sanitizedData()} />
50
+ <div className={`alert alert-${type} p-1 d-flex justify-content-between`} role='alert'>
51
+ <div className='d-flex'>
52
+ {heading && <h4 className='alert-heading'>{heading}</h4>}
53
+ {type === 'success' && <Icon display='check' size={iconSize} />}
54
+ {type === 'danger' && <Icon display='warningCircle' size={iconSize} />}
55
+ {type === 'info' && <Icon display='info' size={iconSize} />}
56
+ <span dangerouslySetInnerHTML={sanitizedData()} />
57
+ </div>
58
+ <button type='button' className='close pl-5' aria-label='Close' onClick={() => onDismiss()}>
59
+ X
60
+ </button>
35
61
  </div>
36
62
  )
37
63
  }
@@ -22,6 +22,7 @@ import { Column } from '../../types/Column'
22
22
  import { pivotData } from '../../helpers/pivotData'
23
23
  import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
24
24
  import './data-table.css'
25
+ import _ from 'lodash'
25
26
 
26
27
  export type DataTableProps = {
27
28
  applyLegendToRow?: Function
@@ -73,9 +74,14 @@ const DataTable = (props: DataTableProps) => {
73
74
  const runtimeData = useMemo(() => {
74
75
  const data = removeNullColumns(parentRuntimeData)
75
76
  if (config.table.pivot) {
77
+ const excludeColumns = Object.values(config.columns || {})
78
+ .filter(column => column.dataTable === false)
79
+ .map(col => col.name)
76
80
  const { columnName, valueColumns } = config.table.pivot
77
81
  if (columnName && valueColumns) {
78
- return pivotData(data, columnName, valueColumns)
82
+ // remove excluded columns so that they aren't included in the pivot calculation
83
+ const _data = data.map(row => _.omit(row, excludeColumns))
84
+ return pivotData(_data, columnName, valueColumns)
79
85
  }
80
86
  }
81
87
  return data
@@ -84,7 +90,7 @@ const DataTable = (props: DataTableProps) => {
84
90
  const [expanded, setExpanded] = useState(expandDataTable)
85
91
 
86
92
  const [sortBy, setSortBy] = useState<any>({
87
- column: config.type === 'map' ? 'geo' : 'date',
93
+ column: '',
88
94
  asc: false,
89
95
  colIndex: null
90
96
  })
@@ -200,6 +206,7 @@ const DataTable = (props: DataTableProps) => {
200
206
  : config.runtime?.seriesKeys),
201
207
  [config.runtime?.seriesKeys]) // eslint-disable-line
202
208
 
209
+ const hasNoData = runtimeData.length === 0
203
210
  if (config.visualizationType !== 'Box Plot') {
204
211
  const getDownloadData = () => {
205
212
  // only use fullGeoName on County maps and no other
@@ -263,6 +270,7 @@ const DataTable = (props: DataTableProps) => {
263
270
  preliminaryData={config.preliminaryData}
264
271
  viewport={viewport}
265
272
  wrapColumns={wrapColumns}
273
+ noData={hasNoData}
266
274
  childrenMatrix={
267
275
  config.type === 'map'
268
276
  ? mapCellMatrix({ rows, wrapColumns, ...props, runtimeData, viewport })
@@ -306,6 +314,7 @@ const DataTable = (props: DataTableProps) => {
306
314
  viewport={viewport}
307
315
  wrapColumns={wrapColumns}
308
316
  childrenMatrix={regionCellMatrix({ config })}
317
+ noData={hasNoData}
309
318
  tableName={config.visualizationType}
310
319
  caption='Table of the highlighted regions in the visualization'
311
320
  headContent={
@@ -343,6 +352,7 @@ const DataTable = (props: DataTableProps) => {
343
352
  viewport={viewport}
344
353
  wrapColumns={wrapColumns}
345
354
  childrenMatrix={boxplotCellMatrix({ rows: tableData, config })}
355
+ noData={hasNoData}
346
356
  tableName={config.visualizationType}
347
357
  caption={caption}
348
358
  stickyHeader
@@ -211,33 +211,15 @@ table.data-table {
211
211
  }
212
212
  button {
213
213
  background: var(--mediumGray);
214
- padding: 0.6rem 0.8rem;
215
214
  &:hover {
216
215
  background: lighten(var(--mediumGray), 5%);
217
216
  }
218
217
  }
219
- button.btn-next {
220
- &::before {
221
- content: ' ';
222
- background-image: url(../assets/icon-caret-filled-up.svg);
223
- background-size: 10px 5px;
224
- width: 10px;
225
- height: 5px;
226
- display: block;
227
- transform: rotate(90deg);
228
- }
229
- }
230
- button.btn-prev {
231
- &::before {
232
- content: ' ';
233
- background-image: url(../assets/icon-caret-filled-up.svg);
234
- background-size: 10px 5px;
235
- width: 10px;
236
- height: 5px;
237
- display: block;
238
- transform: rotate(-90deg);
239
- }
218
+
219
+ button.btn svg {
220
+ margin: 0px 0 3px 0;
240
221
  }
222
+
241
223
  button[disabled] {
242
224
  background: var(--mediumGray);
243
225
  opacity: 0.3;
@@ -16,7 +16,7 @@ const boxplotCellMatrix = ({ rows, config }): CellMatrix => {
16
16
  columnThirdQuartile: labels.q3,
17
17
  columnOutliers: labels.outliers,
18
18
  values: labels.values,
19
- columnTotal: labels.total,
19
+ columnCount: labels.count,
20
20
  columnSd: 'Standard Deviation',
21
21
  nonOutlierValues: 'Non Outliers',
22
22
  columnLowerBounds: labels.lowerBounds,
@@ -28,30 +28,31 @@ const boxplotCellMatrix = ({ rows, config }): CellMatrix => {
28
28
  return resolvedName
29
29
  }
30
30
  let resolveCell = (rowid, plot) => {
31
- if (Number(rowid) === 0) return true
32
- if (Number(rowid) === 1) return plot.columnMax
33
- if (Number(rowid) === 2) return plot.columnThirdQuartile
34
- if (Number(rowid) === 3) return plot.columnMedian
35
- if (Number(rowid) === 4) return plot.columnFirstQuartile
36
- if (Number(rowid) === 5) return plot.columnMin
37
- if (Number(rowid) === 6) return plot.columnTotal
38
- if (Number(rowid) === 7) return plot.columnSd
39
- if (Number(rowid) === 8) return plot.columnMean
40
- if (Number(rowid) === 9) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
41
- if (Number(rowid) === 10) return plot.values.length > 0 ? plot.values.toString() : '-'
31
+ if (Number(rowid) === 0) return plot.columnMax
32
+ if (Number(rowid) === 1) return plot.columnThirdQuartile
33
+ if (Number(rowid) === 2) return plot.columnMedian
34
+ if (Number(rowid) === 3) return plot.columnFirstQuartile
35
+ if (Number(rowid) === 4) return plot.columnMin
36
+ if (Number(rowid) === 5) return plot.columnCount
37
+ if (Number(rowid) === 6) return plot.columnSd
38
+ if (Number(rowid) === 7) return plot.columnMean
39
+ if (Number(rowid) === 8) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
40
+ if (Number(rowid) === 9) return plot.values.length > 0 ? plot.values.toString() : '-'
42
41
  return <p>-</p>
43
42
  }
44
43
  // get list of data keys for each row
45
44
  let dataKeys = rows.map(row => {
46
45
  return row[0]
47
46
  })
47
+
48
48
  let columns = ['Measures', ...config.boxplot.categories]
49
49
  dataKeys.shift() // remove index 0 // we did header column separately
50
+
50
51
  return dataKeys.map((rowkey, index) => {
51
52
  return columns.map((column, colnum) => {
52
53
  let cellValue
53
54
  if (column === 'Measures') {
54
- let labelValue = index > 0 ? resolveName(rowkey) : ''
55
+ let labelValue = resolveName(rowkey)
55
56
  cellValue = <>{labelValue}</>
56
57
  } else {
57
58
  cellValue = resolveCell(index, config.boxplot.plots[colnum - 1])
@@ -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 : ''
@@ -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'}
@@ -83,7 +83,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
83
83
  </Tooltip>
84
84
  }
85
85
  />
86
- {config.type !== 'table' && (
86
+ {config.type !== 'table' ? (
87
87
  <CheckBox
88
88
  value={config.table.show}
89
89
  fieldName='show'
@@ -108,6 +108,15 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
108
108
  </Tooltip>
109
109
  }
110
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
+ />
111
120
  )}
112
121
 
113
122
  {config.visualizationType !== 'Box Plot' && config.type !== 'table' && (
@@ -249,15 +258,14 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
249
258
  updateField={updateField}
250
259
  />
251
260
  )}
252
- {config.type !== 'table' && (
253
- <CheckBox
254
- value={config.table.showDownloadLinkBelow}
255
- fieldName='showDownloadLinkBelow'
256
- label='Show Download Link Below Table'
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
+ />
261
269
  <label>
262
270
  <span className='edit-label column-heading'>Table Cell Min Width</span>
263
271
  <input
@@ -267,9 +275,17 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
267
275
  />
268
276
  </label>
269
277
  {config?.visualizationType !== 'Sankey' && (
270
- <label>
271
- <span className='edit-label column-heading'>
272
- 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={
273
289
  <Tooltip style={{ textTransform: 'none' }}>
274
290
  <Tooltip.Target>
275
291
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
@@ -281,27 +297,11 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
281
297
  </p>
282
298
  </Tooltip.Content>
283
299
  </Tooltip>
284
- </span>
285
-
286
- <select
287
- value={config.table.groupBy}
288
- onChange={event => {
289
- changeGroupBy(event.target.value)
290
- }}
291
- >
292
- {[
293
- PLACEHOLDER,
294
- ...groupPivotColumns.filter(
295
- col => col !== config.table.pivot?.columnName && col !== config.table.pivot?.valueColumn
296
- )
297
- ].map(option => (
298
- <option key={option}>{option}</option>
299
- ))}
300
- </select>
301
- </label>
300
+ }
301
+ />
302
302
  )}
303
303
  <Select
304
- label='Pivot Column: '
304
+ label='Pivot Column'
305
305
  tooltip={
306
306
  <Tooltip style={{ textTransform: 'none' }}>
307
307
  <Tooltip.Target>
@@ -314,7 +314,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
314
314
  }
315
315
  value={config.table.pivot?.columnName}
316
316
  options={groupPivotColumns.filter(
317
- col => col !== config.table.groupBy && col !== config.table.pivot?.valueColumn
317
+ col => col !== config.table.groupBy && !(config.table.pivot?.valueColumns || []).includes(col)
318
318
  )}
319
319
  initial='-Select-'
320
320
  section='table'
@@ -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