@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.
Files changed (66) hide show
  1. package/components/AdvancedEditor/AdvancedEditor.tsx +93 -0
  2. package/components/AdvancedEditor/advanced-editor-styles.css +3 -0
  3. package/components/AdvancedEditor/index.ts +1 -0
  4. package/components/DataTable/DataTable.tsx +21 -2
  5. package/components/DataTable/DataTableStandAlone.tsx +4 -25
  6. package/components/DataTable/components/DataTableEditorPanel.tsx +4 -4
  7. package/components/DataTable/components/ExpandCollapse.tsx +1 -1
  8. package/components/DataTable/helpers/chartCellMatrix.tsx +3 -9
  9. package/components/DataTable/helpers/getChartCellValue.ts +8 -4
  10. package/components/DataTable/helpers/getDataSeriesColumns.ts +8 -5
  11. package/components/DataTable/helpers/getRowType.ts +6 -0
  12. package/components/DataTable/types/TableConfig.ts +1 -0
  13. package/components/EditorPanel/ColumnsEditor.tsx +3 -30
  14. package/components/EditorPanel/DataTableEditor.tsx +66 -22
  15. package/components/EditorPanel/FieldSetWrapper.tsx +51 -0
  16. package/components/EditorPanel/FootnotesEditor.tsx +77 -0
  17. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +227 -0
  18. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +54 -0
  19. package/components/EditorPanel/VizFilterEditor/index.ts +1 -0
  20. package/components/EditorWrapper/EditorWrapper.tsx +3 -4
  21. package/components/EditorWrapper/index.ts +1 -0
  22. package/components/{Filters.jsx → Filters.tsx} +40 -24
  23. package/components/Footnotes/Footnotes.tsx +25 -0
  24. package/components/Footnotes/FootnotesStandAlone.tsx +45 -0
  25. package/components/Footnotes/footnotes.css +5 -0
  26. package/components/Footnotes/index.ts +1 -0
  27. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +8 -4
  28. package/components/Layout/components/Visualization/index.tsx +12 -5
  29. package/components/MultiSelect/MultiSelect.tsx +36 -9
  30. package/components/MultiSelect/multiselect.styles.css +0 -3
  31. package/components/_stories/Footnotes.stories.tsx +17 -0
  32. package/components/_stories/styles.scss +1 -0
  33. package/components/inputs/InputSelect.tsx +17 -6
  34. package/components/ui/Icon.tsx +1 -2
  35. package/helpers/addValuesToFilters.ts +56 -0
  36. package/helpers/cove/accessibility.ts +1 -0
  37. package/helpers/cove/fontSettings.ts +2 -0
  38. package/helpers/coveUpdateWorker.ts +7 -0
  39. package/helpers/filterVizData.ts +30 -0
  40. package/helpers/formatConfigBeforeSave.ts +90 -0
  41. package/helpers/gatherQueryParams.ts +14 -7
  42. package/helpers/lineChartHelpers.js +2 -1
  43. package/helpers/pivotData.ts +18 -0
  44. package/helpers/queryStringUtils.ts +29 -0
  45. package/helpers/tests/updateFieldFactory.test.ts +1 -0
  46. package/helpers/updateFieldFactory.ts +1 -1
  47. package/helpers/ver/4.24.7.ts +92 -0
  48. package/package.json +6 -4
  49. package/styles/_button-section.scss +6 -1
  50. package/styles/_data-table.scss +0 -1
  51. package/styles/base.scss +4 -0
  52. package/styles/v2/themes/_color-definitions.scss +1 -0
  53. package/types/Annotation.ts +46 -0
  54. package/types/Axis.ts +0 -2
  55. package/types/ConfigureData.ts +1 -1
  56. package/types/Footnotes.ts +17 -0
  57. package/types/General.ts +5 -0
  58. package/types/Runtime.ts +2 -7
  59. package/types/Table.ts +6 -0
  60. package/types/Visualization.ts +31 -9
  61. package/types/VizFilter.ts +16 -5
  62. package/LICENSE +0 -201
  63. package/components/AdvancedEditor.jsx +0 -74
  64. package/components/EditorPanel/VizFilterEditor.tsx +0 -234
  65. package/helpers/queryStringUtils.js +0 -26
  66. 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 React, { useState, useEffect, useRef } from 'react'
1
+ import { useState, useEffect } from 'react'
2
2
  import { useId } from 'react'
3
3
 
4
4
  // CDC
5
- import Button from '@cdc/core/components/elements/Button'
6
- import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryStringUtils'
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, filterData, getUniqueValues } = props
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(filterData(newFilters, excludedData))
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(filterData(newFilters, excludedData))
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(filterData(newFilters, excludedData))
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
- const Filters = props => {
221
- const { config: visualizationConfig, filteredData, dimensions, getUniqueValues } = props
222
- const { filters, type, general, theme, filterBehavior, data } = visualizationConfig
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
- let el = document.getElementById(selectedFilter.id)
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 = props => {
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'>{props.children}</div>
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 = props => {
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.forEach((filterOption, index) => {
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,5 @@
1
+ :is(ul).cove-footnotes {
2
+ :is(li) {
3
+ list-style: none;
4
+ }
5
+ }
@@ -0,0 +1 @@
1
+ export { default } from './Footnotes'