@cdc/core 4.24.5 → 4.24.7

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 (66) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +93 -0
  2. package/components/AdvancedEditor/advanced-editor-styles.css +3 -0
  3. package/components/AdvancedEditor/index.ts +1 -0
  4. package/components/DataTable/DataTable.tsx +21 -2
  5. package/components/DataTable/DataTableStandAlone.tsx +4 -25
  6. package/components/DataTable/components/DataTableEditorPanel.tsx +4 -4
  7. package/components/DataTable/components/ExpandCollapse.tsx +1 -1
  8. package/components/DataTable/helpers/chartCellMatrix.tsx +3 -9
  9. package/components/DataTable/helpers/getChartCellValue.ts +8 -4
  10. package/components/DataTable/helpers/getDataSeriesColumns.ts +8 -5
  11. package/components/DataTable/helpers/getRowType.ts +6 -0
  12. package/components/DataTable/types/TableConfig.ts +1 -0
  13. package/components/EditorPanel/ColumnsEditor.tsx +3 -30
  14. package/components/EditorPanel/DataTableEditor.tsx +66 -22
  15. package/components/EditorPanel/FieldSetWrapper.tsx +51 -0
  16. package/components/EditorPanel/FootnotesEditor.tsx +77 -0
  17. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +227 -0
  18. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +54 -0
  19. package/components/EditorPanel/VizFilterEditor/index.ts +1 -0
  20. package/components/EditorWrapper/EditorWrapper.tsx +3 -4
  21. package/components/EditorWrapper/index.ts +1 -0
  22. package/components/{Filters.jsx → Filters.tsx} +40 -24
  23. package/components/Footnotes/Footnotes.tsx +25 -0
  24. package/components/Footnotes/FootnotesStandAlone.tsx +45 -0
  25. package/components/Footnotes/footnotes.css +5 -0
  26. package/components/Footnotes/index.ts +1 -0
  27. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +8 -4
  28. package/components/Layout/components/Visualization/index.tsx +12 -5
  29. package/components/MultiSelect/MultiSelect.tsx +36 -9
  30. package/components/MultiSelect/multiselect.styles.css +0 -3
  31. package/components/_stories/Footnotes.stories.tsx +17 -0
  32. package/components/_stories/styles.scss +1 -0
  33. package/components/inputs/InputSelect.tsx +17 -6
  34. package/components/ui/Icon.tsx +1 -2
  35. package/helpers/addValuesToFilters.ts +56 -0
  36. package/helpers/cove/accessibility.ts +1 -0
  37. package/helpers/cove/fontSettings.ts +2 -0
  38. package/helpers/coveUpdateWorker.ts +7 -0
  39. package/helpers/filterVizData.ts +30 -0
  40. package/helpers/formatConfigBeforeSave.ts +90 -0
  41. package/helpers/gatherQueryParams.ts +14 -7
  42. package/helpers/lineChartHelpers.js +2 -1
  43. package/helpers/pivotData.ts +18 -0
  44. package/helpers/queryStringUtils.ts +29 -0
  45. package/helpers/tests/updateFieldFactory.test.ts +1 -0
  46. package/helpers/updateFieldFactory.ts +1 -1
  47. package/helpers/ver/4.24.7.ts +92 -0
  48. package/package.json +6 -4
  49. package/styles/_button-section.scss +6 -1
  50. package/styles/_data-table.scss +0 -1
  51. package/styles/base.scss +4 -0
  52. package/styles/v2/themes/_color-definitions.scss +1 -0
  53. package/types/Annotation.ts +46 -0
  54. package/types/Axis.ts +0 -2
  55. package/types/ConfigureData.ts +1 -1
  56. package/types/Footnotes.ts +17 -0
  57. package/types/General.ts +5 -0
  58. package/types/Runtime.ts +2 -7
  59. package/types/Table.ts +6 -0
  60. package/types/Visualization.ts +31 -9
  61. package/types/VizFilter.ts +16 -5
  62. package/LICENSE +0 -201
  63. package/components/AdvancedEditor.jsx +0 -74
  64. package/components/EditorPanel/VizFilterEditor.tsx +0 -234
  65. package/helpers/queryStringUtils.js +0 -26
  66. package/types/BaseVisualizationType.ts +0 -1
