@cdc/core 4.24.1 → 4.24.3

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 (71) hide show
  1. package/assets/icon-sankey.svg +1 -0
  2. package/assets/icon-table.svg +1 -0
  3. package/components/DataTable/DataTable.tsx +44 -15
  4. package/components/DataTable/DataTableStandAlone.tsx +15 -0
  5. package/components/DataTable/components/CellAnchor.tsx +3 -1
  6. package/components/DataTable/components/ChartHeader.tsx +48 -12
  7. package/components/DataTable/components/DataTableEditorPanel.tsx +42 -0
  8. package/components/DataTable/components/MapHeader.tsx +10 -5
  9. package/components/DataTable/helpers/customColumns.ts +4 -2
  10. package/components/DataTable/helpers/customSort.ts +9 -0
  11. package/components/DataTable/helpers/getChartCellValue.ts +5 -3
  12. package/components/DataTable/helpers/getDataSeriesColumns.ts +10 -2
  13. package/components/DataTable/helpers/getSeriesName.ts +15 -20
  14. package/components/DataTable/helpers/mapCellMatrix.tsx +4 -0
  15. package/components/DataTable/types/TableConfig.ts +12 -37
  16. package/components/EditorPanel/ColumnsEditor.tsx +311 -0
  17. package/components/EditorPanel/DataTableEditor.tsx +27 -28
  18. package/components/Filters.jsx +35 -16
  19. package/components/MultiSelect/MultiSelect.tsx +39 -20
  20. package/components/MultiSelect/multiselect.styles.css +44 -27
  21. package/components/NestedDropdown/NestedDropdown.tsx +257 -0
  22. package/components/NestedDropdown/index.ts +1 -0
  23. package/components/NestedDropdown/nesteddropdown.styles.css +70 -0
  24. package/components/Table/Table.tsx +1 -1
  25. package/components/_stories/MultiSelect.stories.tsx +10 -1
  26. package/components/_stories/NestedDropdown.stories.tsx +58 -0
  27. package/components/createBarElement.jsx +117 -0
  28. package/components/elements/ScreenReaderText.tsx +8 -0
  29. package/components/elements/SkipTo.tsx +14 -0
  30. package/components/ui/Icon.tsx +5 -1
  31. package/components/ui/Title/Title.scss +7 -1
  32. package/components/ui/Title/index.tsx +3 -3
  33. package/components/ui/Tooltip.jsx +1 -1
  34. package/components/ui/_stories/Colors.stories.tsx +92 -0
  35. package/components/ui/_stories/Icon.stories.tsx +17 -10
  36. package/data/colorPalettes.js +1 -6
  37. package/helpers/cove/accessibility.ts +23 -0
  38. package/helpers/cove/date.ts +19 -0
  39. package/helpers/coveUpdateWorker.js +4 -0
  40. package/helpers/fetchRemoteData.js +5 -5
  41. package/helpers/getViewport.ts +23 -0
  42. package/helpers/isDomainExternal.js +14 -0
  43. package/helpers/isSolr.js +13 -0
  44. package/helpers/queryStringUtils.js +26 -0
  45. package/helpers/tests/updateFieldFactory.test.ts +89 -0
  46. package/helpers/updateFieldFactory.ts +38 -0
  47. package/helpers/useDataVizClasses.js +2 -2
  48. package/helpers/ver/4.24.3.js +25 -0
  49. package/helpers/withDevTools.ts +50 -0
  50. package/package.json +4 -3
  51. package/styles/_data-table.scss +2 -20
  52. package/styles/_global-variables.scss +75 -0
  53. package/styles/base.scss +97 -69
  54. package/types/Action.ts +1 -0
  55. package/types/Axis.ts +3 -0
  56. package/types/BaseVisualizationType.ts +1 -0
  57. package/types/BoxPlot.ts +21 -0
  58. package/types/Column.ts +1 -0
  59. package/types/ConfidenceInterval.ts +1 -0
  60. package/types/General.ts +9 -0
  61. package/types/Legend.ts +18 -0
  62. package/types/Region.ts +10 -0
  63. package/types/Runtime.ts +3 -1
  64. package/types/Table.ts +5 -2
  65. package/types/UpdateFieldFunc.ts +1 -1
  66. package/types/ViewPort.ts +2 -0
  67. package/types/Visualization.ts +23 -5
  68. package/types/WCMSProps.ts +11 -0
  69. package/components/DataTable/components/SkipNav.tsx +0 -7
  70. package/helpers/cove/date.js +0 -9
  71. package/helpers/getViewport.js +0 -21
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96.31 82.55"><defs><style>.c{stroke-linecap:round;stroke-width:3px;}.c,.d,.e{stroke:#000;}.e{fill:none;}</style></defs><g id="a"/><g id="b"><path class="c" d="M6.88,39.98V5.57"/><path class="c" d="M89.42,22.98V5.57"/><path class="c" d="M89.42,52.79V27.38"/><path class="c" d="M89.42,76v-18.41"/><path class="c" d="M6.88,76v-28.43"/><rect class="e" x="12.3" y="5.98" width="71.62" height="9.69"/><path class="d" d="M83.92,16.67s-11.46,1.07-27.63,11.61-27.1,16.32-35.66,17.97c-4.59,.88-8.34,.72-8.34,.72v5.31s3.75,.17,8.34-.72c8.56-1.65,19.49-7.42,35.66-17.97s27.63-11.61,27.63-11.61v-5.31Z"/><path class="e" d="M12.3,16.67s41.36,10.71,71.62,10.71v8.71s-31.58,1.88-71.62-10.71v-8.71Z"/><path class="d" d="M83.92,53.29s-13.18,1.49-35.77,9.06c0,0-18.05,7.37-35.85,7.37v-15.89c17.8,0,35.85-7.37,35.85-7.37,22.59-7.57,35.77-9.06,35.77-9.06v15.89Z"/><rect class="d" x="12.3" y="71" width="71.62" height="5.44"/><path class="e" d="M83.92,57.58l-8.82-2.38c-6.78-1.93-13.27-4.73-19.33-8.34l-14.69-8.76c-5.94-3.54-12.3-6.31-18.93-8.25l-9.85-2.88v12.14l9.85,2.88c6.64,1.94,13,4.71,18.93,8.25l14.69,8.76c6.05,3.61,12.55,6.41,19.33,8.34l8.82,2.38v-12.14Z"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 256V160H224v96H64zm0 64H224v96H64V320zm224 96V320H448v96H288zM448 256H288V160H448v96zM64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64z"/></svg>
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState, useMemo } from 'react'
2
+ import { timeParse } from 'd3-time-format'
2
3
 
