@cdc/core 4.24.4 → 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.
- package/components/AdvancedEditor/AdvancedEditor.tsx +93 -0
- package/components/AdvancedEditor/advanced-editor-styles.css +3 -0
- package/components/AdvancedEditor/index.ts +1 -0
- package/components/DataTable/DataTable.tsx +33 -15
- package/components/DataTable/DataTableStandAlone.tsx +30 -4
- package/components/DataTable/components/ChartHeader.tsx +3 -2
- package/components/DataTable/components/DataTableEditorPanel.tsx +23 -6
- package/components/DataTable/components/ExpandCollapse.tsx +1 -1
- package/components/DataTable/helpers/chartCellMatrix.tsx +5 -10
- package/components/DataTable/helpers/getChartCellValue.ts +26 -2
- package/components/DataTable/helpers/getDataSeriesColumns.ts +40 -16
- package/components/DataTable/helpers/getRowType.ts +6 -0
- package/components/DataTable/helpers/getSeriesName.ts +2 -1
- package/components/DataTable/helpers/{customColumns.ts → removeNullColumns.ts} +3 -3
- package/components/DataTable/types/TableConfig.ts +12 -22
- package/components/EditorPanel/ColumnsEditor.tsx +278 -262
- package/components/EditorPanel/DataTableEditor.tsx +159 -60
- package/components/EditorPanel/FieldSetWrapper.tsx +51 -0
- package/components/EditorPanel/FootnotesEditor.tsx +77 -0
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +227 -0
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +54 -0
- package/components/EditorPanel/VizFilterEditor/index.ts +1 -0
- package/components/EditorWrapper/EditorWrapper.tsx +47 -0
- package/components/EditorWrapper/editor-wrapper.style.css +14 -0
- package/components/EditorWrapper/index.ts +1 -0
- package/components/{Filters.jsx → Filters.tsx} +102 -70
- package/components/Footnotes/Footnotes.tsx +25 -0
- package/components/Footnotes/FootnotesStandAlone.tsx +45 -0
- package/components/Footnotes/footnotes.css +5 -0
- package/components/Footnotes/index.ts +1 -0
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +8 -4
- package/components/Layout/components/Visualization/index.tsx +14 -5
- package/components/MediaControls.jsx +1 -1
- package/components/MultiSelect/MultiSelect.tsx +36 -9
- package/components/MultiSelect/multiselect.styles.css +0 -3
- package/components/_stories/DataTable.stories.tsx +1 -2
- package/components/_stories/EditorPanel.stories.tsx +1 -0
- package/components/_stories/Footnotes.stories.tsx +17 -0
- package/components/inputs/InputSelect.tsx +17 -6
- package/components/ui/Icon.tsx +1 -2
- package/helpers/DataTransform.ts +9 -32
- package/helpers/addValuesToFilters.ts +56 -0
- package/helpers/cove/accessibility.ts +1 -0
- package/helpers/cove/fontSettings.ts +2 -0
- package/helpers/coveUpdateWorker.ts +11 -2
- package/helpers/filterVizData.ts +30 -0
- package/helpers/footnoteSymbols.ts +11 -0
- package/helpers/formatConfigBeforeSave.ts +90 -0
- package/helpers/gatherQueryParams.ts +14 -7
- package/helpers/lineChartHelpers.js +2 -1
- package/helpers/pivotData.ts +18 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/tests/updateFieldFactory.test.ts +1 -0
- package/helpers/updateFieldFactory.ts +1 -1
- package/helpers/useDataVizClasses.js +0 -4
- package/helpers/ver/4.23.4.ts +27 -0
- package/helpers/ver/4.24.5.ts +32 -0
- package/helpers/ver/4.24.7.ts +92 -0
- package/package.json +6 -4
- package/styles/_button-section.scss +6 -1
- package/styles/_data-table.scss +0 -1
- package/styles/_reset.scss +7 -6
- package/styles/base.scss +4 -0
- package/styles/v2/themes/_color-definitions.scss +1 -0
- package/types/Annotation.ts +46 -0
- package/types/Column.ts +1 -0
- package/types/ConfigureData.ts +1 -1
- package/types/Footnotes.ts +17 -0
- package/types/General.ts +5 -0
- package/types/Legend.ts +1 -0
- package/types/MarkupInclude.ts +26 -0
- package/types/Runtime.ts +3 -7
- package/types/Series.ts +1 -1
- package/types/Table.ts +21 -14
- package/types/Visualization.ts +40 -11
- package/types/VizFilter.ts +24 -0
- package/LICENSE +0 -201
- package/components/AdvancedEditor.jsx +0 -74
- package/helpers/queryStringUtils.js +0 -26
- package/types/BaseVisualizationType.ts +0 -1
|
@@ -1,21 +1,64 @@
|
|
|
1
|
-
import React from 'react'
|
|
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'
|
|
5
|
-
import type { Table } from '@cdc/core/types/Table'
|
|
4
|
+
import { CheckBox, TextField, Select } from './Inputs'
|
|
6
5
|
import MultiSelect from '../MultiSelect'
|
|
7
6
|
import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
|
|
8
7
|
import { Visualization } from '../../types/Visualization'
|
|
8
|
+
import _ from 'lodash'
|
|
9
|
+
import { Column } from '../../types/Column'
|
|
9
10
|
|
|
10
11
|
interface DataTableProps {
|
|
11
|
-
config: Visualization
|
|
12
|
-
updateField: UpdateFieldFunc<string | boolean | string[] | number
|
|
12
|
+
config: Partial<Visualization>
|
|
13
|
+
updateField: UpdateFieldFunc<string | boolean | string[] | number | Record<string, Partial<Column>>>
|
|
13
14
|
isDashboard: boolean
|
|
14
15
|
columns: string[]
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
+
const PLACEHOLDER = '-Select-'
|
|
19
|
+
|
|
20
|
+
const DataTableEditor: React.FC<DataTableProps> = ({ config, updateField, isDashboard, columns: dataColumns }) => {
|
|
18
21
|
const isLoadedFromUrl = config.dataKey?.includes('http://') || config?.dataKey?.includes('https://')
|
|
22
|
+
const excludedColumns = useMemo(() => {
|
|
23
|
+
return Object.keys(config.columns)
|
|
24
|
+
.map<[string, boolean]>(key => [key, config.columns[key].dataTable])
|
|
25
|
+
.filter(([key, dataTable]) => !dataTable)
|
|
26
|
+
.map(([key]) => key)
|
|
27
|
+
}, [config.columns])
|
|
28
|
+
|
|
29
|
+
const groupPivotColumns = useMemo(() => {
|
|
30
|
+
const columns: string[] = config.data.flatMap(Object.keys)
|
|
31
|
+
const cols = _.uniq(columns).filter(key => {
|
|
32
|
+
return true
|
|
33
|
+
})
|
|
34
|
+
return cols
|
|
35
|
+
}, [config.data])
|
|
36
|
+
|
|
37
|
+
const changeGroupBy = (value: string) => {
|
|
38
|
+
if (value === PLACEHOLDER) value = undefined
|
|
39
|
+
updateField('table', null, 'groupBy', value)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const excludeColumns = (section, subSection, fieldName, excludedColNames: string[]) => {
|
|
43
|
+
const newColumns = _.cloneDeep(config.columns)
|
|
44
|
+
const colNames: string[] = []
|
|
45
|
+
for (let colKey in newColumns) {
|
|
46
|
+
const col = newColumns[colKey]
|
|
47
|
+
colNames.push(col.name) // keep track of all column names
|
|
48
|
+
if (excludedColNames.includes(col.name)) {
|
|
49
|
+
// ensure all excluded columns are set to false
|
|
50
|
+
newColumns[colKey].dataTable = false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
excludedColNames.forEach(colName => {
|
|
54
|
+
if (!colNames.includes(colName)) {
|
|
55
|
+
// make sure there is a column config to set to dataTable: false
|
|
56
|
+
newColumns[colName] = { name: colName, dataTable: false }
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
updateField(null, null, 'columns', newColumns)
|
|
60
|
+
}
|
|
61
|
+
|
|
19
62
|
return (
|
|
20
63
|
<>
|
|
21
64
|
<TextField
|
|
@@ -37,25 +80,28 @@ const DataTable: React.FC<DataTableProps> = ({ config, updateField, isDashboard,
|
|
|
37
80
|
</Tooltip>
|
|
38
81
|
}
|
|
39
82
|
/>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<Tooltip
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
83
|
+
{config.type !== 'table' && (
|
|
84
|
+
<CheckBox
|
|
85
|
+
value={config.table.show}
|
|
86
|
+
fieldName='show'
|
|
87
|
+
label='Show Data Table'
|
|
88
|
+
section='table'
|
|
89
|
+
updateField={updateField}
|
|
90
|
+
className='column-heading'
|
|
91
|
+
tooltip={
|
|
92
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
93
|
+
<Tooltip.Target>
|
|
94
|
+
<Icon display='question' style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }} />
|
|
95
|
+
</Tooltip.Target>
|
|
96
|
+
<Tooltip.Content>
|
|
97
|
+
<p>Hiding the data table may affect accessibility. An alternate form of accessing visualization data is a 508 requirement.</p>
|
|
98
|
+
</Tooltip.Content>
|
|
99
|
+
</Tooltip>
|
|
100
|
+
}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{config.visualizationType !== 'Box Plot' && config.type !== 'table' && (
|
|
59
105
|
<CheckBox
|
|
60
106
|
value={config.table.showVertical}
|
|
61
107
|
fieldName='showVertical'
|
|
@@ -75,74 +121,127 @@ const DataTable: React.FC<DataTableProps> = ({ config, updateField, isDashboard,
|
|
|
75
121
|
}
|
|
76
122
|
/>
|
|
77
123
|
)}
|
|
124
|
+
|
|
125
|
+
{config.type !== 'table' && (
|
|
126
|
+
<TextField
|
|
127
|
+
value={config.table.indexLabel}
|
|
128
|
+
section='table'
|
|
129
|
+
fieldName='indexLabel'
|
|
130
|
+
label='Index Column Header'
|
|
131
|
+
updateField={updateField}
|
|
132
|
+
tooltip={
|
|
133
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
134
|
+
<Tooltip.Target>
|
|
135
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
136
|
+
</Tooltip.Target>
|
|
137
|
+
<Tooltip.Content>
|
|
138
|
+
<p>To comply with 508 standards, if the first column in the data table has no header, enter a brief one here.</p>
|
|
139
|
+
</Tooltip.Content>
|
|
140
|
+
</Tooltip>
|
|
141
|
+
}
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
78
144
|
<TextField
|
|
79
|
-
value={config.table.
|
|
80
|
-
section='table'
|
|
81
|
-
fieldName='indexLabel'
|
|
82
|
-
label='Index Column Header'
|
|
145
|
+
value={config.table.caption}
|
|
83
146
|
updateField={updateField}
|
|
147
|
+
section='table'
|
|
148
|
+
type='textarea'
|
|
149
|
+
fieldName='caption'
|
|
150
|
+
label='Screen Reader Description'
|
|
151
|
+
placeholder=' Data table'
|
|
84
152
|
tooltip={
|
|
85
153
|
<Tooltip style={{ textTransform: 'none' }}>
|
|
86
154
|
<Tooltip.Target>
|
|
87
155
|
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
88
156
|
</Tooltip.Target>
|
|
89
157
|
<Tooltip.Content>
|
|
90
|
-
<p>
|
|
158
|
+
<p>Enter a description of the data table to be read by screen readers.</p>
|
|
91
159
|
</Tooltip.Content>
|
|
92
160
|
</Tooltip>
|
|
93
161
|
}
|
|
94
162
|
/>
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
163
|
+
<CheckBox value={config.table.limitHeight} section='table' fieldName='limitHeight' label=' Limit Table Height' updateField={updateField} />
|
|
164
|
+
{config.table.limitHeight && <TextField value={config.table.height} section='table' fieldName='height' label='Data Table Height' type='number' min={0} max={500} placeholder='Height(px)' updateField={updateField} />}
|
|
165
|
+
{config?.visualizationType !== 'Sankey' && <MultiSelect key={excludedColumns.join('') + 'excluded'} options={dataColumns.map(c => ({ label: c, value: c }))} selected={excludedColumns} fieldName='dataTable' label='Exclude Columns' section='columns' updateField={excludeColumns} />}
|
|
166
|
+
<CheckBox value={config.table.collapsible} fieldName='collapsible' label=' Collapsible' section='table' updateField={updateField} />
|
|
167
|
+
{config.table.collapsible !== false && <CheckBox value={config.table.expanded} fieldName='expanded' label=' Expanded by Default' section='table' updateField={updateField} />}
|
|
168
|
+
{isDashboard && config.type !== 'table' && <CheckBox value={config.table.showDataTableLink} fieldName='showDataTableLink' label='Show Data Table Name & Link' section='table' updateField={updateField} />}
|
|
169
|
+
{isLoadedFromUrl && <CheckBox value={config.table.showDownloadUrl} fieldName='showDownloadUrl' label='Show URL to Automatically Updated Data' section='table' updateField={updateField} />}
|
|
170
|
+
{config.type !== 'table' && <CheckBox value={config.table.showDownloadImgButton} fieldName='showDownloadImgButton' label='Display Image Button' section='table' updateField={updateField} />}
|
|
171
|
+
<label>
|
|
172
|
+
<span className='edit-label column-heading'>Table Cell Min Width</span>
|
|
173
|
+
<input type='number' value={config.table.cellMinWidth ? config.table.cellMinWidth : 0} onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)} />
|
|
174
|
+
</label>
|
|
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: '
|
|
103
203
|
tooltip={
|
|
104
204
|
<Tooltip style={{ textTransform: 'none' }}>
|
|
105
205
|
<Tooltip.Target>
|
|
106
206
|
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
107
207
|
</Tooltip.Target>
|
|
108
208
|
<Tooltip.Content>
|
|
109
|
-
<p>
|
|
209
|
+
<p>Select a Column whos data values will be pivoted to Column Values.</p>
|
|
110
210
|
</Tooltip.Content>
|
|
111
211
|
</Tooltip>
|
|
112
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}
|
|
113
220
|
/>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
value={config.table.customTableConfig}
|
|
118
|
-
fieldName='customTableConfig'
|
|
119
|
-
label='Customize Table Config'
|
|
120
|
-
section='table'
|
|
121
|
-
updateField={updateField}
|
|
221
|
+
{config.table.pivot?.columnName && (
|
|
222
|
+
<Select
|
|
223
|
+
label='Pivot Value Column: '
|
|
122
224
|
tooltip={
|
|
123
225
|
<Tooltip style={{ textTransform: 'none' }}>
|
|
124
226
|
<Tooltip.Target>
|
|
125
|
-
<Icon display='question' style={{ marginLeft: '0.5rem'
|
|
227
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
126
228
|
</Tooltip.Target>
|
|
127
229
|
<Tooltip.Content>
|
|
128
|
-
<p>
|
|
230
|
+
<p>The column whos values will be pivoted under the column selected as the Filter.</p>
|
|
129
231
|
</Tooltip.Content>
|
|
130
232
|
</Tooltip>
|
|
131
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}
|
|
132
241
|
/>
|
|
133
242
|
)}
|
|
134
|
-
{config.table.customTableConfig && <MultiSelect options={columns.map(c => ({ label: c, value: c }))} fieldName='excludeColumns' label='Exclude Columns' section='table' updateField={updateField} />}
|
|
135
|
-
{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} />}
|
|
136
|
-
<CheckBox value={config.table.expanded} fieldName='expanded' label='Expanded by Default' section='table' updateField={updateField} />
|
|
137
|
-
{isDashboard && config.type !== 'table' && <CheckBox value={config.table.showDataTableLink} fieldName='showDataTableLink' label='Show Data Table Name & Link' section='table' updateField={updateField} />}
|
|
138
|
-
{isLoadedFromUrl && <CheckBox value={config.table.showDownloadUrl} fieldName='showDownloadUrl' label='Show URL to Automatically Updated Data' section='table' updateField={updateField} />}
|
|
139
|
-
{config.type !== 'table' && <CheckBox value={config.table.showDownloadImgButton} fieldName='showDownloadImgButton' label='Display Image Button' section='table' updateField={updateField} />}
|
|
140
|
-
<label>
|
|
141
|
-
<span className='edit-label column-heading'>Table Cell Min Width</span>
|
|
142
|
-
<input type='number' value={config.table.cellMinWidth ? config.table.cellMinWidth : 0} onChange={e => updateField('table', null, 'cellMinWidth', e.target.value)} />
|
|
143
|
-
</label>
|
|
144
243
|
</>
|
|
145
244
|
)
|
|
146
245
|
}
|
|
147
246
|
|
|
148
|
-
export default
|
|
247
|
+
export default DataTableEditor
|
|
@@ -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
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { UpdateFieldFunc } from '../../types/UpdateFieldFunc'
|
|
2
|
+
import _ from 'lodash'
|
|
3
|
+
import Footnotes, { Footnote } from '../../types/Footnotes'
|
|
4
|
+
import { footnotesSymbols } from '../../helpers/footnoteSymbols'
|
|
5
|
+
import InputSelect from '../inputs/InputSelect'
|
|
6
|
+
import { TextField } from './Inputs'
|
|
7
|
+
interface FootnotesEditorProps {
|
|
8
|
+
config: Footnotes
|
|
9
|
+
updateField: UpdateFieldFunc<Footnote[]>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FootnotesEditor: React.FC<FootnotesEditorProps> = ({ config, updateField }) => {
|
|
13
|
+
const addStaticFootnote = () => {
|
|
14
|
+
const newStaticNotes = [...(config.staticFootnotes || []), { text: 'Add Footnote Text' }]
|
|
15
|
+
updateField(null, null, 'staticFootnotes', newStaticNotes)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const updateStaticFootnote = (footnoteIndex, footnoteUpdate: Footnote) => {
|
|
19
|
+
const footnoteCopy = _.cloneDeep(config.staticFootnotes)
|
|
20
|
+
footnoteCopy[footnoteIndex] = footnoteUpdate
|
|
21
|
+
updateField(null, null, 'staticFootnotes', footnoteCopy)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const deleteStaticFootnote = footnoteIndex => {
|
|
25
|
+
const footnoteCopy = _.cloneDeep(config.staticFootnotes)
|
|
26
|
+
footnoteCopy.splice(footnoteIndex, 1)
|
|
27
|
+
updateField(null, null, 'staticFootnotes', footnoteCopy)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const getOptions = (opts: string[]) => {
|
|
31
|
+
return [['', '--Select--']].concat(opts.map(key => [key, key]))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const datasets = config.datasets || {}
|
|
35
|
+
|
|
36
|
+
const dataColumns = config.dataKey ? getOptions(Object.keys(datasets[config.dataKey]?.data?.[0] || {})) : []
|
|
37
|
+
const dataSetOptions = getOptions(Object.keys(datasets))
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<em>Dynamic Footnotes</em>
|
|
41
|
+
<div className='row border p-2'>
|
|
42
|
+
<InputSelect label='Select a Footnote Dataset' value={config.dataKey} options={dataSetOptions} fieldName='dataKey' updateField={updateField} />
|
|
43
|
+
|
|
44
|
+
{config.dataKey && (
|
|
45
|
+
<div className='p-3'>
|
|
46
|
+
<InputSelect label='Footnote Symbol Column' value={config.dynamicFootnotes?.symbolColumn} options={dataColumns} section='dynamicFootnotes' fieldName='symbolColumn' updateField={updateField} />
|
|
47
|
+
<InputSelect label='Footnote Text Column' value={config.dynamicFootnotes?.textColumn} options={dataColumns} section='dynamicFootnotes' fieldName='textColumn' updateField={updateField} />
|
|
48
|
+
<InputSelect label='Footnote Order Column' value={config.dynamicFootnotes?.orderColumn} options={dataColumns} section='dynamicFootnotes' fieldName='orderColumn' updateField={updateField} />
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<hr />
|
|
54
|
+
|
|
55
|
+
<em>Static Footnotes</em>
|
|
56
|
+
|
|
57
|
+
{config.staticFootnotes?.map((note, index) => (
|
|
58
|
+
<div key={index} className='row border p-2'>
|
|
59
|
+
<div className='col-8'>
|
|
60
|
+
<InputSelect label='Symbol' value={note.symbol} options={[['', '--Select--'], ...footnotesSymbols]} fieldName='symbol' updateField={(section, subsection, fieldName, value) => updateStaticFootnote(index, { ...note, symbol: value })} />{' '}
|
|
61
|
+
<TextField label='Text' value={note.text} fieldName='text' updateField={(section, subsection, fieldName, value) => updateStaticFootnote(index, { ...note, text: value })} />
|
|
62
|
+
</div>
|
|
63
|
+
<div className='col-2 ml-4'>
|
|
64
|
+
<button className='btn btn-danger p-1' onClick={() => deleteStaticFootnote(index)}>
|
|
65
|
+
Delete
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
<button className='btn btn-primary' onClick={addStaticFootnote}>
|
|
71
|
+
Add Static Footnote
|
|
72
|
+
</button>
|
|
73
|
+
</>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default FootnotesEditor
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { Select, TextField } from '../Inputs'
|
|
2
|
+
import Tooltip from '../../ui/Tooltip'
|
|
3
|
+
import Icon from '../../ui/Icon'
|
|
4
|
+
import { Visualization } from '../../../types/Visualization'
|
|
5
|
+
import { UpdateFieldFunc } from '../../../types/UpdateFieldFunc'
|
|
6
|
+
import _ from 'lodash'
|
|
7
|
+
import { MultiSelectFilter, VizFilter } from '../../../types/VizFilter'
|
|
8
|
+
import { filterStyleOptions, handleSorting } from '../../Filters'
|
|
9
|
+
import FieldSetWrapper from '../FieldSetWrapper'
|
|
10
|
+
|
|
11
|
+
import FilterOrder from './components/FilterOrder'
|
|
12
|
+
import { useMemo, useState } from 'react'
|
|
13
|
+
|
|
14
|
+
type VizFilterProps = {
|
|
15
|
+
config: Visualization
|
|
16
|
+
updateField: UpdateFieldFunc<string | VizFilter[] | VizFilter>
|
|
17
|
+
rawData: Object[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const VizFilterEditor: React.FC<VizFilterProps> = ({ config, updateField, rawData }) => {
|
|
21
|
+
const openControls = useState({})
|
|
22
|
+
const dataColumns = useMemo(() => {
|
|
23
|
+
return _.uniq(_.flatten(rawData?.map(row => Object.keys(row))))
|
|
24
|
+
}, [rawData])
|
|
25
|
+
|
|
26
|
+
const removeFilter = index => {
|
|
27
|
+
let filters = _.cloneDeep(config.filters)
|
|
28
|
+
|
|
29
|
+
filters.splice(index, 1)
|
|
30
|
+
|
|
31
|
+
updateField(null, null, 'filters', filters)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const updateFilterProp = (prop, index, value) => {
|
|
35
|
+
updateField('filters', index, prop, value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const updateFilterStyle = (index, value) => {
|
|
39
|
+
const filters = _.cloneDeep(config.filters)
|
|
40
|
+
const currentFilter = filters[index]
|
|
41
|
+
currentFilter.filterStyle = value
|
|
42
|
+
if (value === 'multi-select') {
|
|
43
|
+
currentFilter.active = Array.isArray(currentFilter.active) ? currentFilter.active : [currentFilter.active]
|
|
44
|
+
} else if (Array.isArray(currentFilter.active)) {
|
|
45
|
+
currentFilter.active = currentFilter.active[0]
|
|
46
|
+
}
|
|
47
|
+
filters[index] = currentFilter
|
|
48
|
+
updateField(null, null, 'filters', filters)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleNameChange = (filterIndex, columnName) => {
|
|
52
|
+
const values = _.uniq(rawData.map(row => row[columnName]))
|
|
53
|
+
const copiedFilter = { ..._.cloneDeep(config.filters[filterIndex]), columnName, values }
|
|
54
|
+
handleSorting(copiedFilter) // sorts dropdown values in place
|
|
55
|
+
copiedFilter.active = copiedFilter.values[0]
|
|
56
|
+
const newFilters = config.filters.map((filter, index) => {
|
|
57
|
+
if (index === filterIndex) return copiedFilter
|
|
58
|
+
return filter
|
|
59
|
+
})
|
|
60
|
+
updateField(null, null, 'filters', newFilters)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const addNewFilter = () => {
|
|
64
|
+
const filters = config.filters ? [...config.filters] : []
|
|
65
|
+
const newVizFilter: VizFilter = { values: [], filterStyle: 'dropdown' } as VizFilter
|
|
66
|
+
filters.push(newVizFilter)
|
|
67
|
+
updateField(null, null, 'filters', filters)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handleFilterOrder = (idx1, idx2, filterIndex, filter) => {
|
|
71
|
+
// Create a shallow copy of the filter values array & update position of the values
|
|
72
|
+
const updatedValues = [...filter.values]
|
|
73
|
+
const [movedItem] = updatedValues.splice(idx1, 1)
|
|
74
|
+
updatedValues.splice(idx2, 0, movedItem)
|
|
75
|
+
|
|
76
|
+
const filtersCopy = _.cloneDeep(config.filters)
|
|
77
|
+
const filterItem = { ...filtersCopy[filterIndex] }
|
|
78
|
+
|
|
79
|
+
// Overwrite filterItem.values since thats what we map through in the editor panel
|
|
80
|
+
filterItem.values = updatedValues
|
|
81
|
+
filterItem.orderedValues = updatedValues
|
|
82
|
+
filterItem.active = updatedValues[0]
|
|
83
|
+
filterItem.order = 'cust'
|
|
84
|
+
|
|
85
|
+
// Update the filters
|
|
86
|
+
filtersCopy[filterIndex] = filterItem
|
|
87
|
+
|
|
88
|
+
updateField(null, null, 'filters', filtersCopy)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
{config.filters && (
|
|
94
|
+
<>
|
|
95
|
+
<Select
|
|
96
|
+
value={config.filterBehavior}
|
|
97
|
+
fieldName='filterBehavior'
|
|
98
|
+
label='Filter Behavior'
|
|
99
|
+
updateField={updateField}
|
|
100
|
+
options={['Apply Button', 'Filter Change']}
|
|
101
|
+
tooltip={
|
|
102
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
103
|
+
<Tooltip.Target>
|
|
104
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
105
|
+
</Tooltip.Target>
|
|
106
|
+
<Tooltip.Content>
|
|
107
|
+
<p>The Apply Button option changes the visualization when the user clicks "apply". The Filter Change option immediately changes the visualization when the selection is changed.</p>
|
|
108
|
+
</Tooltip.Content>
|
|
109
|
+
</Tooltip>
|
|
110
|
+
}
|
|
111
|
+
/>
|
|
112
|
+
<br />
|
|
113
|
+
<ul className='filters-list'>
|
|
114
|
+
{/* Whether filters should apply onChange or Apply Button */}
|
|
115
|
+
|
|
116
|
+
{config.filters.map((filter, index) => {
|
|
117
|
+
if (filter.type === 'url') return <></>
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<FieldSetWrapper fieldName={filter.columnName} fieldKey={index} fieldType='Filter' controls={openControls} deleteField={() => removeFilter(index)}>
|
|
121
|
+
<label>
|
|
122
|
+
<span className='edit-label column-heading'>Filter</span>
|
|
123
|
+
<select
|
|
124
|
+
value={filter.columnName}
|
|
125
|
+
onChange={e => {
|
|
126
|
+
handleNameChange(index, e.target.value)
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
<option value=''>- Select Option -</option>
|
|
130
|
+
{dataColumns.map((dataKey, index) => (
|
|
131
|
+
<option value={dataKey} key={index}>
|
|
132
|
+
{dataKey}
|
|
133
|
+
</option>
|
|
134
|
+
))}
|
|
135
|
+
</select>
|
|
136
|
+
</label>
|
|
137
|
+
|
|
138
|
+
<label>
|
|
139
|
+
<span className='edit-showDropdown column-heading'>Show Filter Input</span>
|
|
140
|
+
<input
|
|
141
|
+
type='checkbox'
|
|
142
|
+
checked={filter.showDropdown === undefined ? true : filter.showDropdown}
|
|
143
|
+
onChange={e => {
|
|
144
|
+
updateFilterProp('showDropdown', index, e.target.checked)
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
</label>
|
|
148
|
+
|
|
149
|
+
<label>
|
|
150
|
+
<span className='edit-label column-heading'>Filter Style</span>
|
|
151
|
+
|
|
152
|
+
<select
|
|
153
|
+
value={filter.filterStyle}
|
|
154
|
+
onChange={e => {
|
|
155
|
+
updateFilterStyle(index, e.target.value)
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{filterStyleOptions.map((item, index) => {
|
|
159
|
+
return (
|
|
160
|
+
<option key={`filter-style-${index}`} value={item}>
|
|
161
|
+
{item}
|
|
162
|
+
</option>
|
|
163
|
+
)
|
|
164
|
+
})}
|
|
165
|
+
</select>
|
|
166
|
+
</label>
|
|
167
|
+
|
|
168
|
+
<label>
|
|
169
|
+
<span className='edit-label column-heading'>Label</span>
|
|
170
|
+
<input
|
|
171
|
+
type='text'
|
|
172
|
+
value={filter.label}
|
|
173
|
+
onChange={e => {
|
|
174
|
+
updateFilterProp('label', index, e.target.value)
|
|
175
|
+
}}
|
|
176
|
+
/>
|
|
177
|
+
</label>
|
|
178
|
+
|
|
179
|
+
{filter.filterStyle === 'multi-select' && (
|
|
180
|
+
<TextField
|
|
181
|
+
label='Select Limit'
|
|
182
|
+
value={(filter as MultiSelectFilter).selectLimit}
|
|
183
|
+
updateField={updateField}
|
|
184
|
+
section='filters'
|
|
185
|
+
subsection={index}
|
|
186
|
+
fieldName='selectLimit'
|
|
187
|
+
type='number'
|
|
188
|
+
tooltip={
|
|
189
|
+
<Tooltip style={{ textTransform: 'none' }}>
|
|
190
|
+
<Tooltip.Target>
|
|
191
|
+
<Icon display='question' style={{ marginLeft: '0.5rem' }} />
|
|
192
|
+
</Tooltip.Target>
|
|
193
|
+
<Tooltip.Content>
|
|
194
|
+
<p>The maximum number of items that can be selected.</p>
|
|
195
|
+
</Tooltip.Content>
|
|
196
|
+
</Tooltip>
|
|
197
|
+
}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
<label>
|
|
202
|
+
<span className='edit-label column-heading'>Default Value Set By Query String Parameter</span>
|
|
203
|
+
<input
|
|
204
|
+
type='text'
|
|
205
|
+
value={filter.setByQueryParameter}
|
|
206
|
+
onChange={e => {
|
|
207
|
+
updateFilterProp('setByQueryParameter', index, e.target.value)
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
</label>
|
|
211
|
+
|
|
212
|
+
<FilterOrder filterIndex={index} filter={filter} updateFilterProp={updateFilterProp} handleFilterOrder={handleFilterOrder} />
|
|
213
|
+
</FieldSetWrapper>
|
|
214
|
+
)
|
|
215
|
+
})}
|
|
216
|
+
</ul>
|
|
217
|
+
</>
|
|
218
|
+
)}
|
|
219
|
+
{!config.filters && <p style={{ textAlign: 'center' }}>There are currently no filters.</p>}
|
|
220
|
+
<button type='button' onClick={addNewFilter} className='btn btn-primary full-width'>
|
|
221
|
+
Add Filter
|
|
222
|
+
</button>
|
|
223
|
+
</>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export default VizFilterEditor
|