@@ -0,0 +1,93 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import MapIcon from '../../assets/map-folded.svg'
3
+ import ChartIcon from '../../assets/icon-chart-bar.svg'
4
+ import MarkupIncludeIcon from '../../assets/icon-code.svg'
5
+ import { FilterFunction, JsonEditor, UpdateFunction } from 'json-edit-react'
6
+ import { formatConfigBeforeSave as stripConfig } from '../../helpers/formatConfigBeforeSave'
7
+ import './advanced-editor-styles.css'
8
+ import _ from 'lodash'
9
+
10
+ export const AdvancedEditor = ({ loadConfig, config, convertStateToConfig, onExpandCollapse = () => {} }) => {
11
+ const [advancedToggle, _setAdvancedToggle] = useState(false)
12
+ const [configTextboxValue, setConfigTextbox] = useState<Record<string, any>>({})
13
+ const setAdvancedToggle = val => {
14
+ _setAdvancedToggle(val)
15
+ onExpandCollapse()
16
+ }
17
+
18
+ const collapseFields: FilterFunction = input => {
19
+ if (['datasets', 'data', 'originalFormattedData', 'formattedData'].includes(String(input.key))) return true
20
+ return false
21
+ }
22
+
23
+ const onUpdate: UpdateFunction = val => {
24
+ setConfigTextbox(val.newData)
25
+ }
26
+
27
+ useEffect(() => {
28
+ let parsedConfig = stripConfig(config)
29
+ if (config.type !== 'dashboard') {
30
+ parsedConfig = convertStateToConfig()
31
+ }
32
+
33
+ setConfigTextbox(parsedConfig)
34
+ }, [config])
35
+
36
+ const typeLookup = {
37
+ chart: ['Charts', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/bar-chart.html', <ChartIcon />],
38
+ dashboard: ['Dashboard', 'https://www.cdc.gov/wcms/4.0/cdc-wp/data-presentation/bar-chart.html', <ChartIcon />],
39
+ 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
+ }
42
+
43
+ if (!config.type) return <></>
44
+ return (
45
+ <>
46
+ <a href={typeLookup[config.type][1]} target='_blank' rel='noopener noreferrer' className='guidance-link'>
47
+ {typeLookup[config.type][2]}
48
+ <div>
49
+ <span className='heading-3'>Get Help with {typeLookup[config.type][0]}</span>
50
+ <p>Examples and documentation</p>
51
+ </div>
52
+ </a>
53
+ <div className='advanced'>
54
+ <span className='advanced-toggle-link' onClick={() => setAdvancedToggle(!advancedToggle)}>
55
+ <span>{advancedToggle ? `— ` : `+ `}</span>Advanced Options
56
+ </span>
57
+ {advancedToggle && (
58
+ <React.Fragment>
59
+ <section className='error-box py-2 px-3 my-2'>
60
+ <div>
61
+ <strong className='pt-1'>Warning</strong>
62
+ <p>This can cause serious errors in your visualization.</p>
63
+ </div>
64
+ </section>
65
+ <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.
67
+ </p>
68
+ <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'
79
+ onClick={() => {
80
+ loadConfig(configTextboxValue)
81
+ setAdvancedToggle(!advancedToggle)
82
+ }}
83
+ >
84
+ Apply
85
+ </button>
86
+ </React.Fragment>
87
+ )}
88
+ </div>
89
+ </>
90
+ )
91
+ }
92
+
93
+ export default AdvancedEditor
@@ -0,0 +1,3 @@
1
+ .advanced-json-editor {
2
+ width: 100vw;
3
+ }
@@ -0,0 +1 @@
1
+ export { default } from './AdvancedEditor'
@@ -19,6 +19,7 @@ import boxplotCellMatrix from './helpers/boxplotCellMatrix'
19
19
  import removeNullColumns from './helpers/removeNullColumns'
20
20
  import { TableConfig } from './types/TableConfig'
21
21
  import { Column } from '../../types/Column'
22
+ import { pivotData } from '../../helpers/pivotData'
22
23
 
23
24
  export type DataTableProps = {
24
25
  applyLegendToRow?: Function
@@ -53,7 +54,17 @@ export type DataTableProps = {
53
54
  /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
54
55
  const DataTable = (props: DataTableProps) => {
55
56
  const { config, dataConfig, tableTitle, vizTitle, rawData, runtimeData: parentRuntimeData, headerColor, expandDataTable, columns, viewport, formatLegendLocation, tabbingId, wrapColumns } = props
56
- const runtimeData = removeNullColumns(parentRuntimeData)
57
+ const runtimeData = useMemo(() => {
58
+ const data = removeNullColumns(parentRuntimeData)
59
+ if (config.table.pivot) {
60
+ const { columnName, valueColumn } = config.table.pivot
61
+ if (columnName && valueColumn) {
62
+ return pivotData(data, columnName, valueColumn)
63
+ }
64
+ }
65
+ return data
66
+ }, [parentRuntimeData, config.table.pivot?.columnName, config.table.pivot?.valueColumn])
67
+
57
68
  const [expanded, setExpanded] = useState(expandDataTable)
58
69
 
59
70
  const [sortBy, setSortBy] = useState<any>({ column: config.type === 'map' ? 'geo' : 'date', asc: false, colIndex: null })
@@ -176,9 +187,17 @@ const DataTable = (props: DataTableProps) => {
176
187
  }
177
188
  }
178
189
 
190
+ const getMediaControlsClasses = () => {
191
+ const classes = ['download-links']
192
+ const isLegendOnBottom = config?.legend?.position === 'bottom' || ['sm', 'xs', 'xxs'].includes(viewport)
193
+ if (config.brush?.active && !isLegendOnBottom) classes.push('brush-active')
194
+ if (config.brush?.active && config.legend.hide) classes.push('brush-active')
195
+ return classes
196
+ }
197
+
179
198
  return (
180
199
  <ErrorBoundary component='DataTable'>
181
- <MediaControls.Section classes={['download-links']}>
200
+ <MediaControls.Section classes={getMediaControlsClasses()}>
182
201
  <MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
183
202
  {(config.table.download || config.general?.showDownloadButton) && <DownloadButton rawData={getDownloadData()} fileName={`${vizTitle || 'data-table'}.csv`} headerColor={headerColor} />}
184
203
  </MediaControls.Section>
@@ -5,6 +5,7 @@ import DataTable from './DataTable'
5
5
  import DataTableEditorPanel from './components/DataTableEditorPanel'
6
6
  import Filters from '../Filters'
7
7
  import { TableConfig } from './types/TableConfig'
8
+ import { filterVizData } from '../../helpers/filterVizData'
8
9
 
9
10
  type StandAloneProps = {
10
11
  visualizationKey: string
@@ -14,34 +15,12 @@ type StandAloneProps = {
14
15
  updateConfig?: (Visualization) => void
15
16
  }
16
17
 
17
- // filterData is copied from ./packages/chart/src/helpers/filterData.ts
18
- // consider moving this to a shared location
19
- const filterData = (filters, data) => {
20
- if (!filters) return data
21
- const filteredData: any[] = []
22
-
23
- data.forEach(row => {
24
- let add = true
25
- filters
26
- .filter(filter => filter.type !== 'url')
27
- .forEach(filter => {
28
- if (row[filter.columnName] != filter.active) {
29
- add = false
30
- }
31
- })
32
-
33
- if (add) filteredData.push(row)
34
- })
35
-
36
- return filteredData
37
- }
38
-
39
18
  const DataTableStandAlone: React.FC<StandAloneProps> = ({ visualizationKey, config, updateConfig, viewport, isEditor }) => {
40
- const [filteredData, setFilteredData] = useState<Record<string, any>[]>(filterData(config.filters, config.formattedData))
19
+ const [filteredData, setFilteredData] = useState<Record<string, any>[]>(filterVizData(config.filters, config.formattedData))
41
20
 
42
21
  useEffect(() => {
43
22
  // when using editor changes to filter should update the data
44
- setFilteredData(filterData(config.filters, config.formattedData))
23
+ setFilteredData(filterVizData(config.filters, config?.formattedData?.length > 0 ? config.formattedData : config.data))
45
24
  }, [config.filters])
46
25
 
47
26
  if (isEditor)
@@ -53,7 +32,7 @@ const DataTableStandAlone: React.FC<StandAloneProps> = ({ visualizationKey, conf
53
32
 
54
33
  return (
55
34
  <>
56
- <Filters config={config} setConfig={updateConfig} setFilteredData={setFilteredData} filterData={filterData} filteredData={filteredData} excludedData={config.formattedData} />
35
+ <Filters config={config} setConfig={updateConfig} setFilteredData={setFilteredData} filteredData={filteredData} excludedData={config.formattedData} />
57
36
  <DataTable expandDataTable={true} config={config} rawData={config.data} runtimeData={filteredData} tabbingId={visualizationKey} tableTitle={config.table.label} viewport={viewport || 'lg'} />
58
37
  </>
59
38
  )
@@ -1,4 +1,4 @@
1
- import { AccordionItem, AccordionItemButton, AccordionItemHeading, AccordionItemPanel } from 'react-accessible-accordion'
1
+ import { Accordion, AccordionItem, AccordionItemButton, AccordionItemHeading, AccordionItemPanel } from 'react-accessible-accordion'
2
2
  import DataTableEditor from '../../EditorPanel/DataTableEditor'
3
3
  import { Visualization } from '@cdc/core/types/Visualization'
4
4
  import { updateFieldFactory } from '@cdc/core/helpers/updateFieldFactory'
@@ -25,9 +25,9 @@ const DataTableEditorPanel: React.FC<DataTableEditorProps> = ({ config, updateCo
25
25
  })
26
26
  }
27
27
 
28
- const columns = Object.keys(config.originalFormattedData[0] || {})
28
+ const columns = Object.keys(config.originalFormattedData?.[0] || {})
29
29
  return (
30
- <>
30
+ <Accordion allowZeroExpanded={true}>
31
31
  <AccordionItem>
32
32
  <AccordionItemHeading>
33
33
  <AccordionItemButton>Filters</AccordionItemButton>
@@ -52,7 +52,7 @@ const DataTableEditorPanel: React.FC<DataTableEditorProps> = ({ config, updateCo
52
52
  <DataTableEditor config={config} columns={columns} updateField={updateField} isDashboard={true} />
53
53
  </AccordionItemPanel>
54
54
  </AccordionItem>
55
- </>
55
+ </Accordion>
56
56
  )
57
57
  }
58
58
 
@@ -1,7 +1,7 @@
1
1
  import Icon from '../../ui/Icon'
2
+ import { fontSizes } from '../../../helpers/cove/fontSettings'
2
3
 
3
4
  const ExpandCollapse = ({ expanded, setExpanded, tableTitle, fontSize, viewport }) => {
4
- const fontSizes = { small: 16, medium: 18, large: 20 }
5
5
  const titleFontSize = ['sm', 'xs', 'xxs'].includes(viewport) ? '13px' : `${fontSizes[fontSize]}px`
6
6
  return (
7
7
  <div
@@ -6,6 +6,7 @@ import { getChartCellValue } from './getChartCellValue'
6
6
  import { getDataSeriesColumns } from './getDataSeriesColumns'
7
7
  import { ReactNode } from 'react'
8
8
  import { CellMatrix, GroupCellMatrix } from '../../Table/types/CellMatrix'
9
+ import { getRowType } from './getRowType'
9
10
 
10
11
  type ChartRowsProps = DataTableProps & {
11
12
  rows: string[]
@@ -58,15 +59,8 @@ const chartCellArray = ({ rows, runtimeData, config, isVertical, sortBy, colorSc
58
59
  } else {
59
60
  return rows.map(row => {
60
61
  if (hasRowType) {
61
- let rowType
62
- let rowValues = []
63
- dataSeriesColumns.forEach((column, j) => {
64
- if (column.match(/row[_-]?type/i)) {
65
- rowType = getChartCellValue(row, column, config, runtimeData)
66
- } else {
67
- rowValues.push(getChartCellValue(row, column, config, runtimeData))
68
- }
69
- })
62
+ const rowType = getRowType(runtimeData[row])
63
+ const rowValues = dataSeriesColumns.map(column => getChartCellValue(row, column, config, runtimeData))
70
64
  return [rowType, ...rowValues]
71
65
  } else {
72
66
  return dataSeriesColumns.map((column, j) => getChartCellValue(row, column, config, runtimeData))
@@ -60,19 +60,23 @@ export const getChartCellValue = (row: string, column: string, config: TableConf
60
60
  const isValueMatch = String(pd.value) === String(labelValue)
61
61
  // check entered suppression column against table key
62
62
  const isColumnMatch = !pd.column || pd.column === column
63
+ const barSeriesExist = config.runtime?.barSeriesKeys?.includes(column)
64
+ const lineSeriesExist = config.runtime?.lineSeriesKeys?.includes(column)
65
+ const showSymbol = config.general.showSuppressedSymbol
63
66
  if (isValueMatch && isColumnMatch && pd.displayTable && pd.type === 'suppression') {
64
67
  switch (config.visualizationType) {
65
68
  case 'Combo':
66
- cellValue = config.runtime.barSeriesKeys.includes(column) ? pd.iconCode : config.runtime.lineSeriesKeys.includes(column) ? pd.lineCode : ''
69
+ cellValue = barSeriesExist && showSymbol ? pd.iconCode : lineSeriesExist && showSymbol ? pd.lineCode : ''
67
70
  break
68
71
  case 'Bar':
69
- cellValue = pd.iconCode
72
+ cellValue = !showSymbol ? '' : pd.iconCode
70
73
  break
71
74
  case 'Line':
72
- cellValue = pd.lineCode
75
+ cellValue = !showSymbol ? '' : pd.lineCode
73
76
  break
74
77
  }
75
78
  }
76
79
  })
77
- return cellValue
80
+ const shoMissingDataCellValue = config.general?.showMissingDataLabel && (!labelValue || labelValue === 'null')
81
+ return shoMissingDataCellValue ? 'N/A' : cellValue
78
82
  }
@@ -3,7 +3,8 @@ import _ from 'lodash'
3
3
  import { Column } from '../../../types/Column'
4
4
 
5
5
  export const getDataSeriesColumns = (config: TableConfig, isVertical: boolean, runtimeData: Object[]): string[] => {
6
- const configColumns: Record<string, Column> = _.cloneDeep(config.columns) || {}
6
+ if (config.visualizationType === 'Sankey') return Object.keys(config?.data?.[0]?.tableData[0])
7
+ const configColumns = _.cloneDeep(config.columns) || ({} as Record<string, Column>)
7
8
  const excludeColumns = Object.values(configColumns)
8
9
  .filter(column => column.dataTable === false)
9
10
  .map(column => column.name)
@@ -21,12 +22,15 @@ export const getDataSeriesColumns = (config: TableConfig, isVertical: boolean, r
21
22
  tmpSeriesColumns = isVertical ? [config.xAxis?.dataKey, config.yAxis?.dataKey] : [config.yAxis?.dataKey]
22
23
  }
23
24
 
25
+ const dataColumns = Object.keys(runtimeData[0] || {})
26
+
24
27
  // then add the additional Columns
25
- Object.keys(configColumns).forEach(function (key) {
26
- var value = configColumns[key]
28
+ Object.values(configColumns).forEach(function (value) {
29
+ if (!value.name) return
27
30
  // add if not the index AND it is enabled to be added to data table
28
31
  const alreadyAdded = tmpSeriesColumns.includes(value.name)
29
- if (value.name !== config.xAxis?.dataKey && value.dataTable === true && !alreadyAdded) {
32
+ const columnIsInData = dataColumns.includes(value.name) // null columns are excluded from data
33
+ if (value.name !== config.xAxis?.dataKey && value.dataTable === true && !alreadyAdded && columnIsInData) {
30
34
  tmpSeriesColumns.push(value.name)
31
35
  }
32
36
  })
@@ -53,6 +57,5 @@ export const getDataSeriesColumns = (config: TableConfig, isVertical: boolean, r
53
57
  })
54
58
 
55
59
  tmpSeriesColumns.sort((a, b) => columnOrderingHash[a] - columnOrderingHash[b])
56
-
57
60
  return tmpSeriesColumns
58
61
  }
@@ -0,0 +1,6 @@
1
+ import { RowType } from '../../Table/types/RowType'
2
+
3
+ export const getRowType = (row: Record<string, any>): RowType => {
4
+ const rowTypeKey = Object.keys(row).find(key => key.match(/row[_-]?type/i))
5
+ return row[rowTypeKey]
6
+ }
@@ -14,4 +14,5 @@ export type TableConfig = Visualization & {
14
14
  xAxis?: Axis
15
15
  yAxis?: Axis
16
16
  preliminaryData: PreliminaryDataItem[]
17
+ brush: { active: boolean }
17
18
  }
@@ -1,4 +1,3 @@
1
- import { AccordionItem, AccordionItemButton, AccordionItemHeading, AccordionItemPanel } from 'react-accessible-accordion'
2
1
  import Tooltip from '../ui/Tooltip'
3
2
  import Icon from '../ui/Icon'
4
3
  import { TextField } from './Inputs'
@@ -7,6 +6,7 @@ import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
7
6
  import { Column } from '../../types/Column'
8
7
  import _ from 'lodash'
9
8
  import React, { useState } from 'react'
9
+ import FieldSetWrapper from './FieldSetWrapper'
10
10
 
11
11
  interface ColumnsEditorProps {
12
12
  config: Partial<Visualization>
@@ -18,10 +18,6 @@ type OpenControls = [Record<string, boolean>, Function] // useState type
18
18
 
19
19
  const FieldSet: React.FC<ColumnsEditorProps & { colKey: string; controls: OpenControls }> = ({ config, deleteColumn, updateField, colKey, controls }) => {
20
20
  const [openControls, setOpenControls] = controls
21
- const show = openControls[colKey]
22
- const setShow = (key, value) => {
23
- setOpenControls({ ...openControls, [key]: value })
24
- }
25
21
 
26
22
  const editColumn = (key, value) => {
27
23
  if (key === 'dataTable' && value === true) {
@@ -66,31 +62,8 @@ const FieldSet: React.FC<ColumnsEditorProps & { colKey: string; controls: OpenCo
66
62
 
67
63
  const colName = config.columns[colKey]?.name
68
64
 
69
- if (!show)
70
- return (
71
- <div className='mb-1'>
72
- <button onClick={() => setShow(colKey, true)}>
73
- <Icon display='caretDown' />
74
- </button>
75
- <span> {colName ? `${colName}` : 'New Column'}</span>
76
- </div>
77
- )
78
65
  return (
79
- <fieldset className='edit-block mb-1' key={colKey}>
80
- <div className='d-flex justify-content-between'>
81
- <button onClick={() => setShow(colKey, false)}>
82
- <Icon display='caretUp' />
83
- </button>
84
- <button
85
- className='btn btn-danger btn-sm'
86
- onClick={event => {
87
- event.preventDefault()
88
- deleteColumn(colKey)
89
- }}
90
- >
91
- Remove
92
- </button>
93
- </div>
66
+ <FieldSetWrapper fieldName={colName} fieldKey={colKey} fieldType='Column' controls={controls} deleteField={() => deleteColumn(colKey)}>
94
67
  <label>
95
68
  <span className='edit-label column-heading'>Column</span>
96
69
  <select
@@ -231,7 +204,7 @@ const FieldSet: React.FC<ColumnsEditorProps & { colKey: string; controls: OpenCo
231
204
  <span className='edit-label column-heading'>Order</span>
232
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))} />
233
206
  </label>
234
- </fieldset>
207
+ </FieldSetWrapper>
235
208
  )
236
209
  }
237
210
 
@@ -1,7 +1,7 @@
1
1
  import React, { useMemo } from 'react'
2
2
  import Tooltip from '@cdc/core/components/ui/Tooltip'
3
3
  import Icon from '../ui/Icon'
4
- import { CheckBox, TextField } from './Inputs'
4
+ import { CheckBox, TextField, Select } from './Inputs'
5
5
  import MultiSelect from '../MultiSelect'
6
6
  import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
7
7
  import { Visualization } from '../../types/Visualization'
@@ -26,15 +26,13 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
26
26
  .map(([key]) => key)
27
27
  }, [config.columns])
28
28
 
29
- const getGroupableColumns = () => {
29
+ const groupPivotColumns = useMemo(() => {
30
30
  const columns: string[] = config.data.flatMap(Object.keys)
31
- const configuredColumns = Object.values(config.columns).map(col => col.name)
32
31
  const cols = _.uniq(columns).filter(key => {
33
- if (configuredColumns.includes(key)) return false
34
32
  return true
35
33
  })
36
34
  return cols
37
- }
35
+ }, [config.data])
38
36
 
39
37
  const changeGroupBy = (value: string) => {
40
38
  if (value === PLACEHOLDER) value = undefined
@@ -123,6 +121,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
123
121
  }
124
122
  />
125
123
  )}
124
+
126
125
  {config.type !== 'table' && (
127
126
  <TextField
128
127
  value={config.table.indexLabel}
@@ -163,7 +162,7 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
163
162
  />
164
163
  <CheckBox value={config.table.limitHeight} section='table' fieldName='limitHeight' label=' Limit Table Height' updateField={updateField} />
165
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} />}
166
- <MultiSelect key={excludedColumns.join('') + 'excluded'} options={dataColumns.map(c => ({ label: c, value: c }))} selected={excludedColumns} fieldName='dataTable' label='Exclude Columns' section='columns' updateField={excludeColumns} />
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} />}
167
166
  <CheckBox value={config.table.collapsible} fieldName='collapsible' label=' Collapsible' section='table' updateField={updateField} />
168
167
  {config.table.collapsible !== false && <CheckBox value={config.table.expanded} fieldName='expanded' label=' Expanded by Default' section='table' updateField={updateField} />}
169
168
  {isDashboard && config.type !== 'table' && <CheckBox value={config.table.showDataTableLink} fieldName='showDataTableLink' label='Show Data Table Name & Link' section='table' updateField={updateField} />}
@@ -173,29 +172,74 @@ const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDash
173
172
  <span className='edit-label column-heading'>Table Cell Min Width</span>
174
173
  <input type='number' value={config.table.cellMinWidth ? config.table.cellMinWidth : 0} onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)} />
175
174
  </label>
176
- <label>
177
- <span className='edit-label column-heading'>
178
- Group By{' '}
175
+ {config?.visualizationType !== 'Sankey' && (
176
+ <label>
177
+ <span className='edit-label column-heading'>
178
+ Group By{' '}
179
+ <Tooltip style={{ textTransform: 'none' }}>
180
+ <Tooltip.Target>
181
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
182
+ </Tooltip.Target>
183
+ <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>
185
+ </Tooltip.Content>
186
+ </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>
200
+ )}
201
+ <Select
202
+ label='Pivot Column: '
203
+ tooltip={
179
204
  <Tooltip style={{ textTransform: 'none' }}>
180
205
  <Tooltip.Target>
181
206
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
182
207
  </Tooltip.Target>
183
208
  <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>
209
+ <p>Select a Column whos data values will be pivoted to Column Values.</p>
185
210
  </Tooltip.Content>
186
211
  </Tooltip>
187
- </span>
188
- <select
189
- value={config.table.groupBy}
190
- onChange={event => {
191
- changeGroupBy(event.target.value)
192
- }}
193
- >
194
- {[PLACEHOLDER, ...getGroupableColumns()].map(option => (
195
- <option key={option}>{option}</option>
196
- ))}
197
- </select>
198
- </label>
212
+ }
213
+ value={config.table.pivot?.columnName}
214
+ options={groupPivotColumns.filter(col => col !== config.table.groupBy && col !== config.table.pivot?.valueColumn)}
215
+ initial='-Select-'
216
+ section='table'
217
+ subsection='pivot'
218
+ fieldName='columnName'
219
+ updateField={updateField}
220
+ />
221
+ {config.table.pivot?.columnName && (
222
+ <Select
223
+ label='Pivot Value Column: '
224
+ tooltip={
225
+ <Tooltip style={{ textTransform: 'none' }}>
226
+ <Tooltip.Target>
227
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
228
+ </Tooltip.Target>
229
+ <Tooltip.Content>
230
+ <p>The column whos values will be pivoted under the column selected as the Filter.</p>
231
+ </Tooltip.Content>
232
+ </Tooltip>
233
+ }
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
+ />
242
+ )}
199
243
  </>
200
244
  )
201
245
  }
@@ -0,0 +1,51 @@
1
+ import Icon from '../ui/Icon'
2
+
3
+ type OpenControls = [Record<string, boolean>, Function] // useState type
4
+
5
+ type FieldSetProps = {
6
+ fieldName: string
7
+ fieldKey: string | number
8
+ fieldType: string
9
+ controls: OpenControls
10
+ deleteField: Function
11
+ children: React.ReactNode
12
+ }
13
+
14
+ const FieldSet: React.FC<FieldSetProps> = ({ fieldName, fieldKey, fieldType, controls, deleteField, children }) => {
15
+ const [openControls, setOpenControls] = controls
16
+ const show = openControls[fieldKey]
17
+ const setShow = (key, value) => {
18
+ setOpenControls({ ...openControls, [key]: value })
19
+ }
20
+
21
+ if (!show)
22
+ return (
23
+ <div className='mb-1'>
24
+ <button onClick={() => setShow(fieldKey, true)}>
25
+ <Icon display='caretDown' />
26
+ </button>
27
+ <span> {fieldName ? `${fieldName}` : 'New ' + fieldType}</span>
28
+ </div>
29
+ )
30
+ return (
31
+ <fieldset className='edit-block mb-1' key={fieldKey}>
32
+ <div className='d-flex justify-content-between'>
33
+ <button onClick={() => setShow(fieldKey, false)}>
34
+ <Icon display='caretUp' />
35
+ </button>
36
+ <button
37
+ className='btn btn-danger btn-sm'
38
+ onClick={event => {
39
+ event.preventDefault()
40
+ deleteField()
41
+ }}
42
+ >
43
+ Remove
44
+ </button>
45
+ </div>
46
+ {children}
47
+ </fieldset>
48
+ )
49
+ }
50
+
51
+ export default FieldSet