3
4
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
4
5
  import MediaControls from '@cdc/core/components/MediaControls'
@@ -8,7 +9,7 @@ import { customSort } from './helpers/customSort'
8
9
  import ChartHeader from './components/ChartHeader'
9
10
  import BoxplotHeader from './components/BoxplotHeader'
10
11
  import MapHeader from './components/MapHeader'
11
- import SkipNav from './components/SkipNav'
12
+ import SkipTo from '../elements/SkipTo'
12
13
  import ExpandCollapse from './components/ExpandCollapse'
13
14
  import mapCellMatrix from './helpers/mapCellMatrix'
14
15
  import Table from '../Table'
@@ -17,13 +18,12 @@ import regionCellMatrix from './helpers/regionCellMatrix'
17
18
  import boxplotCellMatrix from './helpers/boxplotCellMatrix'
18
19
  import customColumns from './helpers/customColumns'
19
20
  import { TableConfig } from './types/TableConfig'
21
+ import { Column } from '../../types/Column'
20
22
 
21
23
  export type DataTableProps = {
22
24
  applyLegendToRow?: Function
23
25
  colorScale?: Function
24
- columns?: { navigate: { name: string } }
25
- // determines if columns should be wrapped in the table
26
- wrapColumns?: boolean
26
+ columns?: Record<string, Column>
27
27
  config: TableConfig
28
28
  dataConfig?: Object
29
29
  displayDataAsText?: Function
@@ -32,21 +32,27 @@ export type DataTableProps = {
32
32
  formatLegendLocation?: Function
33
33
  groupBy?: string
34
34
  headerColor?: string
35
+ imageRef?: string
35
36
  indexTitle?: string
37
+ isDebug?: boolean
38
+ isEditor?: boolean
36
39
  navigationHandler?: Function
40
+ outerContainerRef?: Function
37
41
  rawData: Object[]
38
- runtimeData: Object[] | Record<string, Object> // UNSAFE
42
+ runtimeData: Object[] & Record<string, Object>
39
43
  setFilteredCountryCode?: Function // used for Maps only
44
+ showDownloadButton?: boolean
40
45
  tabbingId: string
41
46
  tableTitle: string
42
47
  viewport: string
43
48
  vizTitle?: string
49
+ // determines if columns should be wrapped in the table
50
+ wrapColumns?: boolean
44
51
  }
45
52
 
46
53
  /* eslint-disable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/no-static-element-interactions */
47
54
  const DataTable = (props: DataTableProps) => {
48
55
  const { config, dataConfig, tableTitle, vizTitle, rawData, runtimeData, headerColor, expandDataTable, columns, viewport, formatLegendLocation, tabbingId, wrapColumns } = props
49
-
50
56
  const [expanded, setExpanded] = useState(expandDataTable)
51
57
 
52
58
  const [sortBy, setSortBy] = useState<any>({ column: config.type === 'map' ? 'geo' : 'date', asc: false, colIndex: null })
@@ -84,7 +90,7 @@ const DataTable = (props: DataTableProps) => {
84
90
  case 'Box Plot':
85
91
  if (!config.boxplot) return <Loading />
86
92
  break
87
- case 'Line' || 'Bar' || 'Combo' || 'Pie' || 'Deviation Bar' || 'Paired Bar':
93
+ case 'Line' || 'Bar' || 'Combo' || 'Pie' || 'Deviation Bar' || 'Paired Bar' || 'Sankey' || 'Table':
88
94
  if (!runtimeData) return <Loading />
89
95
  break
90
96
  default:
@@ -92,9 +98,9 @@ const DataTable = (props: DataTableProps) => {
92
98
  break
93
99
  }
94
100
 
95
- const _runtimeData = config.table.customTableConfig ? customColumns(runtimeData, config.table.excludeColumns) : runtimeData
101
+ const _runtimeData = config.table.customTableConfig ? customColumns(rawData, config.table.excludeColumns) : runtimeData
96
102
 
97
- const rawRows = Object.keys(_runtimeData)
103
+ const rawRows = Object.keys(_runtimeData).filter(column => column != 'columns')
98
104
  const rows = isVertical
99
105
  ? rawRows.sort((a, b) => {
100
106
  let dataA
@@ -108,6 +114,10 @@ const DataTable = (props: DataTableProps) => {
108
114
  dataA = _runtimeData[a][sortBy.column]
109
115
  dataB = _runtimeData[b][sortBy.column]
110
116
  }
117
+ if (!dataA && !dataB && config.type === 'chart' && config.xAxis && config.xAxis.type === 'date-time') {
118
+ dataA = timeParse(config.runtime.xAxis.dateParseFormat)(_runtimeData[a][config.xAxis.dataKey])
119
+ dataB = timeParse(config.runtime.xAxis.dateParseFormat)(_runtimeData[b][config.xAxis.dataKey])
120
+ }
111
121
  return dataA && dataB ? customSort(dataA, dataB, sortBy, config) : 0
112
122
  })
113
123
  : rawRows
@@ -117,7 +127,7 @@ const DataTable = (props: DataTableProps) => {
117
127
  OverflowY: 'scroll'
118
128
  }
119
129
 
120
- const hasRowType = !!Object.keys(rawData[0] || {}).find((v: string) => v.match(/row[_-]?type/i))
130
+ const hasRowType = !!Object.keys(rawData?.[0] || {}).find((v: string) => v.match(/row[_-]?type/i))
121
131
 
122
132
  const caption = useMemo(() => {
123
133
  if (config.type === 'map') {
@@ -131,12 +141,25 @@ const DataTable = (props: DataTableProps) => {
131
141
  // If a relative region is found we don't want to display the data table.
132
142
  // Takes backwards compatibility into consideration, ie !region.toType || !region.fromType
133
143
  const noRelativeRegions = config?.regions?.every(region => {
134
- return (region.toType === 'Fixed' && region.fromType === 'Fixed') || (!region.toType && !region.fromType) || (!region.toType && region.fromType === 'Fixed') || (!region.fromType && region.toType === 'Fixed')
144
+ const toTypeFixed = region.toType === 'Fixed'
145
+ const fromTypeFixed = region.fromType === 'Fixed'
146
+ const toIsNotSet = !region.toType
147
+ const fromIsNotSet = !region.fromType
148
+ const BothFixed = toTypeFixed && fromTypeFixed
149
+ const NeitherFixed = toIsNotSet && fromIsNotSet
150
+ const ToFixedFromNotSet = toTypeFixed && fromIsNotSet
151
+ const FromFixedToNotSet = fromTypeFixed && toIsNotSet
152
+
153
+ return BothFixed || NeitherFixed || ToFixedFromNotSet || FromFixedToNotSet
135
154
  })
136
155
 
137
156
  // prettier-ignore
138
157
  const tableData = useMemo(() => (
139
- config.visualizationType === 'Pie'
158
+ config.data?.[0]?.tableData
159
+ ? config.data?.[0]?.tableData
160
+ : config.visualizationType === 'Sankey'
161
+ ? config.data?.[0]?.tableData
162
+ : config.visualizationType === 'Pie'
140
163
  ? [config.yAxis.dataKey]
141
164
  : config.visualizationType === 'Box Plot'
142
165
  ? Object.entries(config.boxplot.tableData[0])
@@ -158,10 +181,10 @@ const DataTable = (props: DataTableProps) => {
158
181
  <ErrorBoundary component='DataTable'>
159
182
  <MediaControls.Section classes={['download-links']}>
160
183
  <MediaControls.Link config={config} dashboardDataConfig={dataConfig} />
161
- {(config.table.download || config.general?.showDownloadButton) && <DownloadButton rawData={getDownloadData()} fileName={`${vizTitle || 'data-table'}.csv`} headerColor={headerColor} skipId={skipId} />}
184
+ {(config.table.download || config.general?.showDownloadButton) && <DownloadButton rawData={getDownloadData()} fileName={`${vizTitle || 'data-table'}.csv`} headerColor={headerColor} />}
162
185
  </MediaControls.Section>
163
186
  <section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
164
- <SkipNav skipId={skipId} />
187
+ <SkipTo skipId={skipId} skipMessage='Skip Data Table' />
165
188
  <ExpandCollapse expanded={expanded} setExpanded={setExpanded} tableTitle={tableTitle} />
166
189
  <div className='table-container' style={limitHeight}>
167
190
  <Table
@@ -194,6 +217,9 @@ const DataTable = (props: DataTableProps) => {
194
217
  )}
195
218
  </div>
196
219
  </section>
220
+ <div id={skipId} className='cdcdataviz-sr-only'>
221
+ Skipped data table.
222
+ </div>
197
223
  </ErrorBoundary>
198
224
  )
199
225
  } else {
@@ -201,7 +227,7 @@ const DataTable = (props: DataTableProps) => {
201
227
  return (
202
228
  <ErrorBoundary component='DataTable'>
203
229
  <section id={tabbingId.replace('#', '')} className={`data-table-container ${viewport}`} aria-label={accessibilityLabel}>
204
- <SkipNav skipId={skipId} />
230
+ <SkipTo skipId={skipId} skipMessage='Skip Data Table' />
205
231
  <ExpandCollapse expanded={expanded} setExpanded={setExpanded} tableTitle={tableTitle} />
206
232
  <div className='table-container' style={limitHeight}>
207
233
  <Table
@@ -215,6 +241,9 @@ const DataTable = (props: DataTableProps) => {
215
241
  />
216
242
  </div>
217
243
  </section>
244
+ <div id={skipId} className='cdcdataviz-sr-only'>
245
+ Skipped data table.
246
+ </div>
218
247
  </ErrorBoundary>
219
248
  )
220
249
  }
@@ -0,0 +1,15 @@
1
+ import { ViewPort } from '../../types/ViewPort'
2
+ import { Visualization } from '../../types/Visualization'
3
+ import DataTable from './DataTable'
4
+
5
+ type StandAloneProps = {
6
+ visualizationKey: string
7
+ config: Visualization
8
+ viewport?: ViewPort
9
+ }
10
+
11
+ const DataTableStandAlone: React.FC<StandAloneProps> = ({ visualizationKey, config, viewport }) => {
12
+ return <DataTable expandDataTable={true} config={config} rawData={config.data} runtimeData={config.formattedData} tabbingId={visualizationKey} tableTitle={config.table.label} viewport={viewport || 'lg'} />
13
+ }
14
+
15
+ export default DataTableStandAlone
@@ -1,4 +1,6 @@
1
1
  import ExternalIcon from '@cdc/core/assets/external-link.svg'
2
+ import isDomainExternal from '@cdc/core/helpers/isDomainExternal'
3
+
2
4
  // Optionally wrap cell with anchor if config defines a navigation url
3
5
  const CellAnchor = ({ markup, row, columns, navigationHandler, mapZoomHandler }) => {
4
6
  if (columns.navigate && row[columns.navigate.name]) {
@@ -16,7 +18,7 @@ const CellAnchor = ({ markup, row, columns, navigationHandler, mapZoomHandler })
16
18
  }}
17
19
  >
18
20
  {markup}
19
- <ExternalIcon className='inline-icon' />
21
+ {isDomainExternal(row[columns.navigate.name]) && <ExternalIcon className='inline-icon' />}
20
22
  </span>
21
23
  )
22
24
  } else if (mapZoomHandler) {
@@ -2,6 +2,7 @@ import { getChartCellValue } from '../helpers/getChartCellValue'
2
2
  import { getSeriesName } from '../helpers/getSeriesName'
3
3
  import { getDataSeriesColumns } from '../helpers/getDataSeriesColumns'
4
4
  import { DownIcon, UpIcon } from './Icons'
5
+ import ScreenReaderText from '@cdc/core/components/elements/ScreenReaderText'
5
6
 
6
7
  type ChartHeaderProps = { data; isVertical; config; setSortBy; sortBy; groupBy?; hasRowType? }
7
8
 
@@ -16,6 +17,45 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, groupBy, has
16
17
  dataSeriesColumns = groupHeaderRemoved
17
18
  }
18
19
  }
20
+
21
+ const handleHeaderClasses = (sortBy, text) => {
22
+ let classes = ['sort']
23
+ if (sortBy.column === text && sortBy.asc) {
24
+ classes.push('sort-asc')
25
+ }
26
+ if (sortBy.column === text && sortBy.desc) {
27
+ classes.push('sort-desc')
28
+ }
29
+ return classes.join(' ')
30
+ }
31
+
32
+ const ScreenReaderSortByText = ({ text, config, sortBy }) => {
33
+ const notApplicableText = 'Not Applicable'
34
+ let columnHeaderText = `${text} `
35
+ if (text !== '__series__' || text !== '') {
36
+ columnHeaderText = `${text} `
37
+ }
38
+
39
+ if ((text === '__series__' || text === '') && !config.table.indexLabel) {
40
+ columnHeaderText = notApplicableText
41
+ }
42
+
43
+ if ((text === '__series__' || text === '') && config.table.indexLabel) {
44
+ columnHeaderText = config.table.indexLabel
45
+ }
46
+
47
+ if (columnHeaderText === notApplicableText) return
48
+
49
+ return <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'} order`}</span>
50
+ }
51
+
52
+ const ColumnHeadingText = ({ column, text, config }) => {
53
+ let notApplicableText = 'Not Applicable'
54
+ if (text === '__series__' && config.table.indexLabel) return `${config.table.indexLabel} `
55
+ if (text === '__series__' && !config.table.indexLabel) return <ScreenReaderText as='span'>{notApplicableText}</ScreenReaderText>
56
+ return text
57
+ }
58
+
19
59
  if (isVertical) {
20
60
  if (hasRowType) {
21
61
  // find the row type column and place it at the beginning of the array
@@ -25,6 +65,7 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, groupBy, has
25
65
  dataSeriesColumns.splice(rowTypeIndex, 1)
26
66
  }
27
67
  }
68
+
28
69
  return (
29
70
  <tr>
30
71
  {dataSeriesColumns.map((column, index) => {
@@ -35,7 +76,6 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, groupBy, has
35
76
  style={{ minWidth: (config.table.cellMinWidth || 0) + 'px' }}
36
77
  key={`col-header-${column}__${index}`}
37
78
  tabIndex={0}
38
- title={text}
39
79
  role='columnheader'
40
80
  scope='col'
41
81
  onClick={() => {
@@ -48,14 +88,12 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, groupBy, has
48
88
  setSortBy({ column, asc: sortBy.column === column ? !sortBy.asc : false, colIndex: index })
49
89
  }
50
90
  }}
51
- className={sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
91
+ className={handleHeaderClasses(sortBy, text)}
52
92
  {...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
53
93
  >
54
- {text}
94
+ <ColumnHeadingText text={text} column={column} config={config} />
55
95
  {column === sortBy.column && <span className={'sort-icon'}>{!sortBy.asc ? <UpIcon /> : <DownIcon />}</span>}
56
- <button>
57
- <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
58
- </button>
96
+ <ScreenReaderSortByText sortBy={sortBy} config={config} text={text} />
59
97
  </th>
60
98
  )
61
99
  })}
@@ -68,12 +106,12 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, groupBy, has
68
106
  {['__series__', ...Object.keys(data)].slice(sliceVal).map((row, index) => {
69
107
  let column = config.xAxis?.dataKey
70
108
  let text = row !== '__series__' ? getChartCellValue(row, column, config, data) : '__series__'
109
+
71
110
  return (
72
111
  <th
73
112
  style={{ minWidth: (config.table.cellMinWidth || 0) + 'px' }}
74
113
  key={`col-header-${text}__${index}`}
75
114
  tabIndex={0}
76
- title={text}
77
115
  role='columnheader'
78
116
  scope='col'
79
117
  onClick={() => {
@@ -84,14 +122,12 @@ const ChartHeader = ({ data, isVertical, config, setSortBy, sortBy, groupBy, has
84
122
  setSortBy({ column: text, asc: sortBy.column === text ? !sortBy.asc : false, colIndex: index })
85
123
  }
86
124
  }}
87
- className={sortBy.column === text ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
125
+ className={handleHeaderClasses(sortBy, text)}
88
126
  {...(sortBy.column === text ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
89
127
  >
90
- {text === '__series__' ? '' : text}
128
+ <ColumnHeadingText text={text} column={column} config={config} />
91
129
  {index === sortBy.colIndex && <span className={'sort-icon'}>{!sortBy.asc ? <UpIcon /> : <DownIcon />}</span>}
92
- <button>
93
- <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === text ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
94
- </button>
130
+ <ScreenReaderSortByText text={text} config={config} sortBy={sortBy} />
95
131
  </th>
96
132
  )
97
133
  })}
@@ -0,0 +1,42 @@
1
+ import { AccordionItem, AccordionItemButton, AccordionItemHeading, AccordionItemPanel } from 'react-accessible-accordion'
2
+ import DataTableEditor from '../../EditorPanel/DataTableEditor'
3
+ import { Visualization } from '@cdc/core/types/Visualization'
4
+ import { updateFieldFactory } from '@cdc/core/helpers/updateFieldFactory'
5
+ import { useMemo } from 'react'
6
+ import ColumnsEditor from '../../EditorPanel/ColumnsEditor'
7
+
8
+ type DataTableEditorProps = {
9
+ config: Visualization
10
+ updateConfig: Function
11
+ }
12
+
13
+ const DataTableEditorPanel: React.FC<DataTableEditorProps> = ({ config, updateConfig }) => {
14
+ const updateField = useMemo(() => updateFieldFactory(config, updateConfig), [JSON.stringify(config)])
15
+ const deleteColumn = columnName => {
16
+ const newColumns = config.columns
17
+
18
+ delete newColumns[columnName]
19
+
20
+ updateConfig({
21
+ ...config,
22
+ columns: newColumns
23
+ })
24
+ }
25
+
26
+ const columns = Object.keys(config.columns || {})
27
+ return (
28
+ <>
29
+ <ColumnsEditor config={config} updateField={updateField} deleteColumn={deleteColumn} />
30
+ <AccordionItem>
31
+ <AccordionItemHeading>
32
+ <AccordionItemButton>Data Table</AccordionItemButton>
33
+ </AccordionItemHeading>
34
+ <AccordionItemPanel>
35
+ <DataTableEditor config={config} columns={columns} updateField={updateField} isDashboard={true} />
36
+ </AccordionItemPanel>
37
+ </AccordionItem>
38
+ </>
39
+ )
40
+ }
41
+
42
+ export default DataTableEditorPanel
@@ -1,11 +1,19 @@
1
1
  import { DataTableProps } from '../DataTable'
2
2
  import { DownIcon, UpIcon } from './Icons'
3
+ import ScreenReaderText from '../../elements/ScreenReaderText'
3
4
 
4
5
  type MapHeaderProps = DataTableProps & {
5
6
  sortBy: { column; asc }
6
7
  setSortBy: Function
7
8
  }
8
9
 
10
+ const ColumnHeadingText = ({ column, text, config }) => {
11
+ let notApplicableText = 'Not Applicable'
12
+ if (text === '__series__' && config.table.indexLabel) return `${config.table.indexLabel} `
13
+ if (text === '__series__' && !config.table.indexLabel) return <ScreenReaderText as='span'>{notApplicableText}</ScreenReaderText>
14
+ return text
15
+ }
16
+
9
17
  const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy }: MapHeaderProps) => {
10
18
  return (
11
19
  <tr>
@@ -27,7 +35,6 @@ const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy }: MapHeader
27
35
  key={`col-header-${column}__${index}`}
28
36
  id={column}
29
37
  tabIndex={0}
30
- title={text}
31
38
  role='columnheader'
32
39
  scope='col'
33
40
  onClick={() => {
@@ -41,11 +48,9 @@ const MapHeader = ({ columns, config, indexTitle, sortBy, setSortBy }: MapHeader
41
48
  className={sortBy.column === column ? (sortBy.asc ? 'sort sort-asc' : 'sort sort-desc') : 'sort'}
42
49
  {...(sortBy.column === column ? (sortBy.asc ? { 'aria-sort': 'ascending' } : { 'aria-sort': 'descending' }) : null)}
43
50
  >
44
- {text}
51
+ <ColumnHeadingText text={text} config={config} column={column} />
45
52
  {sortBy.column === column && <span className={'sort-icon'}>{!sortBy.asc ? <UpIcon /> : <DownIcon />}</span>}
46
- <button>
47
- <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} `} order</span>
48
- </button>
53
+ <span className='cdcdataviz-sr-only'>{`Sort by ${text} in ${sortBy.column === column ? (!sortBy.asc ? 'descending' : 'ascending') : 'descending'} order`}</span>
49
54
  </th>
50
55
  )
51
56
  })}
@@ -1,5 +1,7 @@
1
+ type RuntimeData = Object[] & Record<string, Object>
2
+
1
3
  // removes null and excluded columns
2
- const customColumns = (runtimeData: Object[] | Record<string, Object>, excludeColumns: string[] = []): Object[] | Record<string, Object> => {
4
+ const customColumns = (runtimeData: Object[] | RuntimeData, excludeColumns: string[] = []): RuntimeData => {
3
5
  if (!Array.isArray(runtimeData)) {
4
6
  // currently we don't support Record types
5
7
  return runtimeData
@@ -18,7 +20,7 @@ const customColumns = (runtimeData: Object[] | Record<string, Object>, excludeCo
18
20
  if (runtimeDataMemo[key] === true) row[key] = d[key]
19
21
  })
20
22
  return row
21
- })
23
+ }) as RuntimeData
22
24
  }
23
25
  }
24
26
 
@@ -8,7 +8,16 @@ export const customSort = (a, b, sortBy, config) => {
8
8
  if (config.type === 'map') {
9
9
  valueA = standardizeStateName(a)
10
10
  valueB = standardizeStateName(b)
11
+ // sort for Regions table for Map
12
+ if (String(valueA).toLowerCase().includes('region') && String(valueB).toLowerCase().includes('region')) {
13
+ const numberA = parseInt(a.match(/\d+$/)[0], 10)
14
+ const numberB = parseInt(b.match(/\d+$/)[0], 10)
15
+
16
+ // Compare the numeric parts
17
+ return !sortBy.asc ? Number(numberA) - Number(numberB) : Number(numberB) - Number(numberA)
18
+ }
11
19
  }
20
+
12
21
  // Treat booleans and nulls as an empty string
13
22
  valueA = valueA === false || valueA === true || valueA === null ? '' : valueA
14
23
  valueB = valueB === false || valueB === true || valueB === null ? '' : valueB
@@ -1,5 +1,6 @@
1
1
  import { parseDate, formatDate } from '@cdc/core/helpers/cove/date'
2
2
  import { formatNumber } from '@cdc/core/helpers/cove/number'
3
+ import { TableConfig } from '../types/TableConfig'
3
4
 
4
5
  // if its additional column, return formatting params
5
6
  const isAdditionalColumn = (column, config) => {
@@ -23,14 +24,15 @@ const isAdditionalColumn = (column, config) => {
23
24
  return formattingParams
24
25
  }
25
26
 
26
- export const getChartCellValue = (row, column, config, runtimeData) => {
27
- if (config.table.customTableConfig) return runtimeData[row][column]
27
+ export const getChartCellValue = (row: string, column: string, config: TableConfig, runtimeData: Object[]) => {
28
+ if (config.table.customTableConfig || config.visualizationType === 'Sankey' || runtimeData?.[0]?.tableData) return runtimeData[row][column]
29
+
28
30
  const rowObj = runtimeData[row]
29
31
  let cellValue // placeholder for formatting below
30
32
  let labelValue = rowObj[column] // just raw X axis string
31
33
  if (column === config.xAxis?.dataKey) {
32
34
  // not the prettiest, but helper functions work nicely here.
33
- cellValue = config.xAxis?.type === 'date' ? formatDate(config.xAxis?.dateDisplayFormat, parseDate(config.xAxis?.dateParseFormat, labelValue)) : labelValue
35
+ cellValue = config.xAxis?.type === 'date' ? formatDate(config.table?.dateDisplayFormat || config.xAxis?.dateDisplayFormat, parseDate(config.xAxis?.dateParseFormat, labelValue)) : labelValue
34
36
  } else {
35
37
  let resolvedAxis = 'left'
36
38
  let leftAxisItems = config.series ? config.series.filter(item => item?.axis === 'Left') : []
@@ -1,5 +1,13 @@
1
- export const getDataSeriesColumns = (config, isVertical, runtimeData): string[] => {
1
+ import { TableConfig } from '../types/TableConfig'
2
+
3
+ export const getDataSeriesColumns = (config: TableConfig, isVertical: boolean, runtimeData: Object[]): string[] => {
2
4
  if (config.table.customTableConfig) return runtimeData[0] ? Object.keys(runtimeData[0]) : []
5
+ if (config.type === 'table') {
6
+ const excludeColumns = Object.values(config.columns)
7
+ .filter(column => column.dataTable === false)
8
+ .map(column => column.name)
9
+ return Object.keys(runtimeData[0]).filter(columnName => !excludeColumns.includes(columnName))
10
+ }
3
11
  let tmpSeriesColumns
4
12
  if (config.visualizationType !== 'Pie') {
5
13
  tmpSeriesColumns = isVertical ? [config.xAxis?.dataKey] : [] //, ...config.runtime.seriesLabelsAll
@@ -11,7 +19,7 @@ export const getDataSeriesColumns = (config, isVertical, runtimeData): string[]
11
19
  tmpSeriesColumns = Object.keys(runtimeData[0])
12
20
  }
13
21
  } else {
14
- tmpSeriesColumns = [config.xAxis?.dataKey, config.yAxis?.dataKey] //Object.keys(runtimeData[0])
22
+ tmpSeriesColumns = isVertical ? [config.xAxis?.dataKey, config.yAxis?.dataKey] : [config.yAxis?.dataKey]
15
23
  }
16
24
 
17
25
  // then add the additional Columns
@@ -1,26 +1,21 @@
1
- const getLabel = (name, config) => {
2
- let custLabel = ''
3
- if (config.columns && Object.keys(config.columns).length > 0) {
4
- Object.keys(config.columns).forEach(function (key) {
5
- var tmpColumn = config.columns[key]
6
- // add if not the index AND it is enabled to be added to data table
7
- if (tmpColumn.name === name) {
8
- custLabel = tmpColumn.label
9
- }
10
- })
11
- return custLabel
1
+ import { TableConfig } from '../types/TableConfig'
2
+
3
+ const getLabel = (name: string, config: TableConfig) => {
4
+ const columns = Object.values(config.columns || {})
5
+ const matchingConfiguredColumn = columns.find(col => col.name === name)
6
+ if (matchingConfiguredColumn?.label) {
7
+ return matchingConfiguredColumn.label
12
8
  }
9
+ return name
13
10
  }
14
11
 
15
- export const getSeriesName = (column, config) => {
12
+ export const getSeriesName = (column: string, config: TableConfig) => {
16
13
  // If a user sets the name on a series use that.
17
- let userUpdatedSeriesName = config.series ? config.series.filter(series => series.dataKey === column)?.[0]?.name : ''
18
- if (userUpdatedSeriesName) return userUpdatedSeriesName
19
-
14
+ const userDefinedSeries = config.series?.find(series => series.dataKey === column)
15
+ if (userDefinedSeries?.name) {
16
+ return userDefinedSeries.name
17
+ }
20
18
  if (config.runtimeSeriesLabels && config.runtimeSeriesLabels[column]) return config.runtimeSeriesLabels[column]
21
-
22
- let custLabel = getLabel(column, config) ? getLabel(column, config) : column
23
- let text = column === config.xAxis?.dataKey ? config.table.indexLabel : custLabel
24
-
25
- return text
19
+ const columnIsDataKey = column === config.xAxis?.dataKey
20
+ return columnIsDataKey ? config.table.indexLabel : getLabel(column, config)
26
21
  }
@@ -21,7 +21,11 @@ const mapCellArray = ({ rows, columns, runtimeData, config, applyLegendToRow, di
21
21
  let labelValue
22
22
  const mapZoomHandler = config.general.type === 'bubble' && config.general.allowMapZoom && config.general.geoType === 'world' ? () => setFilteredCountryCode(row) : undefined
23
23
  if ((config.general.geoType !== 'single-state' && config.general.geoType !== 'us-county') || config.general.type === 'us-geocode') {
24
+ const capitalize = str => {
25
+ return str.charAt(0).toUpperCase() + str.slice(1)
26
+ }
24
27
  labelValue = displayGeoName(row)
28
+ labelValue = String(labelValue).startsWith('region') ? capitalize(labelValue) : labelValue
25
29
  } else {
26
30
  labelValue = formatLegendLocation(row)
27
31
  }
@@ -2,51 +2,26 @@ import { Axis } from '@cdc/core/types/Axis'
2
2
  import { Series } from '@cdc/core/types/Series'
3
3
  import { Runtime } from '@cdc/core/types/Runtime'
4
4
  import { Table } from '@cdc/core/types/Table'
5
+ import { Region } from '@cdc/core/types/Region'
6
+ import { BoxPlot } from '../../../types/BoxPlot'
7
+ import { General } from '../../../types/General'
8
+ import { Column } from '../../../types/Column'
9
+ import { Legend } from '@cdc/core/types/Legend'
5
10
 
6
11
  export type TableConfig = {
7
12
  type?: string
8
13
  table: Table
9
14
  xAxis?: Axis
10
15
  yAxis?: Axis
11
- boxplot?: {
12
- tableData: Object[]
13
- labels: {
14
- mean: string
15
- maximum: string
16
- minimum: string
17
- iqr: string
18
- median: string
19
- q1: string
20
- q3: string
21
- outliers: string
22
- values: string
23
- total: string
24
- lowerBounds: string
25
- upperBounds: string
26
- }
27
- plots: []
28
- categories: string[]
29
- }
30
- visualizationType?: string
31
- general?: {
32
- geoType: string
33
- type: string
34
- showDownloadButton: boolean
35
- allowMapZoom?: boolean
36
- }
37
- columns?: {
38
- geo: {
39
- name: string
40
- }
41
- }
42
- legend?: {
43
- specialClasses?: { key: string; label: string; value: string }[]
44
- hide?: boolean
45
- }
16
+ boxplot?: BoxPlot
17
+ visualizationType: string
18
+ general?: General
19
+ columns?: Record<string, Column>
20
+ legend?: Legend
46
21
  series?: Series
47
- regions?: { label: string; from: string; to: string; fromType: 'Fixed' | 'Previous Days'; toType: 'Fixed' | 'Last Date' }[]
22
+ regions?: Region[]
48
23
  runtimeSeriesLabels?: Object
49
24
  dataFormat?: Object
50
- runtime: Runtime
25
+ runtime?: Runtime
51
26
  data: Object[]
52
27
  }