@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
|
@@ -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'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Visualization } from '../../types/Visualization'
|
|
3
|
+
import { ViewPort } from '../../types/ViewPort'
|
|
4
|
+
import './editor-wrapper.style.css'
|
|
5
|
+
|
|
6
|
+
type StandAloneComponentProps = {
|
|
7
|
+
visualizationKey: string
|
|
8
|
+
config
|
|
9
|
+
updateConfig: (Visualization) => void
|
|
10
|
+
configUrl: string
|
|
11
|
+
setEditing: Function
|
|
12
|
+
hostname: string
|
|
13
|
+
viewport?: ViewPort
|
|
14
|
+
|
|
15
|
+
[key: string]: any
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type EditorProps = {
|
|
19
|
+
component: React.JSXElementConstructor<StandAloneComponentProps>
|
|
20
|
+
type: string
|
|
21
|
+
visualizationKey: string
|
|
22
|
+
visualizationConfig: Visualization
|
|
23
|
+
updateConfig: (Visualization) => void
|
|
24
|
+
viewport?: ViewPort
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const EditorWrapper: React.FC<React.PropsWithChildren<EditorProps>> = ({ children, visualizationKey, visualizationConfig, type, component: Component, updateConfig, viewport }) => {
|
|
28
|
+
const [displayPanel, setDisplayPanel] = React.useState(true)
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<div className='editor-wrapper'>
|
|
32
|
+
<button className={`editor-toggle ${displayPanel ? '' : 'collapsed'}`} title={displayPanel ? `Collapse Editor` : `Expand Editor`} onClick={() => setDisplayPanel(!displayPanel)} />
|
|
33
|
+
<section className={`${displayPanel ? '' : 'hidden'} editor-panel cove`}>
|
|
34
|
+
<div aria-level={2} role='heading' className='heading-2'>
|
|
35
|
+
Configure {type}
|
|
36
|
+
</div>
|
|
37
|
+
<section>{children}</section>
|
|
38
|
+
</section>
|
|
39
|
+
<div className='preview-wrapper'>
|
|
40
|
+
<Component visualizationKey={visualizationKey} config={visualizationConfig} updateConfig={updateConfig} configUrl={undefined} setEditing={undefined} hostname={undefined} viewport={viewport} />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default EditorWrapper
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.editor-wrapper {
|
|
2
|
+
--editorPanelWidth: 350px;
|
|
3
|
+
position: relative;
|
|
4
|
+
min-height: 80vh;
|
|
5
|
+
.editor-panel {
|
|
6
|
+
:is(form) {
|
|
7
|
+
border-right: var(--lightGray) 1px solid;
|
|
8
|
+
flex-grow: 1;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
.preview-wrapper {
|
|
12
|
+
padding-left: var(--editorPanelWidth);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './EditorWrapper'
|
|
@@ -1,37 +1,69 @@
|
|
|
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'
|
|
15
|
+
|
|
16
|
+
export const filterStyleOptions = ['dropdown', 'pill', 'tab', 'tab bar', 'multi-select']
|
|
17
|
+
|
|
18
|
+
export const filterOrderOptions = [
|
|
19
|
+
{
|
|
20
|
+
label: 'Ascending Alphanumeric',
|
|
21
|
+
value: 'asc'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: 'Descending Alphanumeric',
|
|
25
|
+
value: 'desc'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: 'Custom',
|
|
29
|
+
value: 'cust'
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
export const handleSorting = singleFilter => {
|
|
34
|
+
const { order } = singleFilter
|
|
35
|
+
|
|
36
|
+
const sortAsc = (a, b) => {
|
|
37
|
+
return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sortDesc = (a, b) => {
|
|
41
|
+
return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!order || order === '') {
|
|
45
|
+
singleFilter.order = 'asc'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (order === 'desc') {
|
|
49
|
+
singleFilter.values = singleFilter.values.sort(sortDesc)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (order === 'asc') {
|
|
53
|
+
singleFilter.values = singleFilter.values.sort(sortAsc)
|
|
54
|
+
}
|
|
55
|
+
return singleFilter
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hasStandardFilterBehavior = ['chart', 'table']
|
|
10
59
|
|
|
11
60
|
export const useFilters = props => {
|
|
12
61
|
const [showApplyButton, setShowApplyButton] = useState(false)
|
|
13
62
|
|
|
14
63
|
// Desconstructing: notice, adding more descriptive visualizationConfig name over config
|
|
15
64
|
// visualizationConfig feels more robust for all vis types so that its not confused with config/state/etc.
|
|
16
|
-
const { config: visualizationConfig, setConfig, filteredData, setFilteredData, excludedData,
|
|
17
|
-
const { type,
|
|
18
|
-
|
|
19
|
-
const filterStyleOptions = ['dropdown', 'pill', 'tab', 'tab bar']
|
|
20
|
-
|
|
21
|
-
const filterOrderOptions = [
|
|
22
|
-
{
|
|
23
|
-
label: 'Ascending Alphanumeric',
|
|
24
|
-
value: 'asc'
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
label: 'Descending Alphanumeric',
|
|
28
|
-
value: 'desc'
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
label: 'Custom',
|
|
32
|
-
value: 'cust'
|
|
33
|
-
}
|
|
34
|
-
]
|
|
65
|
+
const { config: visualizationConfig, setConfig, filteredData, setFilteredData, excludedData, getUniqueValues } = props
|
|
66
|
+
const { type, data } = visualizationConfig
|
|
35
67
|
|
|
36
68
|
/**
|
|
37
69
|
* Re-orders a filter based on two indices and updates the runtime filters array and filters state
|
|
@@ -51,7 +83,7 @@ export const useFilters = props => {
|
|
|
51
83
|
const [movedItem] = updatedValues.splice(idx1, 1)
|
|
52
84
|
updatedValues.splice(idx2, 0, movedItem)
|
|
53
85
|
|
|
54
|
-
const filtersCopy = visualizationConfig.type
|
|
86
|
+
const filtersCopy = hasStandardFilterBehavior.includes(visualizationConfig.type) ? [...visualizationConfig.filters] : [...filteredData]
|
|
55
87
|
const filterItem = { ...filtersCopy[filterIndex] }
|
|
56
88
|
|
|
57
89
|
// Overwrite filterItem.values since thats what we map through in the editor panel
|
|
@@ -99,8 +131,8 @@ export const useFilters = props => {
|
|
|
99
131
|
}
|
|
100
132
|
|
|
101
133
|
// If we're on a chart and not using the apply button
|
|
102
|
-
if (visualizationConfig.type
|
|
103
|
-
setFilteredData(
|
|
134
|
+
if (hasStandardFilterBehavior.includes(visualizationConfig.type) && visualizationConfig.filterBehavior === 'Filter Change') {
|
|
135
|
+
setFilteredData(filterVizData(newFilters, excludedData))
|
|
104
136
|
}
|
|
105
137
|
}
|
|
106
138
|
|
|
@@ -127,8 +159,8 @@ export const useFilters = props => {
|
|
|
127
159
|
setFilteredData(newFilters, excludedData)
|
|
128
160
|
}
|
|
129
161
|
|
|
130
|
-
if (type
|
|
131
|
-
setFilteredData(
|
|
162
|
+
if (hasStandardFilterBehavior.includes(visualizationConfig.type)) {
|
|
163
|
+
setFilteredData(filterVizData(newFilters, excludedData))
|
|
132
164
|
}
|
|
133
165
|
|
|
134
166
|
setShowApplyButton(false)
|
|
@@ -139,19 +171,31 @@ export const useFilters = props => {
|
|
|
139
171
|
e.preventDefault()
|
|
140
172
|
|
|
141
173
|
// reset to first item in values array.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
174
|
+
let needsQueryUpdate = false
|
|
175
|
+
const queryParams = getQueryParams()
|
|
176
|
+
newFilters.forEach((filter, i) => {
|
|
177
|
+
if (!filter.values || filter.values.length === 0) {
|
|
178
|
+
filter.values = getUniqueValues(data, filter.columnName)
|
|
179
|
+
}
|
|
180
|
+
newFilters[i].active = handleSorting(filter).values[0]
|
|
181
|
+
|
|
182
|
+
if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== filter.active) {
|
|
183
|
+
queryParams[filter.setByQueryParameter] = filter.active
|
|
184
|
+
needsQueryUpdate = true
|
|
185
|
+
}
|
|
146
186
|
})
|
|
147
187
|
|
|
188
|
+
if (needsQueryUpdate) {
|
|
189
|
+
updateQueryString(queryParams)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
setConfig({ ...visualizationConfig, filters: newFilters })
|
|
193
|
+
|
|
148
194
|
if (type === 'map') {
|
|
149
195
|
setFilteredData(newFilters, excludedData)
|
|
150
196
|
} else {
|
|
151
|
-
setFilteredData(
|
|
197
|
+
setFilteredData(filterVizData(newFilters, excludedData))
|
|
152
198
|
}
|
|
153
|
-
|
|
154
|
-
setConfig({ ...visualizationConfig, filters: newFilters })
|
|
155
199
|
}
|
|
156
200
|
|
|
157
201
|
const filterConstants = {
|
|
@@ -161,31 +205,6 @@ export const useFilters = props => {
|
|
|
161
205
|
applyText: 'Select the apply button to update the visualization information.'
|
|
162
206
|
}
|
|
163
207
|
|
|
164
|
-
const handleSorting = singleFilter => {
|
|
165
|
-
const { order } = singleFilter
|
|
166
|
-
|
|
167
|
-
const sortAsc = (a, b) => {
|
|
168
|
-
return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const sortDesc = (a, b) => {
|
|
172
|
-
return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (!order || order === '') {
|
|
176
|
-
singleFilter.order = 'asc'
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (order === 'desc') {
|
|
180
|
-
singleFilter.values = singleFilter.values.sort(sortDesc)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (order === 'asc') {
|
|
184
|
-
singleFilter.values = singleFilter.values.sort(sortAsc)
|
|
185
|
-
}
|
|
186
|
-
return singleFilter
|
|
187
|
-
}
|
|
188
|
-
|
|
189
208
|
// prettier-ignore
|
|
190
209
|
return {
|
|
191
210
|
handleApplyButton,
|
|
@@ -201,11 +220,17 @@ export const useFilters = props => {
|
|
|
201
220
|
}
|
|
202
221
|
}
|
|
203
222
|
|
|
204
|
-
|
|
223
|
+
type FilterProps = {
|
|
224
|
+
filteredData
|
|
225
|
+
dimensions
|
|
226
|
+
config: Visualization
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const Filters = (props: FilterProps) => {
|
|
205
230
|
const { config: visualizationConfig, filteredData, dimensions } = props
|
|
206
231
|
const { filters, type, general, theme, filterBehavior } = visualizationConfig
|
|
207
232
|
const [mobileFilterStyle, setMobileFilterStyle] = useState(false)
|
|
208
|
-
const [selectedFilter, setSelectedFilter] = useState(
|
|
233
|
+
const [selectedFilter, setSelectedFilter] = useState<EventTarget>(null)
|
|
209
234
|
const id = useId()
|
|
210
235
|
|
|
211
236
|
// useFilters hook provides data and logic for handling various filter functions
|
|
@@ -231,7 +256,7 @@ const Filters = props => {
|
|
|
231
256
|
|
|
232
257
|
useEffect(() => {
|
|
233
258
|
if (selectedFilter) {
|
|
234
|
-
|
|
259
|
+
const el = document.getElementById(selectedFilter.id)
|
|
235
260
|
if (el) el.focus()
|
|
236
261
|
}
|
|
237
262
|
}, [changeFilterActive, selectedFilter])
|
|
@@ -240,21 +265,21 @@ const Filters = props => {
|
|
|
240
265
|
|
|
241
266
|
const filterSectionClassList = ['filters-section', type === 'map' ? general.headerColor : visualizationConfig?.visualizationType === 'Spark Line' ? null : theme]
|
|
242
267
|
// Exterior Section Wrapper
|
|
243
|
-
Filters.Section =
|
|
268
|
+
Filters.Section = ({ children }) => {
|
|
244
269
|
return (
|
|
245
270
|
visualizationConfig?.filters && (
|
|
246
271
|
<section className={filterSectionClassList.join(' ')}>
|
|
247
272
|
<p className='filters-section__intro-text'>
|
|
248
273
|
{filters?.some(f => f.active) ? filterConstants.introText : ''} {visualizationConfig.filterBehavior === 'Apply Button' && filterConstants.applyText}
|
|
249
274
|
</p>
|
|
250
|
-
<div className='filters-section__wrapper'>{
|
|
275
|
+
<div className='filters-section__wrapper'>{children}</div>
|
|
251
276
|
</section>
|
|
252
277
|
)
|
|
253
278
|
)
|
|
254
279
|
}
|
|
255
280
|
|
|
256
281
|
// Apply/Reset Buttons
|
|
257
|
-
Filters.ApplyBehavior =
|
|
282
|
+
Filters.ApplyBehavior = () => {
|
|
258
283
|
if (filterBehavior !== 'Apply Button') return
|
|
259
284
|
const applyButtonClasses = [general?.headerColor ? general.headerColor : theme, 'apply']
|
|
260
285
|
return (
|
|
@@ -332,7 +357,7 @@ const Filters = props => {
|
|
|
332
357
|
// Remove fromHash if it exists on filters to loop so we can loop nicely
|
|
333
358
|
delete filtersToLoop.fromHash
|
|
334
359
|
|
|
335
|
-
return filtersToLoop.map((singleFilter, outerIndex) => {
|
|
360
|
+
return addValuesToFilters<VizFilter>(filtersToLoop, visualizationConfig.data).map((singleFilter: VizFilter, outerIndex) => {
|
|
336
361
|
if (singleFilter.showDropdown === false) return
|
|
337
362
|
|
|
338
363
|
const values = []
|
|
@@ -340,11 +365,11 @@ const Filters = props => {
|
|
|
340
365
|
const tabValues = []
|
|
341
366
|
const tabBarValues = []
|
|
342
367
|
|
|
343
|
-
const { active, queuedActive, label, filterStyle } = singleFilter
|
|
368
|
+
const { active, queuedActive, label, filterStyle } = singleFilter as VizFilter
|
|
344
369
|
|
|
345
370
|
handleSorting(singleFilter)
|
|
346
371
|
|
|
347
|
-
singleFilter.values
|
|
372
|
+
singleFilter.values?.forEach((filterOption, index) => {
|
|
348
373
|
const pillClassList = ['pill', active === filterOption ? 'pill--active' : null, theme && theme]
|
|
349
374
|
const tabClassList = ['tab', active === filterOption && 'tab--active', theme && theme]
|
|
350
375
|
|
|
@@ -408,6 +433,15 @@ const Filters = props => {
|
|
|
408
433
|
{filterStyle === 'pill' && !mobileFilterStyle && <Filters.Pills pills={pillValues} />}
|
|
409
434
|
{filterStyle === 'tab bar' && !mobileFilterStyle && <Filters.TabBar filter={singleFilter} index={outerIndex} />}
|
|
410
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
|
+
)}
|
|
411
445
|
</>
|
|
412
446
|
</div>
|
|
413
447
|
)
|
|
@@ -437,8 +471,6 @@ Filters.propTypes = {
|
|
|
437
471
|
setConfig: PropTypes.func,
|
|
438
472
|
// exclusions
|
|
439
473
|
excludedData: PropTypes.array,
|
|
440
|
-
// function for filtering the data
|
|
441
|
-
filterData: PropTypes.func,
|
|
442
474
|
dimensions: PropTypes.array
|
|
443
475
|
}
|
|
444
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'
|
|
@@ -601,7 +601,7 @@
|
|
|
601
601
|
/* clicking anywhere will focus the input */
|
|
602
602
|
cursor: text;
|
|
603
603
|
|
|
604
|
-
span {
|
|
604
|
+
span:not(.cove-tooltip, .cove-icon) {
|
|
605
605
|
display: inline;
|
|
606
606
|
}
|
|
607
607
|
}
|
|
@@ -723,6 +723,10 @@
|
|
|
723
723
|
margin-right: 5px;
|
|
724
724
|
}
|
|
725
725
|
|
|
726
|
+
.cove-tooltip {
|
|
727
|
+
position: relative;
|
|
728
|
+
}
|
|
729
|
+
|
|
726
730
|
// tooltips
|
|
727
731
|
.cove-label + .cove-tooltip {
|
|
728
732
|
top: 1px;
|
|
@@ -731,9 +735,9 @@
|
|
|
731
735
|
}
|
|
732
736
|
|
|
733
737
|
.cove-accordion__button .cove-tooltip {
|
|
734
|
-
display: inline-flex;
|
|
735
|
-
right: 1.5rem;
|
|
736
|
-
line-height: inherit;
|
|
738
|
+
// display: inline-flex;
|
|
739
|
+
// right: 1.5rem;
|
|
740
|
+
// line-height: inherit;
|
|
737
741
|
}
|
|
738
742
|
|
|
739
743
|
.cove-list-group__item .cove-tooltip {
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
// main visualization wrapper
|
|
2
2
|
import { ChartConfig } from '@cdc/chart/src/types/ChartConfig'
|
|
3
|
-
import React, { forwardRef
|
|
3
|
+
import React, { forwardRef } from 'react'
|
|
4
4
|
import { Config as DataBiteConfig } from '@cdc/data-bite/src/types/Config'
|
|
5
5
|
import './visualizations.scss'
|
|
6
6
|
import { Config as WaffleChartConfig } from '@cdc/waffle-chart/src/types/Config'
|
|
7
|
+
import { MarkupIncludeConfig } from '@cdc/core/types/MarkupInclude'
|
|
8
|
+
import { DashboardFilters } from '@cdc/dashboard/src/types/DashboardFilters'
|
|
7
9
|
|
|
8
10
|
type VisualizationWrapper = {
|
|
9
|
-
config: ChartConfig | DataBiteConfig | WaffleChartConfig
|
|
10
|
-
isEditor: boolean
|
|
11
|
-
currentViewport: string
|
|
12
|
-
imageId: string
|
|
13
11
|
children: React.ReactNode
|
|
12
|
+
config: ChartConfig | DataBiteConfig | WaffleChartConfig | MarkupIncludeConfig | DashboardFilters
|
|
13
|
+
currentViewport?: string
|
|
14
|
+
imageId?: string
|
|
15
|
+
isEditor: boolean
|
|
16
|
+
showEditorPanel?: boolean
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
const Visualization: React.FC<VisualizationWrapper> = forwardRef((props, ref) => {
|
|
@@ -31,6 +34,12 @@ const Visualization: React.FC<VisualizationWrapper> = forwardRef((props, ref) =>
|
|
|
31
34
|
classes.push('editor-panel--hidden')
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
if (config.type === 'filtered-text') {
|
|
38
|
+
classes.push('type-filtered-text')
|
|
39
|
+
classes = classes.filter(item => item !== 'cove-component__content')
|
|
40
|
+
return classes
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
if (config.type === 'chart') {
|
|
35
44
|
classes.push('type-chart')
|
|
36
45
|
config?.visualizationType === 'Spark Line' && classes.push(`type-sparkline`)
|
|
@@ -59,7 +59,7 @@ const generateMedia = (state, type, elementToCapture) => {
|
|
|
59
59
|
|
|
60
60
|
switch (type) {
|
|
61
61
|
case 'image':
|
|
62
|
-
html2canvas(baseSvg).then(canvas => {
|
|
62
|
+
html2canvas(baseSvg, {ignoreElements: el => el.className?.indexOf && el.className.search(/download-buttons|download-links|data-table-container/) !== -1}).then(canvas => {
|
|
63
63
|
saveImageAs(canvas.toDataURL(), filename + '.png')
|
|
64
64
|
})
|
|
65
65
|
return
|
|
@@ -21,8 +21,8 @@ interface MultiSelectProps {
|
|
|
21
21
|
limit?: number
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField, selected, limit }) => {
|
|
25
|
-
const preselectedItems = options.filter(opt => selected
|
|
24
|
+
const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection = null, fieldName, label, options, updateField, selected = [], limit }) => {
|
|
25
|
+
const preselectedItems = options.filter(opt => selected.includes(opt.value)).slice(0, limit)
|
|
26
26
|
const [selectedItems, setSelectedItems] = useState<Option[]>(preselectedItems)
|
|
27
27
|
const [expanded, setExpanded] = useState(false)
|
|
28
28
|
const multiSelectRef = useRef(null)
|
|
@@ -68,22 +68,39 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection =
|
|
|
68
68
|
return (
|
|
69
69
|
<div ref={multiSelectRef} className='cove-multiselect'>
|
|
70
70
|
{label && (
|
|
71
|
-
<
|
|
71
|
+
<span id={multiID} className='edit-label column-heading'>
|
|
72
72
|
{label}
|
|
73
|
-
</
|
|
73
|
+
</span>
|
|
74
74
|
)}
|
|
75
75
|
|
|
76
76
|
<div className='wrapper'>
|
|
77
77
|
<div className='selected'>
|
|
78
78
|
{selectedItems.map(item => (
|
|
79
|
-
<div key={item.value} aria-labelledby={label ? multiID : undefined}
|
|
79
|
+
<div key={item.value} aria-labelledby={label ? multiID : undefined}>
|
|
80
80
|
{item.label}
|
|
81
|
-
<button
|
|
81
|
+
<button
|
|
82
|
+
aria-label='Remove'
|
|
83
|
+
onClick={e => {
|
|
84
|
+
e.preventDefault()
|
|
85
|
+
handleItemRemove(item)
|
|
86
|
+
}}
|
|
87
|
+
onKeyUp={e => {
|
|
88
|
+
handleItemRemove(item, e)
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
82
91
|
x
|
|
83
92
|
</button>
|
|
84
93
|
</div>
|
|
85
94
|
))}
|
|
86
|
-
<button
|
|
95
|
+
<button
|
|
96
|
+
aria-label={expanded ? 'Collapse' : 'Expand'}
|
|
97
|
+
aria-labelledby={label ? multiID : undefined}
|
|
98
|
+
className='expand'
|
|
99
|
+
onClick={e => {
|
|
100
|
+
e.preventDefault()
|
|
101
|
+
setExpanded(!expanded)
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
87
104
|
<Icon display={expanded ? 'caretDown' : 'caretUp'} style={{ cursor: 'pointer' }} />
|
|
88
105
|
</button>
|
|
89
106
|
</div>
|
|
@@ -98,11 +115,21 @@ const MultiSelect: React.FC<MultiSelectProps> = ({ section = null, subsection =
|
|
|
98
115
|
</Tooltip>
|
|
99
116
|
)}
|
|
100
117
|
</div>
|
|
101
|
-
<ul className={'dropdown' + (expanded ? '' : '
|
|
118
|
+
<ul className={'dropdown' + (expanded ? '' : ' d-none')}>
|
|
102
119
|
{options
|
|
103
120
|
.filter(option => !selectedItems.find(item => item.value === option.value))
|
|
104
121
|
.map(option => (
|
|
105
|
-
<li
|
|
122
|
+
<li
|
|
123
|
+
className='cove-multiselect-li'
|
|
124
|
+
key={option.value}
|
|
125
|
+
role='option'
|
|
126
|
+
tabIndex={0}
|
|
127
|
+
onClick={e => {
|
|
128
|
+
e.preventDefault()
|
|
129
|
+
handleItemSelect(option, e)
|
|
130
|
+
}}
|
|
131
|
+
onKeyUp={e => handleItemSelect(option, e)}
|
|
132
|
+
>
|
|
106
133
|
{option.label}
|
|
107
134
|
</li>
|
|
108
135
|
))}
|