@cdc/core 4.24.5 → 4.24.9
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/assets/icon-gear-multi.svg +23 -0
- 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/Alert/components/Alert.styles.css +15 -0
- package/components/Alert/components/Alert.tsx +39 -0
- package/components/Alert/index.tsx +3 -0
- package/components/DataTable/DataTable.tsx +127 -32
- 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 +6 -12
- package/components/DataTable/helpers/getChartCellValue.ts +9 -5
- package/components/DataTable/helpers/getDataSeriesColumns.ts +10 -7
- package/components/DataTable/helpers/getRowType.ts +6 -0
- package/components/DataTable/helpers/mapCellMatrix.tsx +3 -3
- package/components/DataTable/types/TableConfig.ts +2 -1
- 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/Inputs.tsx +13 -4
- package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +268 -0
- package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +306 -0
- package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +40 -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.tsx +520 -0
- 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/Responsive.tsx +14 -4
- package/components/Layout/components/Sidebar/components/Sidebar.tsx +14 -5
- package/components/Layout/components/Sidebar/components/sidebar.styles.scss +23 -20
- package/components/Layout/components/Visualization/index.tsx +19 -6
- package/components/Layout/components/Visualization/visualizations.scss +32 -26
- package/components/Layout/styles/editor.scss +0 -8
- package/components/Legend/Legend.Gradient.tsx +133 -0
- package/components/LegendShape.tsx +28 -0
- package/components/MultiSelect/MultiSelect.tsx +41 -11
- package/components/MultiSelect/multiselect.styles.css +0 -3
- package/components/NestedDropdown/NestedDropdown.tsx +47 -52
- package/components/NestedDropdown/nesteddropdown.styles.css +19 -25
- package/components/Table/Table.tsx +8 -5
- package/components/Table/components/Cell.tsx +2 -2
- package/components/Table/components/Row.tsx +25 -7
- package/components/_stories/Footnotes.stories.tsx +17 -0
- package/components/_stories/Layout.Debug.stories.tsx +91 -0
- package/components/_stories/_mocks/bar-chart-suppressed.json +474 -0
- package/components/_stories/styles.scss +14 -1
- package/components/createBarElement.jsx +4 -4
- package/components/inputs/InputSelect.tsx +17 -6
- package/components/ui/Icon.tsx +22 -16
- package/components/ui/Title/Title.scss +0 -8
- package/helpers/DataTransform.ts +2 -2
- package/helpers/addValuesToFilters.ts +135 -0
- package/helpers/cove/accessibility.ts +17 -4
- package/helpers/cove/fontSettings.ts +2 -0
- package/helpers/coveUpdateWorker.ts +30 -9
- package/helpers/filterVizData.ts +49 -0
- package/helpers/formatConfigBeforeSave.ts +95 -0
- package/helpers/gatherQueryParams.ts +14 -7
- package/helpers/getGradientLegendWidth.ts +15 -0
- package/helpers/getTextWidth.ts +18 -0
- package/helpers/lineChartHelpers.js +2 -1
- package/helpers/pivotData.ts +18 -0
- package/helpers/queryStringUtils.ts +29 -0
- package/helpers/scaling.ts +7 -0
- package/helpers/tests/addValuesToFilters.test.ts +55 -0
- package/helpers/tests/filterVizData.test.ts +31 -0
- package/helpers/tests/invertValue.test.ts +35 -0
- package/helpers/tests/updateFieldFactory.test.ts +1 -0
- package/helpers/updateFieldFactory.ts +1 -1
- package/helpers/updatePaletteNames.ts +19 -0
- package/helpers/{useDataVizClasses.js → useDataVizClasses.ts} +3 -2
- package/helpers/ver/4.24.5.ts +3 -3
- package/helpers/ver/4.24.7.ts +123 -0
- package/helpers/ver/4.24.9.ts +63 -0
- package/helpers/ver/tests/4.24.9.test.ts +22 -0
- package/helpers/ver/versionNeedsUpdate.ts +9 -0
- package/package.json +6 -4
- package/styles/_button-section.scss +7 -2
- package/styles/_data-table.scss +0 -1
- package/styles/_global.scss +6 -2
- package/styles/base.scss +4 -0
- package/styles/filters.scss +4 -0
- package/styles/v2/themes/_color-definitions.scss +1 -0
- package/types/Annotation.ts +46 -0
- package/types/Axis.ts +3 -2
- package/types/ConfigureData.ts +1 -1
- package/types/Dimensions.ts +1 -0
- 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 +39 -7
- package/LICENSE +0 -201
- package/components/AdvancedEditor.jsx +0 -74
- package/components/EditorPanel/VizFilterEditor.tsx +0 -234
- package/components/Filters.jsx +0 -461
- package/components/LegendCircle.jsx +0 -17
- package/helpers/queryStringUtils.js +0 -26
- package/helpers/updatePaletteNames.js +0 -16
- package/types/BaseVisualizationType.ts +0 -1
- /package/components/{Waiting.jsx → Waiting.tsx} +0 -0
- /package/helpers/ver/{4.23.4.ts → 4.24.4.ts} +0 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
2
|
+
import { useId } from 'react'
|
|
3
|
+
|
|
4
|
+
// CDC
|
|
5
|
+
import Button from './elements/Button'
|
|
6
|
+
import { getQueryParams, updateQueryString } from '../helpers/queryStringUtils'
|
|
7
|
+
import MultiSelect from './MultiSelect'
|
|
8
|
+
import { Visualization } from '../types/Visualization'
|
|
9
|
+
import { MultiSelectFilter, OrderBy, VizFilter } from '../types/VizFilter'
|
|
10
|
+
import { filterVizData } from '../helpers/filterVizData'
|
|
11
|
+
import { addValuesToFilters } from '../helpers/addValuesToFilters'
|
|
12
|
+
import { DimensionsType } from '../types/Dimensions'
|
|
13
|
+
import NestedDropdown from './NestedDropdown'
|
|
14
|
+
import _ from 'lodash'
|
|
15
|
+
|
|
16
|
+
export const filterStyleOptions = ['dropdown', 'nested-dropdown', 'pill', 'tab', 'tab bar', 'multi-select']
|
|
17
|
+
|
|
18
|
+
export const filterOrderOptions: { label: string; value: OrderBy }[] = [
|
|
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 singleFilterValues = _.cloneDeep(singleFilter.values)
|
|
35
|
+
if (singleFilter.order === 'cust' && singleFilter.filterStyle !== 'nested-dropdown') {
|
|
36
|
+
singleFilter.values = singleFilter.orderedValues?.length ? singleFilter.orderedValues : singleFilterValues
|
|
37
|
+
return singleFilter
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sort = (a, b) => {
|
|
41
|
+
const asc = singleFilter.order !== 'desc'
|
|
42
|
+
return (asc ? a : b).toString().localeCompare((asc ? b : a).toString(), 'en', { numeric: true })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
singleFilter.values = singleFilterValues.sort(sort)
|
|
46
|
+
|
|
47
|
+
return singleFilter
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const hasStandardFilterBehavior = ['chart', 'table']
|
|
51
|
+
|
|
52
|
+
export const useFilters = props => {
|
|
53
|
+
const [showApplyButton, setShowApplyButton] = useState(false)
|
|
54
|
+
|
|
55
|
+
// Desconstructing: notice, adding more descriptive visualizationConfig name over config
|
|
56
|
+
// visualizationConfig feels more robust for all vis types so that its not confused with config/state/etc.
|
|
57
|
+
const { config: visualizationConfig, setConfig, filteredData, setFilteredData, excludedData, getUniqueValues } = props
|
|
58
|
+
const { type, data } = visualizationConfig
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Re-orders a filter based on two indices and updates the runtime filters array and filters state
|
|
62
|
+
* @param {number} idx1 - The index of the original position of the filter value.
|
|
63
|
+
* @param {number} idx2 - The index of the new position for the filter value.
|
|
64
|
+
* @param {number} filterIndex - The index of the filter item within the array of filter items.
|
|
65
|
+
* @param {object} filter - The filter item itself, which contains an array of filter values.
|
|
66
|
+
* @return {void} None. This function only updates the state of the component.
|
|
67
|
+
*
|
|
68
|
+
* @modifies {object} - The filter object passed in as a parameter
|
|
69
|
+
* @modifies {array} - The filteredData state if visualizationConfig.type equals 'map'
|
|
70
|
+
* @modifies {object} - The visualizationConfig state
|
|
71
|
+
*/
|
|
72
|
+
const handleFilterOrder = (idx1, idx2, filterIndex, filter) => {
|
|
73
|
+
// Create a shallow copy of the filter values array & update position of the values
|
|
74
|
+
const updatedValues = [...filter.values]
|
|
75
|
+
const [movedItem] = updatedValues.splice(idx1, 1)
|
|
76
|
+
updatedValues.splice(idx2, 0, movedItem)
|
|
77
|
+
|
|
78
|
+
const filtersCopy = hasStandardFilterBehavior.includes(visualizationConfig.type)
|
|
79
|
+
? [...visualizationConfig.filters]
|
|
80
|
+
: [...filteredData]
|
|
81
|
+
const filterItem = { ...filtersCopy[filterIndex] }
|
|
82
|
+
|
|
83
|
+
// Overwrite filterItem.values since thats what we map through in the editor panel
|
|
84
|
+
filterItem.values = updatedValues
|
|
85
|
+
filterItem.orderedValues = updatedValues
|
|
86
|
+
filterItem.active = updatedValues[0]
|
|
87
|
+
filterItem.order = 'cust'
|
|
88
|
+
|
|
89
|
+
// Update the filters
|
|
90
|
+
filtersCopy[filterIndex] = filterItem
|
|
91
|
+
|
|
92
|
+
if (visualizationConfig.type === 'map') {
|
|
93
|
+
setFilteredData(filtersCopy)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setConfig({ ...visualizationConfig, filters: filtersCopy })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const changeFilterActive = (index, value) => {
|
|
100
|
+
let newFilters = visualizationConfig.type === 'map' ? [...filteredData] : [...visualizationConfig.filters]
|
|
101
|
+
|
|
102
|
+
if (visualizationConfig.filterBehavior === 'Apply Button') {
|
|
103
|
+
newFilters[index].queuedActive = value
|
|
104
|
+
setShowApplyButton(true)
|
|
105
|
+
} else {
|
|
106
|
+
const newFilter = newFilters[index]
|
|
107
|
+
if (newFilter.filterStyle !== 'nested-dropdown') {
|
|
108
|
+
newFilter.active = value
|
|
109
|
+
} else {
|
|
110
|
+
newFilter.active = value[0]
|
|
111
|
+
newFilter.subGrouping.active = value[1]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const queryParams = getQueryParams()
|
|
115
|
+
if (newFilter.setByQueryParameter && queryParams[newFilter.setByQueryParameter] !== newFilter.active) {
|
|
116
|
+
queryParams[newFilter.setByQueryParameter] = newFilter.active
|
|
117
|
+
updateQueryString(queryParams)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!visualizationConfig.dynamicSeries) {
|
|
122
|
+
newFilters = addValuesToFilters(newFilters, excludedData)
|
|
123
|
+
setConfig({
|
|
124
|
+
...visualizationConfig,
|
|
125
|
+
filters: newFilters
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Used for setting active filter, fromHash breaks the filteredData functionality.
|
|
130
|
+
if (visualizationConfig.type === 'map' && visualizationConfig.filterBehavior === 'Filter Change') {
|
|
131
|
+
setFilteredData(newFilters)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// If we're on a chart and not using the apply button
|
|
135
|
+
if (
|
|
136
|
+
hasStandardFilterBehavior.includes(visualizationConfig.type) &&
|
|
137
|
+
visualizationConfig.filterBehavior === 'Filter Change'
|
|
138
|
+
) {
|
|
139
|
+
const newFilteredData = filterVizData(newFilters, excludedData)
|
|
140
|
+
setFilteredData(newFilteredData)
|
|
141
|
+
|
|
142
|
+
if (visualizationConfig.dynamicSeries) {
|
|
143
|
+
const runtime = visualizationConfig.runtime || {}
|
|
144
|
+
runtime.series = []
|
|
145
|
+
runtime.seriesLabels = {}
|
|
146
|
+
runtime.seriesLabelsAll = []
|
|
147
|
+
|
|
148
|
+
if (newFilteredData && newFilteredData.length && newFilteredData.length > 0) {
|
|
149
|
+
Object.keys(newFilteredData[0]).forEach(seriesKey => {
|
|
150
|
+
if (
|
|
151
|
+
seriesKey !== visualizationConfig.xAxis.dataKey &&
|
|
152
|
+
newFilteredData[0][seriesKey] &&
|
|
153
|
+
(!visualizationConfig.filters ||
|
|
154
|
+
visualizationConfig.filters?.filter(filter => filter.columnName === seriesKey).length === 0) &&
|
|
155
|
+
(!visualizationConfig.columns || Object.keys(visualizationConfig.columns).indexOf(seriesKey) === -1)
|
|
156
|
+
) {
|
|
157
|
+
runtime.series.push({
|
|
158
|
+
dataKey: seriesKey,
|
|
159
|
+
type: visualizationConfig.dynamicSeriesType,
|
|
160
|
+
lineType: visualizationConfig.dynamicSeriesLineType,
|
|
161
|
+
tooltip: true
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
runtime.seriesKeys = runtime.series
|
|
168
|
+
? runtime.series.map(series => {
|
|
169
|
+
runtime.seriesLabels[series.dataKey] = series.name || series.label || series.dataKey
|
|
170
|
+
runtime.seriesLabelsAll.push(series.name || series.dataKey)
|
|
171
|
+
return series.dataKey
|
|
172
|
+
})
|
|
173
|
+
: []
|
|
174
|
+
|
|
175
|
+
setConfig({
|
|
176
|
+
...visualizationConfig,
|
|
177
|
+
filters: newFilters,
|
|
178
|
+
runtime
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const handleApplyButton = newFilters => {
|
|
185
|
+
let needsQueryUpdate = false
|
|
186
|
+
const queryParams = getQueryParams()
|
|
187
|
+
newFilters.forEach(newFilter => {
|
|
188
|
+
if (newFilter.queuedActive) {
|
|
189
|
+
newFilter.active = newFilter.queuedActive
|
|
190
|
+
delete newFilter.queuedActive
|
|
191
|
+
if (newFilter.setByQueryParameter && queryParams[newFilter.setByQueryParameter] !== newFilter.active) {
|
|
192
|
+
queryParams[newFilter.setByQueryParameter] = newFilter.active
|
|
193
|
+
needsQueryUpdate = true
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
if (needsQueryUpdate) {
|
|
198
|
+
updateQueryString(queryParams)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
setConfig({ ...visualizationConfig, filters: newFilters })
|
|
202
|
+
|
|
203
|
+
if (type === 'map') {
|
|
204
|
+
setFilteredData(newFilters, excludedData)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (hasStandardFilterBehavior.includes(visualizationConfig.type)) {
|
|
208
|
+
setFilteredData(filterVizData(newFilters, excludedData))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
setShowApplyButton(false)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const handleReset = e => {
|
|
215
|
+
let newFilters = [...visualizationConfig.filters]
|
|
216
|
+
e.preventDefault()
|
|
217
|
+
|
|
218
|
+
// reset to first item in values array.
|
|
219
|
+
let needsQueryUpdate = false
|
|
220
|
+
const queryParams = getQueryParams()
|
|
221
|
+
newFilters.forEach((filter, i) => {
|
|
222
|
+
if (!filter.values || filter.values.length === 0) {
|
|
223
|
+
filter.values = getUniqueValues(data, filter.columnName)
|
|
224
|
+
}
|
|
225
|
+
newFilters[i].active = handleSorting(filter).values[0]
|
|
226
|
+
|
|
227
|
+
if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== filter.active) {
|
|
228
|
+
queryParams[filter.setByQueryParameter] = filter.active
|
|
229
|
+
needsQueryUpdate = true
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
if (needsQueryUpdate) {
|
|
234
|
+
updateQueryString(queryParams)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
setConfig({ ...visualizationConfig, filters: newFilters })
|
|
238
|
+
|
|
239
|
+
if (type === 'map') {
|
|
240
|
+
setFilteredData(newFilters, excludedData)
|
|
241
|
+
} else {
|
|
242
|
+
setFilteredData(filterVizData(newFilters, excludedData))
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const filterConstants = {
|
|
247
|
+
buttonText: 'Apply Filters',
|
|
248
|
+
resetText: 'Reset All',
|
|
249
|
+
introText: `Make a selection from the filters to change the visualization information.`,
|
|
250
|
+
applyText: 'Select the apply button to update the visualization information.'
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// prettier-ignore
|
|
254
|
+
return {
|
|
255
|
+
handleApplyButton,
|
|
256
|
+
changeFilterActive,
|
|
257
|
+
showApplyButton,
|
|
258
|
+
handleReset,
|
|
259
|
+
filterConstants,
|
|
260
|
+
filterStyleOptions,
|
|
261
|
+
filterOrderOptions,
|
|
262
|
+
handleFilterOrder,
|
|
263
|
+
handleSorting
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
type FilterProps = {
|
|
268
|
+
filteredData: Object[]
|
|
269
|
+
dimensions: DimensionsType
|
|
270
|
+
config: Visualization
|
|
271
|
+
// function for updating the runtime filters
|
|
272
|
+
setFilteredData: Function
|
|
273
|
+
// updating function for setting fitlerBehavior
|
|
274
|
+
setConfig: Function
|
|
275
|
+
// exclusions
|
|
276
|
+
exclusions: any[]
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const Filters = (props: FilterProps) => {
|
|
280
|
+
const { config: visualizationConfig, filteredData, dimensions } = props
|
|
281
|
+
const { filters, type, general, theme, filterBehavior } = visualizationConfig
|
|
282
|
+
const [mobileFilterStyle, setMobileFilterStyle] = useState(false)
|
|
283
|
+
const [selectedFilter, setSelectedFilter] = useState<EventTarget>(null)
|
|
284
|
+
const id = useId()
|
|
285
|
+
|
|
286
|
+
// useFilters hook provides data and logic for handling various filter functions
|
|
287
|
+
// prettier-ignore
|
|
288
|
+
const {
|
|
289
|
+
handleApplyButton,
|
|
290
|
+
changeFilterActive,
|
|
291
|
+
showApplyButton,
|
|
292
|
+
handleReset,
|
|
293
|
+
filterConstants,
|
|
294
|
+
handleSorting
|
|
295
|
+
} = useFilters(props)
|
|
296
|
+
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
if (!dimensions) return
|
|
299
|
+
if (Number(dimensions[0]) < 768 && filters?.length > 0) {
|
|
300
|
+
setMobileFilterStyle(true)
|
|
301
|
+
} else {
|
|
302
|
+
setMobileFilterStyle(false)
|
|
303
|
+
}
|
|
304
|
+
}, [dimensions])
|
|
305
|
+
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (selectedFilter) {
|
|
308
|
+
const el = document.getElementById(selectedFilter.id)
|
|
309
|
+
if (el) el.focus()
|
|
310
|
+
}
|
|
311
|
+
}, [changeFilterActive, selectedFilter])
|
|
312
|
+
|
|
313
|
+
const TabBar = props => {
|
|
314
|
+
const { filter: singleFilter, index: outerIndex } = props
|
|
315
|
+
return (
|
|
316
|
+
<section className='single-filters__tab-bar'>
|
|
317
|
+
{singleFilter.values.map((filter, index) => {
|
|
318
|
+
const buttonClassList = ['button__tab-bar', singleFilter.active === filter ? 'button__tab-bar--active' : '']
|
|
319
|
+
return (
|
|
320
|
+
<button
|
|
321
|
+
id={`${filter}-${outerIndex}-${index}-${id}`}
|
|
322
|
+
className={buttonClassList.join(' ')}
|
|
323
|
+
key={filter}
|
|
324
|
+
onClick={e => {
|
|
325
|
+
changeFilterActive(outerIndex, filter)
|
|
326
|
+
setSelectedFilter(e.target)
|
|
327
|
+
}}
|
|
328
|
+
onKeyDown={e => {
|
|
329
|
+
if (e.keyCode === 13) {
|
|
330
|
+
changeFilterActive(outerIndex, filter)
|
|
331
|
+
setSelectedFilter(e.target)
|
|
332
|
+
}
|
|
333
|
+
}}
|
|
334
|
+
>
|
|
335
|
+
{filter}
|
|
336
|
+
</button>
|
|
337
|
+
)
|
|
338
|
+
})}
|
|
339
|
+
</section>
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const Dropdown = props => {
|
|
344
|
+
const { index: outerIndex, label, active, filters } = props
|
|
345
|
+
return (
|
|
346
|
+
<select
|
|
347
|
+
id={`filter-${outerIndex}`}
|
|
348
|
+
name={label}
|
|
349
|
+
aria-label={`Filter by ${label}`}
|
|
350
|
+
className='filter-select'
|
|
351
|
+
data-index='0'
|
|
352
|
+
value={active}
|
|
353
|
+
onChange={e => {
|
|
354
|
+
changeFilterActive(outerIndex, e.target.value)
|
|
355
|
+
}}
|
|
356
|
+
>
|
|
357
|
+
{filters}
|
|
358
|
+
</select>
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const vizFiltersWithValues = useMemo(() => {
|
|
363
|
+
// Here charts is using config.filters where maps is using a runtime value
|
|
364
|
+
let vizfilters = type === 'map' ? filteredData : filters
|
|
365
|
+
if (!vizfilters) return []
|
|
366
|
+
if (vizfilters.fromHash) delete vizfilters.fromHash // support for Maps config
|
|
367
|
+
return addValuesToFilters(vizfilters as VizFilter[], visualizationConfig.data)
|
|
368
|
+
}, [filters, filteredData])
|
|
369
|
+
|
|
370
|
+
// Resolve Filter Styles
|
|
371
|
+
const Style = () => {
|
|
372
|
+
return vizFiltersWithValues.map((singleFilter: VizFilter, outerIndex) => {
|
|
373
|
+
if (singleFilter.showDropdown === false) return
|
|
374
|
+
|
|
375
|
+
const DropdownOptions = []
|
|
376
|
+
const Pills = []
|
|
377
|
+
const Tabs = []
|
|
378
|
+
|
|
379
|
+
const { active, queuedActive, label, filterStyle } = singleFilter as VizFilter
|
|
380
|
+
|
|
381
|
+
handleSorting(singleFilter)
|
|
382
|
+
|
|
383
|
+
singleFilter.values?.forEach((filterOption, index) => {
|
|
384
|
+
const pillClassList = ['pill', active === filterOption ? 'pill--active' : null, theme && theme]
|
|
385
|
+
const tabClassList = ['tab', active === filterOption && 'tab--active', theme && theme]
|
|
386
|
+
|
|
387
|
+
Pills.push(
|
|
388
|
+
<div className='pill__wrapper' key={`pill-${index}`}>
|
|
389
|
+
<button
|
|
390
|
+
id={`${filterOption}-${outerIndex}-${index}-${id}`}
|
|
391
|
+
className={pillClassList.join(' ')}
|
|
392
|
+
onKeyDown={e => {
|
|
393
|
+
if (e.keyCode === 13) {
|
|
394
|
+
changeFilterActive(outerIndex, filterOption)
|
|
395
|
+
setSelectedFilter(e.target)
|
|
396
|
+
}
|
|
397
|
+
}}
|
|
398
|
+
onClick={e => {
|
|
399
|
+
changeFilterActive(outerIndex, filterOption)
|
|
400
|
+
setSelectedFilter(e.target)
|
|
401
|
+
}}
|
|
402
|
+
name={label}
|
|
403
|
+
>
|
|
404
|
+
{filterOption}
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
DropdownOptions.push(
|
|
410
|
+
<option key={index} value={filterOption} aria-label={filterOption}>
|
|
411
|
+
{singleFilter.labels && singleFilter.labels[filterOption]
|
|
412
|
+
? singleFilter.labels[filterOption]
|
|
413
|
+
: filterOption}
|
|
414
|
+
</option>
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
Tabs.push(
|
|
418
|
+
<button
|
|
419
|
+
id={`${filterOption}-${outerIndex}-${index}-${id}`}
|
|
420
|
+
className={tabClassList.join(' ')}
|
|
421
|
+
onClick={e => {
|
|
422
|
+
changeFilterActive(outerIndex, filterOption)
|
|
423
|
+
setSelectedFilter(e.target)
|
|
424
|
+
}}
|
|
425
|
+
onKeyDown={e => {
|
|
426
|
+
if (e.keyCode === 13) {
|
|
427
|
+
changeFilterActive(outerIndex, filterOption)
|
|
428
|
+
setSelectedFilter(e.target)
|
|
429
|
+
}
|
|
430
|
+
}}
|
|
431
|
+
>
|
|
432
|
+
{filterOption}
|
|
433
|
+
</button>
|
|
434
|
+
)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
const classList = [
|
|
438
|
+
'single-filters',
|
|
439
|
+
mobileFilterStyle ? 'single-filters--dropdown' : `single-filters--${filterStyle}`
|
|
440
|
+
]
|
|
441
|
+
const mobileExempt = ['nested-dropdown', 'multi-select'].includes(filterStyle)
|
|
442
|
+
const showDefaultDropdown = (filterStyle === 'dropdown' || mobileFilterStyle) && !mobileExempt
|
|
443
|
+
return (
|
|
444
|
+
<div className={classList.join(' ')} key={outerIndex}>
|
|
445
|
+
<>
|
|
446
|
+
{label && <label htmlFor={`filter-${outerIndex}`}>{label}</label>}
|
|
447
|
+
{filterStyle === 'tab' && !mobileFilterStyle && Tabs}
|
|
448
|
+
{filterStyle === 'pill' && !mobileFilterStyle && Pills}
|
|
449
|
+
{filterStyle === 'tab bar' && !mobileFilterStyle && <TabBar filter={singleFilter} index={outerIndex} />}
|
|
450
|
+
{filterStyle === 'multi-select' && (
|
|
451
|
+
<MultiSelect
|
|
452
|
+
options={singleFilter.values.map(v => ({ value: v, label: v }))}
|
|
453
|
+
fieldName={outerIndex}
|
|
454
|
+
updateField={(_section, _subSection, fieldName, value) => changeFilterActive(fieldName, value)}
|
|
455
|
+
selected={singleFilter.active as string[]}
|
|
456
|
+
limit={(singleFilter as MultiSelectFilter).selectLimit || 5}
|
|
457
|
+
/>
|
|
458
|
+
)}
|
|
459
|
+
{filterStyle === 'nested-dropdown' && (
|
|
460
|
+
<NestedDropdown
|
|
461
|
+
currentFilter={singleFilter}
|
|
462
|
+
listLabel={label}
|
|
463
|
+
handleSelectedItems={value => changeFilterActive(outerIndex, value)}
|
|
464
|
+
/>
|
|
465
|
+
)}
|
|
466
|
+
{showDefaultDropdown && (
|
|
467
|
+
<Dropdown
|
|
468
|
+
filter={singleFilter}
|
|
469
|
+
index={outerIndex}
|
|
470
|
+
label={label}
|
|
471
|
+
active={queuedActive || active}
|
|
472
|
+
filters={DropdownOptions}
|
|
473
|
+
/>
|
|
474
|
+
)}
|
|
475
|
+
</>
|
|
476
|
+
</div>
|
|
477
|
+
)
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (visualizationConfig?.filters?.length === 0) return
|
|
482
|
+
const filterSectionClassList = [
|
|
483
|
+
`filters-section legend_${visualizationConfig?.legend?.hide ? 'hidden' : 'visible'}_${
|
|
484
|
+
visualizationConfig?.legend?.position || ''
|
|
485
|
+
}`,
|
|
486
|
+
type === 'map' ? general.headerColor : visualizationConfig?.visualizationType === 'Spark Line' ? null : theme
|
|
487
|
+
]
|
|
488
|
+
return (
|
|
489
|
+
<section className={filterSectionClassList.join(' ')}>
|
|
490
|
+
<p className='filters-section__intro-text'>
|
|
491
|
+
{filters?.some(f => f.active && f.showDropdown) ? filterConstants.introText : ''}{' '}
|
|
492
|
+
{visualizationConfig.filterBehavior === 'Apply Button' && filterConstants.applyText}
|
|
493
|
+
</p>
|
|
494
|
+
<div className='filters-section__wrapper'>
|
|
495
|
+
{' '}
|
|
496
|
+
<>
|
|
497
|
+
<Style />
|
|
498
|
+
{filterBehavior === 'Apply Button' ? (
|
|
499
|
+
<div className='filters-section__buttons'>
|
|
500
|
+
<Button
|
|
501
|
+
onClick={() => handleApplyButton(filters)}
|
|
502
|
+
disabled={!showApplyButton}
|
|
503
|
+
className={[general?.headerColor ? general.headerColor : theme, 'apply'].join(' ')}
|
|
504
|
+
>
|
|
505
|
+
{filterConstants.buttonText}
|
|
506
|
+
</Button>
|
|
507
|
+
<a href='#!' role='button' onClick={handleReset}>
|
|
508
|
+
{filterConstants.resetText}
|
|
509
|
+
</a>
|
|
510
|
+
</div>
|
|
511
|
+
) : (
|
|
512
|
+
<></>
|
|
513
|
+
)}
|
|
514
|
+
</>
|
|
515
|
+
</div>
|
|
516
|
+
</section>
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export default Filters
|
|
@@ -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'
|
|
@@ -13,7 +13,8 @@ const breakpoints = [
|
|
|
13
13
|
'1280' // xl
|
|
14
14
|
]
|
|
15
15
|
|
|
16
|
-
const os =
|
|
16
|
+
const os =
|
|
17
|
+
navigator.userAgent.indexOf('Win') !== -1 ? 'Win' : navigator.userAgent.indexOf('Mac') !== -1 ? 'MacOS' : null
|
|
17
18
|
|
|
18
19
|
const Responsive = ({ children, isEditor }) => {
|
|
19
20
|
const [displayPanel, setDisplayPanel] = useState(false)
|
|
@@ -35,6 +36,7 @@ const Responsive = ({ children, isEditor }) => {
|
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
const onKeypress = key => {
|
|
39
|
+
if (!isEditor) return key
|
|
38
40
|
if (key.code === 'KeyL' && key.ctrlKey) setDisplayPanel(display => !display)
|
|
39
41
|
const viewportCommandKey = os === 'MacOS' ? key.metaKey : key.altKey
|
|
40
42
|
if (viewportCommandKey) {
|
|
@@ -113,7 +115,10 @@ const Responsive = ({ children, isEditor }) => {
|
|
|
113
115
|
|
|
114
116
|
return (
|
|
115
117
|
<div className='cove-editor__content' data-grid={displayGrid || null}>
|
|
116
|
-
<div
|
|
118
|
+
<div
|
|
119
|
+
className='cove-editor__content-wrap--x'
|
|
120
|
+
style={viewportPreview ? { maxWidth: viewportPreview + 'px', minWidth: 'unset' } : null}
|
|
121
|
+
>
|
|
117
122
|
<div className='cove-editor__content-wrap--y'>
|
|
118
123
|
<div className='cove-editor-utils__breakpoints--px'>
|
|
119
124
|
{displayGrid && displayPanel && (
|
|
@@ -143,7 +148,8 @@ const Responsive = ({ children, isEditor }) => {
|
|
|
143
148
|
<p className={displayGrid ? 'hotkey--active' : null}>G</p>
|
|
144
149
|
<p className={rotateAnimation ? 'hotkey--active' : null}>R</p>
|
|
145
150
|
<p className={viewportPreview ? 'hotkey--active' : null}>
|
|
146
|
-
{os === 'MacOS' ? <Icon style={{ marginRight: '0.25rem' }} display='command' size={12} /> : 'Alt'} +
|
|
151
|
+
{os === 'MacOS' ? <Icon style={{ marginRight: '0.25rem' }} display='command' size={12} /> : 'Alt'} +{' '}
|
|
152
|
+
{viewportPreview ? breakpoints.indexOf(viewportPreview) + 1 : `[1 - ${breakpoints.length}]`}
|
|
147
153
|
</p>
|
|
148
154
|
</div>
|
|
149
155
|
</div>
|
|
@@ -161,7 +167,11 @@ const Responsive = ({ children, isEditor }) => {
|
|
|
161
167
|
</div>
|
|
162
168
|
</button>
|
|
163
169
|
{breakpoints.map((breakpoint, index) => (
|
|
164
|
-
<button
|
|
170
|
+
<button
|
|
171
|
+
className={`cove-editor-utils__breakpoints-item${viewportPreview === breakpoint ? ' active' : ''}`}
|
|
172
|
+
onClick={() => viewportPreviewController(breakpoint)}
|
|
173
|
+
key={index}
|
|
174
|
+
>
|
|
165
175
|
{breakpoint}px
|
|
166
176
|
</button>
|
|
167
177
|
))}
|
|
@@ -21,21 +21,30 @@ const Sidebar: React.FC<SidebarProps> = props => {
|
|
|
21
21
|
const sectionClasses = ['editor-panel', 'cove', 'sidebar']
|
|
22
22
|
if (!displayPanel) sectionClasses.push('hidden')
|
|
23
23
|
if (isDashboard) sectionClasses.push('dashboard')
|
|
24
|
-
return sectionClasses
|
|
24
|
+
return sectionClasses.join(' ')
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const getButtonClasses = () => {
|
|
28
28
|
const buttonClasses = []
|
|
29
29
|
if (displayPanel) buttonClasses.push('editor-panel__toggle')
|
|
30
30
|
if (!displayPanel) buttonClasses.push('collapsed', 'editor-panel__toggle')
|
|
31
|
-
return buttonClasses
|
|
31
|
+
return buttonClasses.join(' ')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const getTitleClasses = () => {
|
|
35
|
+
const titleClasses = ['editor-panel__title']
|
|
36
|
+
if (!displayPanel) titleClasses.push('collapsed')
|
|
37
|
+
return titleClasses.join(' ')
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
return (
|
|
35
41
|
<>
|
|
36
|
-
|
|
37
|
-
<section className={
|
|
38
|
-
<
|
|
42
|
+
{/* mimic the editor panel title to keep the button visible. */}
|
|
43
|
+
<section className='editor-panel__toggle-wrapper p-absolute' style={{ height: '49.75px', width: '350px' }}>
|
|
44
|
+
<button className={getButtonClasses()} title={displayPanel ? `Collapse Editor` : `Expand Editor`} onClick={onBackClick}></button>
|
|
45
|
+
</section>
|
|
46
|
+
<section className={getSectionClasses()}>
|
|
47
|
+
<h2 className={getTitleClasses()}>{title}</h2>
|
|
39
48
|
<section className='form-container' data-html2canvas-ignore>
|
|
40
49
|
{children}
|
|
41
50
|
</section>
|