@cdc/core 4.24.5 → 4.24.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +21 -2
- package/components/DataTable/DataTableStandAlone.tsx +4 -25
- package/components/DataTable/components/DataTableEditorPanel.tsx +4 -4
- package/components/DataTable/components/ExpandCollapse.tsx +1 -1
- package/components/DataTable/helpers/chartCellMatrix.tsx +3 -9
- package/components/DataTable/helpers/getChartCellValue.ts +8 -4
- package/components/DataTable/helpers/getDataSeriesColumns.ts +8 -5
- package/components/DataTable/helpers/getRowType.ts +6 -0
- package/components/DataTable/types/TableConfig.ts +1 -0
- package/components/EditorPanel/ColumnsEditor.tsx +3 -30
- package/components/EditorPanel/DataTableEditor.tsx +66 -22
- 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 +3 -4
- package/components/EditorWrapper/index.ts +1 -0
- package/components/{Filters.jsx → Filters.tsx} +40 -24
- 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 +12 -5
- package/components/MultiSelect/MultiSelect.tsx +36 -9
- package/components/MultiSelect/multiselect.styles.css +0 -3
- package/components/_stories/Footnotes.stories.tsx +17 -0
- package/components/_stories/styles.scss +1 -0
- package/components/inputs/InputSelect.tsx +17 -6
- package/components/ui/Icon.tsx +1 -2
- package/helpers/addValuesToFilters.ts +56 -0
- package/helpers/cove/accessibility.ts +1 -0
- package/helpers/cove/fontSettings.ts +2 -0
- package/helpers/coveUpdateWorker.ts +7 -0
- package/helpers/filterVizData.ts +30 -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/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/base.scss +4 -0
- package/styles/v2/themes/_color-definitions.scss +1 -0
- package/types/Annotation.ts +46 -0
- package/types/Axis.ts +0 -2
- package/types/ConfigureData.ts +1 -1
- package/types/Footnotes.ts +17 -0
- package/types/General.ts +5 -0
- package/types/Runtime.ts +2 -7
- package/types/Table.ts +6 -0
- package/types/Visualization.ts +31 -9
- package/types/VizFilter.ts +16 -5
- package/LICENSE +0 -201
- package/components/AdvancedEditor.jsx +0 -74
- package/components/EditorPanel/VizFilterEditor.tsx +0 -234
- package/helpers/queryStringUtils.js +0 -26
- package/types/BaseVisualizationType.ts +0 -1
|
@@ -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
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { VizFilter } from '../../../../types/VizFilter'
|
|
2
|
+
import { filterOrderOptions } from '../../../Filters'
|
|
3
|
+
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'
|
|
4
|
+
|
|
5
|
+
type FilterOrderProps = {
|
|
6
|
+
filterIndex: number
|
|
7
|
+
filter: VizFilter
|
|
8
|
+
updateFilterProp: (prop: string, index: number, value: string) => void
|
|
9
|
+
handleFilterOrder: (sourceIndex: number, destinationIndex: number, filterIndex: number, filter: VizFilter) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FilterOrder: React.FC<FilterOrderProps> = ({ filterIndex, filter, updateFilterProp, handleFilterOrder }) => {
|
|
13
|
+
return (
|
|
14
|
+
<label>
|
|
15
|
+
<span className='edit-filterOrder column-heading'>Filter Order</span>
|
|
16
|
+
<select value={filter.order ? filter.order : 'asc'} onChange={e => updateFilterProp('order', filterIndex, e.target.value)}>
|
|
17
|
+
{filterOrderOptions.map((option, index) => {
|
|
18
|
+
return (
|
|
19
|
+
<option value={option.value} key={`filter-${index}`}>
|
|
20
|
+
{option.label}
|
|
21
|
+
</option>
|
|
22
|
+
)
|
|
23
|
+
})}
|
|
24
|
+
</select>
|
|
25
|
+
|
|
26
|
+
{filter.order === 'cust' && (
|
|
27
|
+
<DragDropContext onDragEnd={({ source, destination }) => handleFilterOrder(source.index, destination.index, filterIndex, filter)}>
|
|
28
|
+
<Droppable droppableId='filter_order'>
|
|
29
|
+
{provided => (
|
|
30
|
+
<ul {...provided.droppableProps} className='sort-list' ref={provided.innerRef} style={{ marginTop: '1em' }}>
|
|
31
|
+
{filter?.values.map((value, index) => {
|
|
32
|
+
return (
|
|
33
|
+
<Draggable key={value} draggableId={`draggableFilter-${value}`} index={index}>
|
|
34
|
+
{(provided, snapshot) => (
|
|
35
|
+
<li>
|
|
36
|
+
<div className={snapshot.isDragging ? 'currently-dragging' : ''} style={provided.draggableProps.style} ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
|
37
|
+
{value}
|
|
38
|
+
</div>
|
|
39
|
+
</li>
|
|
40
|
+
)}
|
|
41
|
+
</Draggable>
|
|
42
|
+
)
|
|
43
|
+
})}
|
|
44
|
+
{provided.placeholder}
|
|
45
|
+
</ul>
|
|
46
|
+
)}
|
|
47
|
+
</Droppable>
|
|
48
|
+
</DragDropContext>
|
|
49
|
+
)}
|
|
50
|
+
</label>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default FilterOrder
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './VizFilterEditor'
|
|
@@ -2,7 +2,6 @@ import React from 'react'
|
|
|
2
2
|
import { Visualization } from '../../types/Visualization'
|
|
3
3
|
import { ViewPort } from '../../types/ViewPort'
|
|
4
4
|
import './editor-wrapper.style.css'
|
|
5
|
-
import { Accordion } from 'react-accessible-accordion'
|
|
6
5
|
|
|
7
6
|
type StandAloneComponentProps = {
|
|
8
7
|
visualizationKey: string
|
|
@@ -12,6 +11,8 @@ type StandAloneComponentProps = {
|
|
|
12
11
|
setEditing: Function
|
|
13
12
|
hostname: string
|
|
14
13
|
viewport?: ViewPort
|
|
14
|
+
|
|
15
|
+
[key: string]: any
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
type EditorProps = {
|
|
@@ -33,9 +34,7 @@ const EditorWrapper: React.FC<React.PropsWithChildren<EditorProps>> = ({ childre
|
|
|
33
34
|
<div aria-level={2} role='heading' className='heading-2'>
|
|
34
35
|
Configure {type}
|
|
35
36
|
</div>
|
|
36
|
-
<section>
|
|
37
|
-
<Accordion allowZeroExpanded={true}>{children}</Accordion>
|
|
38
|
-
</section>
|
|
37
|
+
<section>{children}</section>
|
|
39
38
|
</section>
|
|
40
39
|
<div className='preview-wrapper'>
|
|
41
40
|
<Component visualizationKey={visualizationKey} config={visualizationConfig} updateConfig={updateConfig} configUrl={undefined} setEditing={undefined} hostname={undefined} viewport={viewport} />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './EditorWrapper'
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
2
|
import { useId } from 'react'
|
|
3
3
|
|
|
4
4
|
// CDC
|
|
5
|
-
import Button from '
|
|
6
|
-
import { getQueryParams, updateQueryString } from '
|
|
5
|
+
import Button from './elements/Button'
|
|
6
|
+
import { getQueryParams, updateQueryString } from '../helpers/queryStringUtils'
|
|
7
7
|
|
|
8
8
|
// Third Party
|
|
9
9
|
import PropTypes from 'prop-types'
|
|
10
|
+
import MultiSelect from './MultiSelect'
|
|
11
|
+
import { Visualization } from '../types/Visualization'
|
|
12
|
+
import { MultiSelectFilter, VizFilter } from '../types/VizFilter'
|
|
13
|
+
import { filterVizData } from '../helpers/filterVizData'
|
|
14
|
+
import { addValuesToFilters } from '../helpers/addValuesToFilters'
|
|
10
15
|
|
|
11
|
-
export const filterStyleOptions = ['dropdown', 'pill', 'tab', 'tab bar']
|
|
16
|
+
export const filterStyleOptions = ['dropdown', 'pill', 'tab', 'tab bar', 'multi-select']
|
|
12
17
|
|
|
13
18
|
export const filterOrderOptions = [
|
|
14
19
|
{
|
|
@@ -57,7 +62,7 @@ export const useFilters = props => {
|
|
|
57
62
|
|
|
58
63
|
// Desconstructing: notice, adding more descriptive visualizationConfig name over config
|
|
59
64
|
// visualizationConfig feels more robust for all vis types so that its not confused with config/state/etc.
|
|
60
|
-
const { config: visualizationConfig, setConfig, filteredData, setFilteredData, excludedData,
|
|
65
|
+
const { config: visualizationConfig, setConfig, filteredData, setFilteredData, excludedData, getUniqueValues } = props
|
|
61
66
|
const { type, data } = visualizationConfig
|
|
62
67
|
|
|
63
68
|
/**
|
|
@@ -127,7 +132,7 @@ export const useFilters = props => {
|
|
|
127
132
|
|
|
128
133
|
// If we're on a chart and not using the apply button
|
|
129
134
|
if (hasStandardFilterBehavior.includes(visualizationConfig.type) && visualizationConfig.filterBehavior === 'Filter Change') {
|
|
130
|
-
setFilteredData(
|
|
135
|
+
setFilteredData(filterVizData(newFilters, excludedData))
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
|
|
@@ -155,7 +160,7 @@ export const useFilters = props => {
|
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
if (hasStandardFilterBehavior.includes(visualizationConfig.type)) {
|
|
158
|
-
setFilteredData(
|
|
163
|
+
setFilteredData(filterVizData(newFilters, excludedData))
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
setShowApplyButton(false)
|
|
@@ -169,12 +174,11 @@ export const useFilters = props => {
|
|
|
169
174
|
let needsQueryUpdate = false
|
|
170
175
|
const queryParams = getQueryParams()
|
|
171
176
|
newFilters.forEach((filter, i) => {
|
|
172
|
-
if(!filter.values || filter.values.length === 0){
|
|
177
|
+
if (!filter.values || filter.values.length === 0) {
|
|
173
178
|
filter.values = getUniqueValues(data, filter.columnName)
|
|
174
179
|
}
|
|
175
180
|
newFilters[i].active = handleSorting(filter).values[0]
|
|
176
181
|
|
|
177
|
-
|
|
178
182
|
if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== filter.active) {
|
|
179
183
|
queryParams[filter.setByQueryParameter] = filter.active
|
|
180
184
|
needsQueryUpdate = true
|
|
@@ -190,9 +194,8 @@ export const useFilters = props => {
|
|
|
190
194
|
if (type === 'map') {
|
|
191
195
|
setFilteredData(newFilters, excludedData)
|
|
192
196
|
} else {
|
|
193
|
-
setFilteredData(
|
|
197
|
+
setFilteredData(filterVizData(newFilters, excludedData))
|
|
194
198
|
}
|
|
195
|
-
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
const filterConstants = {
|
|
@@ -217,11 +220,17 @@ export const useFilters = props => {
|
|
|
217
220
|
}
|
|
218
221
|
}
|
|
219
222
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
+
type FilterProps = {
|
|
224
|
+
filteredData
|
|
225
|
+
dimensions
|
|
226
|
+
config: Visualization
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const Filters = (props: FilterProps) => {
|
|
230
|
+
const { config: visualizationConfig, filteredData, dimensions } = props
|
|
231
|
+
const { filters, type, general, theme, filterBehavior } = visualizationConfig
|
|
223
232
|
const [mobileFilterStyle, setMobileFilterStyle] = useState(false)
|
|
224
|
-
const [selectedFilter, setSelectedFilter] = useState(
|
|
233
|
+
const [selectedFilter, setSelectedFilter] = useState<EventTarget>(null)
|
|
225
234
|
const id = useId()
|
|
226
235
|
|
|
227
236
|
// useFilters hook provides data and logic for handling various filter functions
|
|
@@ -247,7 +256,7 @@ const Filters = props => {
|
|
|
247
256
|
|
|
248
257
|
useEffect(() => {
|
|
249
258
|
if (selectedFilter) {
|
|
250
|
-
|
|
259
|
+
const el = document.getElementById(selectedFilter.id)
|
|
251
260
|
if (el) el.focus()
|
|
252
261
|
}
|
|
253
262
|
}, [changeFilterActive, selectedFilter])
|
|
@@ -256,21 +265,21 @@ const Filters = props => {
|
|
|
256
265
|
|
|
257
266
|
const filterSectionClassList = ['filters-section', type === 'map' ? general.headerColor : visualizationConfig?.visualizationType === 'Spark Line' ? null : theme]
|
|
258
267
|
// Exterior Section Wrapper
|
|
259
|
-
Filters.Section =
|
|
268
|
+
Filters.Section = ({ children }) => {
|
|
260
269
|
return (
|
|
261
270
|
visualizationConfig?.filters && (
|
|
262
271
|
<section className={filterSectionClassList.join(' ')}>
|
|
263
272
|
<p className='filters-section__intro-text'>
|
|
264
273
|
{filters?.some(f => f.active) ? filterConstants.introText : ''} {visualizationConfig.filterBehavior === 'Apply Button' && filterConstants.applyText}
|
|
265
274
|
</p>
|
|
266
|
-
<div className='filters-section__wrapper'>{
|
|
275
|
+
<div className='filters-section__wrapper'>{children}</div>
|
|
267
276
|
</section>
|
|
268
277
|
)
|
|
269
278
|
)
|
|
270
279
|
}
|
|
271
280
|
|
|
272
281
|
// Apply/Reset Buttons
|
|
273
|
-
Filters.ApplyBehavior =
|
|
282
|
+
Filters.ApplyBehavior = () => {
|
|
274
283
|
if (filterBehavior !== 'Apply Button') return
|
|
275
284
|
const applyButtonClasses = [general?.headerColor ? general.headerColor : theme, 'apply']
|
|
276
285
|
return (
|
|
@@ -348,7 +357,7 @@ const Filters = props => {
|
|
|
348
357
|
// Remove fromHash if it exists on filters to loop so we can loop nicely
|
|
349
358
|
delete filtersToLoop.fromHash
|
|
350
359
|
|
|
351
|
-
return filtersToLoop.map((singleFilter, outerIndex) => {
|
|
360
|
+
return addValuesToFilters<VizFilter>(filtersToLoop, visualizationConfig.data).map((singleFilter: VizFilter, outerIndex) => {
|
|
352
361
|
if (singleFilter.showDropdown === false) return
|
|
353
362
|
|
|
354
363
|
const values = []
|
|
@@ -356,11 +365,11 @@ const Filters = props => {
|
|
|
356
365
|
const tabValues = []
|
|
357
366
|
const tabBarValues = []
|
|
358
367
|
|
|
359
|
-
const { active, queuedActive, label, filterStyle } = singleFilter
|
|
368
|
+
const { active, queuedActive, label, filterStyle } = singleFilter as VizFilter
|
|
360
369
|
|
|
361
370
|
handleSorting(singleFilter)
|
|
362
371
|
|
|
363
|
-
singleFilter.values
|
|
372
|
+
singleFilter.values?.forEach((filterOption, index) => {
|
|
364
373
|
const pillClassList = ['pill', active === filterOption ? 'pill--active' : null, theme && theme]
|
|
365
374
|
const tabClassList = ['tab', active === filterOption && 'tab--active', theme && theme]
|
|
366
375
|
|
|
@@ -424,6 +433,15 @@ const Filters = props => {
|
|
|
424
433
|
{filterStyle === 'pill' && !mobileFilterStyle && <Filters.Pills pills={pillValues} />}
|
|
425
434
|
{filterStyle === 'tab bar' && !mobileFilterStyle && <Filters.TabBar filter={singleFilter} index={outerIndex} />}
|
|
426
435
|
{(filterStyle === 'dropdown' || mobileFilterStyle) && <Filters.Dropdown filter={singleFilter} index={outerIndex} label={label} active={queuedActive || active} filters={values} />}
|
|
436
|
+
{filterStyle === 'multi-select' && (
|
|
437
|
+
<MultiSelect
|
|
438
|
+
options={singleFilter.values.map(v => ({ value: v, label: v }))}
|
|
439
|
+
fieldName={outerIndex}
|
|
440
|
+
updateField={(_section, _subSection, fieldName, value) => changeFilterActive(fieldName, value)}
|
|
441
|
+
selected={singleFilter.active as string[]}
|
|
442
|
+
limit={(singleFilter as MultiSelectFilter).selectLimit || 5}
|
|
443
|
+
/>
|
|
444
|
+
)}
|
|
427
445
|
</>
|
|
428
446
|
</div>
|
|
429
447
|
)
|
|
@@ -453,8 +471,6 @@ Filters.propTypes = {
|
|
|
453
471
|
setConfig: PropTypes.func,
|
|
454
472
|
// exclusions
|
|
455
473
|
excludedData: PropTypes.array,
|
|
456
|
-
// function for filtering the data
|
|
457
|
-
filterData: PropTypes.func,
|
|
458
474
|
dimensions: PropTypes.array
|
|
459
475
|
}
|
|
460
476
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Footnote } from '../../types/Footnotes'
|
|
2
|
+
import './footnotes.css'
|
|
3
|
+
|
|
4
|
+
type FootnotesProps = {
|
|
5
|
+
footnotes: Footnote[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const Footnotes: React.FC<FootnotesProps> = ({ footnotes }) => {
|
|
9
|
+
return (
|
|
10
|
+
<footer className='col-12 m-3 mt-1 mb-0'>
|
|
11
|
+
<ul className='cove-footnotes'>
|
|
12
|
+
{footnotes.map((note, i) => {
|
|
13
|
+
return (
|
|
14
|
+
<li key={note.symbol + i} className='mb-1'>
|
|
15
|
+
{note.symbol && <span className='mr-1'>{note.symbol}</span>}
|
|
16
|
+
{note.text}
|
|
17
|
+
</li>
|
|
18
|
+
)
|
|
19
|
+
})}
|
|
20
|
+
</ul>
|
|
21
|
+
</footer>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default Footnotes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import EditorWrapper from '../EditorWrapper'
|
|
2
|
+
import Footnotes from './Footnotes'
|
|
3
|
+
import FootnotesEditor from '../EditorPanel/FootnotesEditor'
|
|
4
|
+
import { ViewPort } from '../../types/ViewPort'
|
|
5
|
+
import FootnotesConfig, { Footnote } from '../../types/Footnotes'
|
|
6
|
+
import _ from 'lodash'
|
|
7
|
+
import { useMemo } from 'react'
|
|
8
|
+
import { updateFieldFactory } from '../../helpers/updateFieldFactory'
|
|
9
|
+
|
|
10
|
+
type StandAloneProps = {
|
|
11
|
+
isEditor?: boolean
|
|
12
|
+
visualizationKey: string
|
|
13
|
+
config: FootnotesConfig
|
|
14
|
+
updateConfig?: (config: FootnotesConfig) => void
|
|
15
|
+
viewport?: ViewPort
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const FootnotesStandAlone: React.FC<StandAloneProps> = ({ visualizationKey, config, viewport, isEditor, updateConfig }) => {
|
|
19
|
+
const updateField = updateFieldFactory<Footnote[]>(config, updateConfig)
|
|
20
|
+
if (isEditor)
|
|
21
|
+
return (
|
|
22
|
+
<EditorWrapper component={FootnotesStandAlone} visualizationKey={visualizationKey} visualizationConfig={config} updateConfig={updateConfig} type={'Footnotes'} viewport={viewport}>
|
|
23
|
+
<FootnotesEditor key={visualizationKey} config={config} updateField={updateField} />
|
|
24
|
+
</EditorWrapper>
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// get the api footnotes from the config
|
|
28
|
+
const apiFootnotes = useMemo(() => {
|
|
29
|
+
if (config.dataKey && config.dynamicFootnotes) {
|
|
30
|
+
const { symbolColumn, textColumn, orderColumn } = config.dynamicFootnotes
|
|
31
|
+
const configData = config.formattedData || config.data
|
|
32
|
+
const _data = configData.map(row => _.pick(row, [symbolColumn, textColumn, orderColumn]))
|
|
33
|
+
_data.sort((a, b) => a[orderColumn] - b[orderColumn])
|
|
34
|
+
return _data.map(row => ({ symbol: row[symbolColumn], text: row[textColumn] }))
|
|
35
|
+
}
|
|
36
|
+
return []
|
|
37
|
+
}, [config.dynamicFootnotes, config.formattedData, config.data])
|
|
38
|
+
|
|
39
|
+
// get static footnotes from the config.footnotes
|
|
40
|
+
const staticFootnotes = config.staticFootnotes || []
|
|
41
|
+
|
|
42
|
+
return <Footnotes footnotes={[...apiFootnotes, ...staticFootnotes]} />
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default FootnotesStandAlone
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Footnotes'
|