@cdc/core 4.23.11 → 4.24.1
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.
- package/components/DataTable/DataTable.tsx +27 -9
- package/components/DataTable/components/ChartHeader.tsx +17 -5
- package/components/DataTable/components/ExpandCollapse.tsx +1 -1
- package/components/DataTable/helpers/chartCellMatrix.tsx +16 -2
- package/components/DataTable/helpers/customColumns.ts +25 -0
- package/components/DataTable/helpers/getChartCellValue.ts +1 -0
- package/components/DataTable/helpers/getDataSeriesColumns.ts +1 -0
- package/components/DataTable/types/TableConfig.ts +5 -10
- package/components/EditorPanel/DataTableEditor.tsx +133 -0
- package/components/EditorPanel/Inputs.tsx +150 -0
- package/components/Filters.jsx +3 -3
- package/components/MediaControls.jsx +1 -1
- package/components/MultiSelect/MultiSelect.tsx +95 -0
- package/components/MultiSelect/index.ts +1 -0
- package/components/MultiSelect/multiselect.styles.css +50 -0
- package/components/Table/Table.tsx +23 -3
- package/components/Table/components/Cell.tsx +3 -3
- package/components/Table/components/GroupRow.tsx +6 -2
- package/components/Table/components/Row.tsx +9 -2
- package/components/Table/types/RowType.ts +5 -0
- package/components/_stories/DataTable.stories.tsx +41 -0
- package/components/_stories/EditorPanel.stories.tsx +53 -0
- package/components/_stories/Inputs.stories.tsx +37 -0
- package/components/_stories/MultiSelect.stories.tsx +24 -0
- package/components/_stories/_mocks/row_type.json +42 -0
- package/components/inputs/{InputSelect.jsx → InputSelect.tsx} +15 -5
- package/components/managers/DataDesigner.tsx +8 -8
- package/components/ui/{Icon.jsx → Icon.tsx} +3 -3
- package/helpers/DataTransform.ts +30 -2
- package/helpers/getFileExtension.ts +28 -5
- package/package.json +2 -2
- package/styles/_data-table.scss +2 -0
- package/types/Axis.ts +37 -2
- package/types/Column.ts +15 -0
- package/types/FilterBehavior.ts +1 -0
- package/types/Runtime.ts +21 -1
- package/types/Series.ts +1 -1
- package/types/Table.ts +18 -0
- package/types/UpdateFieldFunc.ts +1 -0
- package/types/Visualization.ts +9 -9
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import Icon from '../ui/Icon'
|
|
3
|
+
|
|
4
|
+
import './multiselect.styles.css'
|
|
5
|
+
import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
|
|
6
|
+
|
|
7
|
+
interface Option {
|
|
8
|
+
value: string
|
|
9
|
+
label: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface MultiSelectProps {
|
|
13
|
+
section?: string
|
|
14
|
+
subsection?: string
|
|
15
|
+
fieldName: string
|
|
16
|
+
options: Option[]
|
|
17
|
+
updateField: UpdateFieldFunc<string[]>
|
|
18
|
+
label?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField }) => {
|
|
22
|
+
const [selectedItems, setSelectedItems] = useState<Option[]>([])
|
|
23
|
+
const [expanded, setExpanded] = useState(false)
|
|
24
|
+
const multiSelectRef = useRef(null)
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const handleClickOutside = event => {
|
|
28
|
+
if (multiSelectRef.current && !multiSelectRef.current.contains(event.target)) {
|
|
29
|
+
setExpanded(false)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
37
|
+
}
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
const update = newItems =>
|
|
41
|
+
updateField(
|
|
42
|
+
section,
|
|
43
|
+
subsection,
|
|
44
|
+
fieldName,
|
|
45
|
+
newItems.map(item => item.value)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const handleItemSelect = (option: Option) => {
|
|
49
|
+
const newItems = [...selectedItems, option]
|
|
50
|
+
setSelectedItems(newItems)
|
|
51
|
+
update(newItems)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const handleItemRemove = (option: Option, caller: string) => {
|
|
55
|
+
const newItems = selectedItems.filter(item => item.value !== option.value)
|
|
56
|
+
setSelectedItems(newItems)
|
|
57
|
+
update(newItems)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const multiID = 'multiSelect_' + label
|
|
61
|
+
return (
|
|
62
|
+
<div ref={multiSelectRef} className='cove-multiselect'>
|
|
63
|
+
{label && (
|
|
64
|
+
<span id={multiID} className='edit-label cove-input__label'>
|
|
65
|
+
{label}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
<div aria-labelledby={label ? multiID : undefined} className='selected'>
|
|
70
|
+
{selectedItems.map(item => (
|
|
71
|
+
<div key={item.value} role='button' tabIndex={0} onClick={() => handleItemRemove(item, 'button click')} onKeyUp={() => handleItemRemove(item, 'button key up')}>
|
|
72
|
+
{item.label}
|
|
73
|
+
<button aria-label='Remove' onClick={() => handleItemRemove(item, 'X')}>
|
|
74
|
+
x
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
<button aria-label={expanded ? 'Collapse' : 'Expand'} className='expand' onClick={() => setExpanded(!expanded)}>
|
|
79
|
+
<Icon display={expanded ? 'caretDown' : 'caretUp'} style={{ cursor: 'pointer' }} />
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
<ul className={'dropdown' + (expanded ? '' : ' hide')}>
|
|
83
|
+
{options
|
|
84
|
+
.filter(option => !selectedItems.find(item => item.value === option.value))
|
|
85
|
+
.map(option => (
|
|
86
|
+
<li className='cove-multiselect-li' key={option.value} role='option' tabIndex={0} onClick={() => handleItemSelect(option)} onKeyUp={() => handleItemSelect(option)}>
|
|
87
|
+
{option.label}
|
|
88
|
+
</li>
|
|
89
|
+
))}
|
|
90
|
+
</ul>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default MultiSelect
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './MultiSelect'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
.cove-multiselect {
|
|
2
|
+
position: relative;
|
|
3
|
+
.selected {
|
|
4
|
+
border: 1px solid #ccc;
|
|
5
|
+
padding: 5px;
|
|
6
|
+
min-height: 40px;
|
|
7
|
+
:is(button) {
|
|
8
|
+
border: none;
|
|
9
|
+
background: none;
|
|
10
|
+
}
|
|
11
|
+
:is(div) {
|
|
12
|
+
display: inline-block;
|
|
13
|
+
padding: 0 0 0 5px;
|
|
14
|
+
margin-right: 5px;
|
|
15
|
+
margin-bottom: 2px;
|
|
16
|
+
background: #ccc;
|
|
17
|
+
border-radius: 5px;
|
|
18
|
+
}
|
|
19
|
+
.expand {
|
|
20
|
+
padding: 0 5px;
|
|
21
|
+
border-radius: 5px;
|
|
22
|
+
background: #ccc;
|
|
23
|
+
float: right;
|
|
24
|
+
}
|
|
25
|
+
border-radius: 5px;
|
|
26
|
+
}
|
|
27
|
+
.dropdown {
|
|
28
|
+
background: white;
|
|
29
|
+
position: absolute;
|
|
30
|
+
margin-top: 5px;
|
|
31
|
+
border: 1px solid #ccc;
|
|
32
|
+
padding: 0;
|
|
33
|
+
min-height: 40px;
|
|
34
|
+
overflow: scroll;
|
|
35
|
+
max-height: 200px;
|
|
36
|
+
z-index: 1;
|
|
37
|
+
&.hide {
|
|
38
|
+
display: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
:is(li) {
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
list-style: none;
|
|
44
|
+
padding-left: 10px;
|
|
45
|
+
&:hover {
|
|
46
|
+
background: #ccc;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -2,6 +2,7 @@ import { ReactNode } from 'react'
|
|
|
2
2
|
import Row from './components/Row'
|
|
3
3
|
import GroupRow from './components/GroupRow'
|
|
4
4
|
import { CellMatrix, GroupCellMatrix } from './types/CellMatrix'
|
|
5
|
+
import { RowType } from './types/RowType'
|
|
5
6
|
|
|
6
7
|
type TableProps = {
|
|
7
8
|
childrenMatrix: CellMatrix | GroupCellMatrix
|
|
@@ -14,12 +15,15 @@ type TableProps = {
|
|
|
14
15
|
'aria-live'?: 'off' | 'assertive' | 'polite'
|
|
15
16
|
hidden?: boolean
|
|
16
17
|
'aria-rowcount'?: number
|
|
18
|
+
cellMinWidth?: number
|
|
17
19
|
}
|
|
20
|
+
wrapColumns?: boolean
|
|
21
|
+
hasRowType?: boolean // if it has row type then the first column is the row type which will explain how to render the row
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
type Position = 'sticky'
|
|
21
25
|
|
|
22
|
-
const Table = ({ childrenMatrix, tableName, caption, stickyHeader, headContent, tableOptions }: TableProps) => {
|
|
26
|
+
const Table = ({ childrenMatrix, tableName, caption, stickyHeader, headContent, tableOptions, wrapColumns, hasRowType }: TableProps) => {
|
|
23
27
|
const headStyle = stickyHeader ? { position: 'sticky' as Position, top: 0, zIndex: 999 } : {}
|
|
24
28
|
const isGroupedMatrix = !Array.isArray(childrenMatrix)
|
|
25
29
|
return (
|
|
@@ -33,13 +37,29 @@ const Table = ({ childrenMatrix, tableName, caption, stickyHeader, headContent,
|
|
|
33
37
|
const rows = childrenMatrix[groupName].map((row, i) => {
|
|
34
38
|
colSpan = row.length
|
|
35
39
|
const key = `${tableName}-${groupName}-row-${i}`
|
|
36
|
-
return <Row key={key} rowKey={key} childRow={row} />
|
|
40
|
+
return <Row key={key} rowKey={key} childRow={row} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
|
|
37
41
|
})
|
|
38
42
|
return [<GroupRow label={groupName} colSpan={colSpan} key={`${tableName}-${groupName}`} />, ...rows]
|
|
39
43
|
})
|
|
40
44
|
: childrenMatrix.map((childRow, i) => {
|
|
45
|
+
let childRowCopy = [...childRow]
|
|
46
|
+
let rowType = undefined
|
|
47
|
+
if (hasRowType) rowType = childRowCopy.shift()
|
|
41
48
|
const key = `${tableName}-row-${i}`
|
|
42
|
-
|
|
49
|
+
if (rowType === undefined) {
|
|
50
|
+
return <Row key={key} rowKey={key} childRow={childRow} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
|
|
51
|
+
} else {
|
|
52
|
+
switch (rowType) {
|
|
53
|
+
case RowType.row_group:
|
|
54
|
+
return <GroupRow label={childRowCopy[0]} colSpan={childRowCopy.length} key={key} />
|
|
55
|
+
case RowType.total:
|
|
56
|
+
return <Row key={key} rowKey={key} childRow={childRowCopy} isTotal={true} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
|
|
57
|
+
case RowType.row_group_total:
|
|
58
|
+
return <GroupRow label={childRowCopy[0]} colSpan={1} key={key} data={childRowCopy.slice(1)} />
|
|
59
|
+
default:
|
|
60
|
+
return <Row key={key} rowKey={key} childRow={childRowCopy} wrapColumns={wrapColumns} cellMinWidth={tableOptions.cellMinWidth} />
|
|
61
|
+
}
|
|
62
|
+
}
|
|
43
63
|
})}
|
|
44
64
|
</tbody>
|
|
45
65
|
</table>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const Cell = ({ children }) => {
|
|
1
|
+
const Cell = ({ children, style, isBold = false }) => {
|
|
2
2
|
return (
|
|
3
|
-
<td tabIndex={0} role='gridcell'>
|
|
4
|
-
{children}
|
|
3
|
+
<td tabIndex={0} role='gridcell' style={style}>
|
|
4
|
+
{isBold ? <strong>{children}</strong> : children}
|
|
5
5
|
</td>
|
|
6
6
|
)
|
|
7
7
|
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { ReactNode } from 'react'
|
|
2
|
+
|
|
1
3
|
type GroupRowProps = {
|
|
2
|
-
label:
|
|
4
|
+
label: ReactNode
|
|
3
5
|
colSpan: number
|
|
6
|
+
data?: ReactNode[]
|
|
4
7
|
}
|
|
5
8
|
|
|
6
|
-
const GroupRow = ({ label, colSpan }: GroupRowProps) => {
|
|
9
|
+
const GroupRow = ({ label, colSpan, data }: GroupRowProps) => {
|
|
7
10
|
return (
|
|
8
11
|
<tr>
|
|
9
12
|
<th scope='colgroup' colSpan={colSpan}>
|
|
10
13
|
{label}
|
|
11
14
|
</th>
|
|
15
|
+
{data && data.map((item, i) => <th key={`${label}-${i}`}>{item}</th>)}
|
|
12
16
|
</tr>
|
|
13
17
|
)
|
|
14
18
|
}
|
|
@@ -4,13 +4,20 @@ import Cell from './Cell'
|
|
|
4
4
|
type RowProps = {
|
|
5
5
|
childRow: ReactNode[]
|
|
6
6
|
rowKey: string
|
|
7
|
+
wrapColumns: boolean
|
|
8
|
+
isTotal?: boolean
|
|
9
|
+
cellMinWidth?: number
|
|
7
10
|
}
|
|
8
11
|
|
|
9
|
-
const Row = ({ childRow, rowKey }: RowProps) => {
|
|
12
|
+
const Row = ({ childRow, rowKey, wrapColumns, cellMinWidth = 0, isTotal }: RowProps) => {
|
|
13
|
+
const whiteSpace = wrapColumns ? 'unset' : 'nowrap'
|
|
14
|
+
const minWidth = cellMinWidth + 'px'
|
|
10
15
|
return (
|
|
11
16
|
<tr>
|
|
12
17
|
{childRow.map((child, i) => (
|
|
13
|
-
<Cell key={rowKey + '__' + i}
|
|
18
|
+
<Cell key={rowKey + '__' + i} style={{ whiteSpace, minWidth }} isBold={isTotal}>
|
|
19
|
+
{child}
|
|
20
|
+
</Cell>
|
|
14
21
|
))}
|
|
15
22
|
</tr>
|
|
16
23
|
)
|
|
@@ -5,6 +5,8 @@ import './styles.scss'
|
|
|
5
5
|
import Example_1 from './_mocks/dashboard_no_filter.json'
|
|
6
6
|
import CityStateExample from './_mocks/example-city-state.json'
|
|
7
7
|
import { displayGeoName } from '@cdc/map/src/helpers/displayGeoName'
|
|
8
|
+
import rowTypeData from './_mocks/row_type.json'
|
|
9
|
+
import { TableConfig } from '../DataTable/types/TableConfig'
|
|
8
10
|
|
|
9
11
|
const meta: Meta<typeof DataTable> = {
|
|
10
12
|
title: 'Components/Organisms/DataTable',
|
|
@@ -60,3 +62,42 @@ export const Grouped: Story = {
|
|
|
60
62
|
tabbingId: datasetKey
|
|
61
63
|
}
|
|
62
64
|
}
|
|
65
|
+
|
|
66
|
+
export const RowType: Story = {
|
|
67
|
+
args: {
|
|
68
|
+
config: {
|
|
69
|
+
dashboard: {
|
|
70
|
+
theme: 'theme-blue',
|
|
71
|
+
title: 'RowType'
|
|
72
|
+
},
|
|
73
|
+
title: 'RowType',
|
|
74
|
+
dataUrl: '/examples/feature/__data__/ardi.json',
|
|
75
|
+
animate: false,
|
|
76
|
+
animateReplay: true,
|
|
77
|
+
palette: 'qualitative-soft',
|
|
78
|
+
aspectRatio: 1,
|
|
79
|
+
dataFormat: {
|
|
80
|
+
roundTo: 1,
|
|
81
|
+
commas: false,
|
|
82
|
+
prefix: '',
|
|
83
|
+
suffix: ''
|
|
84
|
+
},
|
|
85
|
+
legend: {
|
|
86
|
+
hide: false
|
|
87
|
+
},
|
|
88
|
+
table: {
|
|
89
|
+
label: 'Data Table',
|
|
90
|
+
expanded: true,
|
|
91
|
+
show: true,
|
|
92
|
+
customTableConfig: true
|
|
93
|
+
}
|
|
94
|
+
} as unknown as TableConfig,
|
|
95
|
+
dataConfig: { data: rowTypeData },
|
|
96
|
+
rawData: rowTypeData,
|
|
97
|
+
runtimeData: rowTypeData,
|
|
98
|
+
expandDataTable: true,
|
|
99
|
+
tableTitle: 'DataTable',
|
|
100
|
+
viewport: 'lg',
|
|
101
|
+
tabbingId: '#asdf'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
|
|
3
|
+
import DataTableEditor from '../EditorPanel/DataTableEditor'
|
|
4
|
+
import { Accordion, AccordionItem, AccordionItemButton, AccordionItemHeading, AccordionItemPanel } from 'react-accessible-accordion'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
|
|
7
|
+
const EditorPanel = () => {
|
|
8
|
+
const { config, isDashboard } = Primary.args
|
|
9
|
+
const [_config, setConfig] = useState(config)
|
|
10
|
+
const updateField = (section, subsection, fieldName, value) => {
|
|
11
|
+
setConfig({
|
|
12
|
+
..._config,
|
|
13
|
+
[section]: {
|
|
14
|
+
..._config[section],
|
|
15
|
+
[fieldName]: value
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
return (
|
|
20
|
+
<Accordion>
|
|
21
|
+
<AccordionItem>
|
|
22
|
+
<AccordionItemHeading>
|
|
23
|
+
<AccordionItemButton>Data Table</AccordionItemButton>
|
|
24
|
+
</AccordionItemHeading>
|
|
25
|
+
<AccordionItemPanel>
|
|
26
|
+
<DataTableEditor config={_config} isDashboard={isDashboard} updateField={updateField} isLoadedFromUrl={false} />
|
|
27
|
+
</AccordionItemPanel>
|
|
28
|
+
</AccordionItem>
|
|
29
|
+
</Accordion>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const meta: Meta<typeof DataTableEditor> = {
|
|
34
|
+
title: 'Components/Organisms/EditorPanel',
|
|
35
|
+
component: EditorPanel
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default meta
|
|
39
|
+
|
|
40
|
+
type Story = StoryObj<typeof DataTableEditor>
|
|
41
|
+
|
|
42
|
+
export const Primary: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
config: {
|
|
45
|
+
table: {
|
|
46
|
+
label: 'Data Table',
|
|
47
|
+
show: true
|
|
48
|
+
},
|
|
49
|
+
visualizationType: 'Pie'
|
|
50
|
+
},
|
|
51
|
+
isDashboard: true
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
|
|
3
|
+
import InputSelect from '../inputs/InputSelect'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
|
|
6
|
+
const Inputs: React.FC = ({ config }: any) => {
|
|
7
|
+
const [_config, setConfig] = useState(config)
|
|
8
|
+
const updateField = (section, subsection, fieldName, value) => {
|
|
9
|
+
setConfig({
|
|
10
|
+
..._config,
|
|
11
|
+
[section]: {
|
|
12
|
+
..._config[section],
|
|
13
|
+
[fieldName]: value
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
return (
|
|
18
|
+
<div>
|
|
19
|
+
<InputSelect label='Select' options={['apple', 'banana', 'orange']} fieldName='inputselect' updateField={updateField} />
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const meta: Meta<typeof Inputs> = {
|
|
25
|
+
title: 'Components/Atoms/Inputs',
|
|
26
|
+
component: Inputs
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default meta
|
|
30
|
+
|
|
31
|
+
type Story = StoryObj<typeof Inputs>
|
|
32
|
+
|
|
33
|
+
export const Select: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
config: {}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
|
|
3
|
+
import MultiSelect from '../MultiSelect'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof MultiSelect> = {
|
|
6
|
+
title: 'Components/Molecules/MultiSelect',
|
|
7
|
+
component: MultiSelect
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Story = StoryObj<typeof MultiSelect>
|
|
11
|
+
|
|
12
|
+
export const Primary: Story = {
|
|
13
|
+
args: {
|
|
14
|
+
options: [
|
|
15
|
+
{ value: '1', label: 'One' },
|
|
16
|
+
{ value: '2', label: 'Two' },
|
|
17
|
+
{ value: '3', label: 'Three' }
|
|
18
|
+
],
|
|
19
|
+
label: 'MultiSelect',
|
|
20
|
+
updateField: (section, subsection, fieldName, value) => {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default meta
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"type": "row_group",
|
|
4
|
+
"overall": "",
|
|
5
|
+
"male": "",
|
|
6
|
+
"female": "",
|
|
7
|
+
"row_type": "row_group",
|
|
8
|
+
"nullColumn": null
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"type": "row_group_total",
|
|
12
|
+
"overall": "100",
|
|
13
|
+
"male": "50",
|
|
14
|
+
"female": "50",
|
|
15
|
+
"row_type": "row_group_total",
|
|
16
|
+
"nullColumn": null
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "regular",
|
|
20
|
+
"overall": "50",
|
|
21
|
+
"male": "25",
|
|
22
|
+
"female": "25",
|
|
23
|
+
"row_type": null,
|
|
24
|
+
"nullColumn": null
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "regular",
|
|
28
|
+
"overall": "50",
|
|
29
|
+
"male": "25",
|
|
30
|
+
"female": "25",
|
|
31
|
+
"row_type": null,
|
|
32
|
+
"nullColumn": null
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "total",
|
|
36
|
+
"overall": "100",
|
|
37
|
+
"male": "50",
|
|
38
|
+
"female": "50",
|
|
39
|
+
"row_type": "total",
|
|
40
|
+
"nullColumn": null
|
|
41
|
+
}
|
|
42
|
+
]
|
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import React, { memo } from 'react'
|
|
2
|
-
|
|
3
1
|
import '../../styles/v2/components/input/index.scss'
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
interface InputProps {
|
|
4
|
+
label?
|
|
5
|
+
value?
|
|
6
|
+
options: string[] | { [key: string]: string }
|
|
7
|
+
fieldName
|
|
8
|
+
section?
|
|
9
|
+
subsection?
|
|
10
|
+
required?
|
|
11
|
+
updateField
|
|
12
|
+
initial?
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const InputSelect = ({ label, value, options, fieldName, section = null, subsection = null, required = false, updateField, initial: initialValue, ...attributes }: InputProps) => {
|
|
16
|
+
let optionsJsx = []
|
|
7
17
|
|
|
8
18
|
if (Array.isArray(options)) {
|
|
9
19
|
//Handle basic array
|
|
@@ -48,6 +58,6 @@ const InputSelect = memo(({ label, value, options, fieldName, section = null, su
|
|
|
48
58
|
</select>
|
|
49
59
|
</label>
|
|
50
60
|
)
|
|
51
|
-
}
|
|
61
|
+
}
|
|
52
62
|
|
|
53
63
|
export default InputSelect
|
|
@@ -8,7 +8,7 @@ import '../../styles/v2/components/data-designer.scss'
|
|
|
8
8
|
|
|
9
9
|
type DataDesignerProps = {
|
|
10
10
|
configureData?: any // todo: add description here when understood better
|
|
11
|
-
updateDescriptionProp?: () => void // used to update data description fields
|
|
11
|
+
updateDescriptionProp?: (vizKey: string, dataKey: string, propName: string, value: any) => void // used to update data description fields
|
|
12
12
|
visualizationKey?: any // todo: add description here when understood better
|
|
13
13
|
dataKey?: string // appears to be the data file name, ie valid-data.csv
|
|
14
14
|
config?: any // can be one of many different types of chart configs that aren't fully established yet
|
|
@@ -198,16 +198,16 @@ const DataDesigner = (props: DataDesignerProps) => {
|
|
|
198
198
|
</div>
|
|
199
199
|
<div className='mb-2'>
|
|
200
200
|
<div className='mb-1'>Which properties in the dataset represent the numeric value? (all remaining properties will be treated as filters)</div>
|
|
201
|
-
{configureData.dataDescription.
|
|
201
|
+
{configureData.dataDescription.valueKeysTallSupport && configureData.dataDescription.valueKeysTallSupport.length > 0 && (
|
|
202
202
|
<ul className='value-list'>
|
|
203
|
-
{configureData.dataDescription.
|
|
203
|
+
{configureData.dataDescription.valueKeysTallSupport.map((valueKey, index) => (
|
|
204
204
|
<li key={`value-keys-list-${index}`}>
|
|
205
205
|
{valueKey}
|
|
206
206
|
<button
|
|
207
207
|
onClick={() => {
|
|
208
|
-
let newValueKeys = configureData.dataDescription.
|
|
208
|
+
let newValueKeys = configureData.dataDescription.valueKeysTallSupport
|
|
209
209
|
newValueKeys.splice(index, 1)
|
|
210
|
-
updateDescriptionProp(visualizationKey, dataKey, '
|
|
210
|
+
updateDescriptionProp(visualizationKey, dataKey, 'valueKeysTallSupport', newValueKeys)
|
|
211
211
|
}}
|
|
212
212
|
>
|
|
213
213
|
X
|
|
@@ -218,14 +218,14 @@ const DataDesigner = (props: DataDesignerProps) => {
|
|
|
218
218
|
)}
|
|
219
219
|
<select
|
|
220
220
|
onChange={e => {
|
|
221
|
-
if (e.target.value && (!configureData.dataDescription.
|
|
222
|
-
updateDescriptionProp(visualizationKey, dataKey, '
|
|
221
|
+
if (e.target.value && (!configureData.dataDescription.valueKeysTallSupport || configureData.dataDescription.valueKeysTallSupport.indexOf(e.target.value) === -1)) {
|
|
222
|
+
updateDescriptionProp(visualizationKey, dataKey, 'valueKeysTallSupport', [...(configureData.dataDescription.valueKeysTallSupport || []), e.target.value])
|
|
223
223
|
}
|
|
224
224
|
}}
|
|
225
225
|
>
|
|
226
226
|
<option value=''>Choose an option</option>
|
|
227
227
|
{Object.keys(configureData.data[0])
|
|
228
|
-
.filter(value => !configureData.dataDescription.
|
|
228
|
+
.filter(value => !configureData.dataDescription.valueKeysTallSupport || configureData.dataDescription.valueKeysTallSupport.indexOf(value) === -1)
|
|
229
229
|
.map((value, index) => (
|
|
230
230
|
<option value={value} key={`value-keys-option-${index}`}>
|
|
231
231
|
{value}
|
|
@@ -69,17 +69,17 @@ const iconHash = {
|
|
|
69
69
|
|
|
70
70
|
export const ICON_TYPES = Object.keys(iconHash)
|
|
71
71
|
|
|
72
|
-
const Icon = ({ display = '', base, alt = '', size, color, style, ...attributes }) => {
|
|
72
|
+
const Icon = ({ display = '', base = undefined, alt = '', size = undefined, color = undefined, style = undefined, ...attributes }) => {
|
|
73
73
|
const IconObj = iconHash[display] || null
|
|
74
74
|
|
|
75
75
|
const filteredAttrs = { ...attributes }
|
|
76
76
|
delete filteredAttrs.className
|
|
77
77
|
|
|
78
78
|
const styles = {
|
|
79
|
-
...style,
|
|
80
79
|
color: color ? color : null,
|
|
81
80
|
width: size ? size + 'px' : null,
|
|
82
|
-
cursor: display === 'move' ? 'move' : 'default'
|
|
81
|
+
cursor: display === 'move' ? 'move' : 'default',
|
|
82
|
+
...style
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
return (
|
package/helpers/DataTransform.ts
CHANGED
|
@@ -128,8 +128,36 @@ export class DataTransform {
|
|
|
128
128
|
return standardized
|
|
129
129
|
}
|
|
130
130
|
} else if (description.series === true && description.singleRow === false) {
|
|
131
|
-
if (description.seriesKey !== undefined && description.xKey !== undefined && (description.valueKey !== undefined || (description.valueKeys !== undefined && description.valueKeys.length > 0))) {
|
|
132
|
-
if (description.
|
|
131
|
+
if (description.seriesKey !== undefined && description.xKey !== undefined && (description.valueKey !== undefined || (description.valueKeys !== undefined && description.valueKeys.length > 0) || (description.valueKeysTallSupport !== undefined && description.valueKeysTallSupport.length > 0))) {
|
|
132
|
+
if (description.valueKeysTallSupport !== undefined) {
|
|
133
|
+
let standardizedMapped = {}
|
|
134
|
+
let standardized: string[] = []
|
|
135
|
+
|
|
136
|
+
data.forEach(row => {
|
|
137
|
+
let uniqueKey = row[description.xKey];
|
|
138
|
+
Object.keys(row).forEach(key => {
|
|
139
|
+
if(key !== description.xKey && key !== description.seriesKey && description.valueKeysTallSupport.indexOf(key) === -1 && (!description.ignoredKeys || description.ignoredKeys.indexOf(key) === -1)){
|
|
140
|
+
uniqueKey += "|" + row[key];
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
if(!standardizedMapped[uniqueKey]){
|
|
145
|
+
standardizedMapped[uniqueKey] = {[description.xKey]: row[description.xKey]}
|
|
146
|
+
}
|
|
147
|
+
Object.keys(row).forEach(key => {
|
|
148
|
+
if(key !== description.xKey && key !== description.seriesKey && description.valueKeysTallSupport.indexOf(key) === -1 && (!description.ignoredKeys || description.ignoredKeys.indexOf(key) === -1)){
|
|
149
|
+
standardizedMapped[uniqueKey][key] = row[key];
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
description.valueKeysTallSupport.forEach(valueKey => {
|
|
153
|
+
standardizedMapped[uniqueKey][row[description.seriesKey] + '-' + valueKey] = row[valueKey];
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
standardized = Object.keys(standardizedMapped).map(key => standardizedMapped[key]);
|
|
158
|
+
|
|
159
|
+
return standardized
|
|
160
|
+
} else if (description.valueKeys !== undefined) {
|
|
133
161
|
let standardizedMapped = {}
|
|
134
162
|
let standardized: string[] = []
|
|
135
163
|
let valueKeys = description.valueKeys
|
|
@@ -1,5 +1,28 @@
|
|
|
1
|
-
export const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
1
|
+
// export const getFileExtensionx = (path: string): string => {
|
|
2
|
+
// const regex = /(?:\.([^.]+))?$/
|
|
3
|
+
// const outCome: RegExpExecArray | null = regex.exec(path)
|
|
4
|
+
// return outCome ? outCome[1] : ''
|
|
5
|
+
// }
|
|
6
|
+
|
|
7
|
+
export const getFileExtension = (url: string): string => {
|
|
8
|
+
const regexForExtension = /(?:\.([^.]+))$/
|
|
9
|
+
const regexForQueryParam = /[?&]wt=(csv|json)(?:&|$)/ // Regular expression for 'wt' query parameter
|
|
10
|
+
|
|
11
|
+
const urlObject = new URL(url, window.location.origin)
|
|
12
|
+
const pathname = urlObject.pathname
|
|
13
|
+
const searchParams = urlObject.search
|
|
14
|
+
|
|
15
|
+
// First, try to get the extension from the pathname
|
|
16
|
+
const pathnameMatch = regexForExtension.exec(pathname)
|
|
17
|
+
if (pathnameMatch && pathnameMatch[1]) {
|
|
18
|
+
return pathnameMatch[1]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Then, try to get it from the query parameter 'wt'
|
|
22
|
+
const queryParamsMatch = regexForQueryParam.exec(searchParams)
|
|
23
|
+
if (queryParamsMatch && queryParamsMatch[1]) {
|
|
24
|
+
return queryParamsMatch[1]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return ''
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdc/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.24.1",
|
|
4
4
|
"description": "Core components, styles, hooks, and helpers, for the CDC Open Visualization project",
|
|
5
5
|
"moduleName": "CdcCore",
|
|
6
6
|
"main": "dist/cdccore",
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
"react": "^18.2.0",
|
|
31
31
|
"react-dom": "^18.2.0"
|
|
32
32
|
},
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "a352a3f74f4b681191e3244061dbb3621f36eec3"
|
|
34
34
|
}
|