@cdc/core 4.24.3 → 4.24.5

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 (72) hide show
  1. package/assets/icon-command.svg +3 -0
  2. package/assets/icon-rotate-left.svg +3 -0
  3. package/components/AdvancedEditor.jsx +9 -0
  4. package/components/DataTable/DataTable.tsx +20 -15
  5. package/components/DataTable/DataTableStandAlone.tsx +51 -4
  6. package/components/DataTable/components/ChartHeader.tsx +3 -2
  7. package/components/DataTable/components/DataTableEditorPanel.tsx +20 -3
  8. package/components/DataTable/components/ExpandCollapse.tsx +22 -16
  9. package/components/DataTable/helpers/chartCellMatrix.tsx +3 -2
  10. package/components/DataTable/helpers/getChartCellValue.ts +21 -1
  11. package/components/DataTable/helpers/getDataSeriesColumns.ts +37 -16
  12. package/components/DataTable/helpers/getSeriesName.ts +2 -1
  13. package/components/DataTable/helpers/mapCellMatrix.tsx +2 -2
  14. package/components/DataTable/helpers/{customColumns.ts → removeNullColumns.ts} +3 -3
  15. package/components/DataTable/types/TableConfig.ts +11 -21
  16. package/components/EditorPanel/ColumnsEditor.tsx +304 -260
  17. package/components/EditorPanel/DataTableEditor.tsx +119 -48
  18. package/components/EditorPanel/VizFilterEditor.tsx +234 -0
  19. package/components/EditorWrapper/EditorWrapper.tsx +48 -0
  20. package/components/EditorWrapper/editor-wrapper.style.css +14 -0
  21. package/components/Filters.jsx +78 -61
  22. package/components/Layout/components/Responsive.tsx +184 -0
  23. package/components/Layout/components/Sidebar/components/Sidebar.tsx +47 -0
  24. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +902 -0
  25. package/components/Layout/components/Sidebar/index.tsx +3 -0
  26. package/components/Layout/components/Visualization/index.tsx +81 -0
  27. package/components/Layout/components/Visualization/visualizations.scss +33 -0
  28. package/components/Layout/index.tsx +11 -0
  29. package/components/Layout/styles/editor-grid-view.scss +156 -0
  30. package/components/Layout/styles/editor-utils.scss +197 -0
  31. package/components/Layout/styles/editor.scss +144 -0
  32. package/components/LegendCircle.jsx +4 -3
  33. package/components/MediaControls.jsx +1 -1
  34. package/components/Table/Table.tsx +7 -5
  35. package/components/Table/components/Row.tsx +6 -2
  36. package/components/Table/types/RowType.ts +3 -0
  37. package/components/Waiting.jsx +11 -1
  38. package/components/_stories/DataTable.stories.tsx +1 -2
  39. package/components/_stories/EditorPanel.stories.tsx +1 -0
  40. package/components/createBarElement.jsx +37 -34
  41. package/components/elements/SkipTo.tsx +37 -5
  42. package/components/managers/DataDesigner.tsx +18 -18
  43. package/components/ui/Icon.tsx +5 -1
  44. package/helpers/DataTransform.ts +9 -32
  45. package/helpers/coveUpdateWorker.ts +21 -0
  46. package/helpers/footnoteSymbols.ts +11 -0
  47. package/helpers/useDataVizClasses.js +1 -5
  48. package/helpers/ver/4.23.4.ts +27 -0
  49. package/helpers/ver/4.24.3.ts +56 -0
  50. package/helpers/ver/4.24.5.ts +32 -0
  51. package/package.json +2 -2
  52. package/styles/_data-table.scss +8 -0
  53. package/styles/_global.scss +7 -4
  54. package/styles/_reset.scss +7 -6
  55. package/styles/_variables.scss +3 -0
  56. package/styles/base.scss +0 -18
  57. package/styles/v2/base/index.scss +1 -1
  58. package/styles/v2/components/ui/tooltip.scss +0 -21
  59. package/types/Axis.ts +4 -0
  60. package/types/Column.ts +1 -0
  61. package/types/ConfigureData.ts +8 -0
  62. package/types/DataDescription.ts +9 -0
  63. package/types/Legend.ts +1 -0
  64. package/types/MarkupInclude.ts +26 -0
  65. package/types/Runtime.ts +1 -0
  66. package/types/Series.ts +1 -1
  67. package/types/Table.ts +15 -13
  68. package/types/Visualization.ts +14 -10
  69. package/types/VizFilter.ts +13 -0
  70. package/helpers/coveUpdateWorker.js +0 -19
  71. package/helpers/ver/4.23.js +0 -10
  72. package/helpers/ver/4.24.3.js +0 -25
@@ -8,30 +8,57 @@ import { getQueryParams, updateQueryString } from '@cdc/core/helpers/queryString
8
8
  // Third Party
9
9
  import PropTypes from 'prop-types'
10
10
 
11
+ export const filterStyleOptions = ['dropdown', 'pill', 'tab', 'tab bar']
12
+
13
+ export const filterOrderOptions = [
14
+ {
15
+ label: 'Ascending Alphanumeric',
16
+ value: 'asc'
17
+ },
18
+ {
19
+ label: 'Descending Alphanumeric',
20
+ value: 'desc'
21
+ },
22
+ {
23
+ label: 'Custom',
24
+ value: 'cust'
25
+ }
26
+ ]
27
+
28
+ export const handleSorting = singleFilter => {
29
+ const { order } = singleFilter
30
+
31
+ const sortAsc = (a, b) => {
32
+ return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
33
+ }
34
+
35
+ const sortDesc = (a, b) => {
36
+ return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
37
+ }
38
+
39
+ if (!order || order === '') {
40
+ singleFilter.order = 'asc'
41
+ }
42
+
43
+ if (order === 'desc') {
44
+ singleFilter.values = singleFilter.values.sort(sortDesc)
45
+ }
46
+
47
+ if (order === 'asc') {
48
+ singleFilter.values = singleFilter.values.sort(sortAsc)
49
+ }
50
+ return singleFilter
51
+ }
52
+
53
+ const hasStandardFilterBehavior = ['chart', 'table']
54
+
11
55
  export const useFilters = props => {
12
56
  const [showApplyButton, setShowApplyButton] = useState(false)
13
57
 
14
58
  // Desconstructing: notice, adding more descriptive visualizationConfig name over config
15
59
  // 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, filterData } = props
17
- const { type, filterBehavior, filters } = visualizationConfig
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
- ]
60
+ const { config: visualizationConfig, setConfig, filteredData, setFilteredData, excludedData, filterData, getUniqueValues } = props
61
+ const { type, data } = visualizationConfig
35
62
 
36
63
  /**
37
64
  * Re-orders a filter based on two indices and updates the runtime filters array and filters state
@@ -51,7 +78,7 @@ export const useFilters = props => {
51
78
  const [movedItem] = updatedValues.splice(idx1, 1)
52
79
  updatedValues.splice(idx2, 0, movedItem)
53
80
 
54
- const filtersCopy = visualizationConfig.type === 'chart' ? [...visualizationConfig.filters] : [...filteredData]
81
+ const filtersCopy = hasStandardFilterBehavior.includes(visualizationConfig.type) ? [...visualizationConfig.filters] : [...filteredData]
55
82
  const filterItem = { ...filtersCopy[filterIndex] }
56
83
 
57
84
  // Overwrite filterItem.values since thats what we map through in the editor panel
@@ -99,7 +126,7 @@ export const useFilters = props => {
99
126
  }
100
127
 
101
128
  // If we're on a chart and not using the apply button
102
- if (visualizationConfig.type === 'chart' && visualizationConfig.filterBehavior === 'Filter Change') {
129
+ if (hasStandardFilterBehavior.includes(visualizationConfig.type) && visualizationConfig.filterBehavior === 'Filter Change') {
103
130
  setFilteredData(filterData(newFilters, excludedData))
104
131
  }
105
132
  }
@@ -127,7 +154,7 @@ export const useFilters = props => {
127
154
  setFilteredData(newFilters, excludedData)
128
155
  }
129
156
 
130
- if (type === 'chart') {
157
+ if (hasStandardFilterBehavior.includes(visualizationConfig.type)) {
131
158
  setFilteredData(filterData(newFilters, excludedData))
132
159
  }
133
160
 
@@ -139,19 +166,33 @@ export const useFilters = props => {
139
166
  e.preventDefault()
140
167
 
141
168
  // reset to first item in values array.
142
- newFilters.map(filter => {
143
- filter = handleSorting(filter)
144
- filter.active = filter.values[0]
145
- return filter
169
+ let needsQueryUpdate = false
170
+ const queryParams = getQueryParams()
171
+ newFilters.forEach((filter, i) => {
172
+ if(!filter.values || filter.values.length === 0){
173
+ filter.values = getUniqueValues(data, filter.columnName)
174
+ }
175
+ newFilters[i].active = handleSorting(filter).values[0]
176
+
177
+
178
+ if (filter.setByQueryParameter && queryParams[filter.setByQueryParameter] !== filter.active) {
179
+ queryParams[filter.setByQueryParameter] = filter.active
180
+ needsQueryUpdate = true
181
+ }
146
182
  })
147
183
 
184
+ if (needsQueryUpdate) {
185
+ updateQueryString(queryParams)
186
+ }
187
+
188
+ setConfig({ ...visualizationConfig, filters: newFilters })
189
+
148
190
  if (type === 'map') {
149
191
  setFilteredData(newFilters, excludedData)
150
192
  } else {
151
193
  setFilteredData(filterData(newFilters, excludedData))
152
194
  }
153
195
 
154
- setConfig({ ...visualizationConfig, filters: newFilters })
155
196
  }
156
197
 
157
198
  const filterConstants = {
@@ -161,31 +202,6 @@ export const useFilters = props => {
161
202
  applyText: 'Select the apply button to update the visualization information.'
162
203
  }
163
204
 
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
205
  // prettier-ignore
190
206
  return {
191
207
  handleApplyButton,
@@ -202,8 +218,8 @@ export const useFilters = props => {
202
218
  }
203
219
 
204
220
  const Filters = props => {
205
- const { config: visualizationConfig, filteredData, dimensions } = props
206
- const { filters, type, general, theme, filterBehavior } = visualizationConfig
221
+ const { config: visualizationConfig, filteredData, dimensions, getUniqueValues } = props
222
+ const { filters, type, general, theme, filterBehavior, data } = visualizationConfig
207
223
  const [mobileFilterStyle, setMobileFilterStyle] = useState(false)
208
224
  const [selectedFilter, setSelectedFilter] = useState('')
209
225
  const id = useId()
@@ -239,16 +255,17 @@ const Filters = props => {
239
255
  const Filters = props => props.children
240
256
 
241
257
  const filterSectionClassList = ['filters-section', type === 'map' ? general.headerColor : visualizationConfig?.visualizationType === 'Spark Line' ? null : theme]
242
-
243
258
  // Exterior Section Wrapper
244
259
  Filters.Section = props => {
245
260
  return (
246
- <section className={filterSectionClassList.join(' ')}>
247
- <p className='filters-section__intro-text'>
248
- {filterConstants.introText} {visualizationConfig.filterBehavior === 'Apply Button' && filterConstants.applyText}
249
- </p>
250
- <div className='filters-section__wrapper'>{props.children}</div>
251
- </section>
261
+ visualizationConfig?.filters && (
262
+ <section className={filterSectionClassList.join(' ')}>
263
+ <p className='filters-section__intro-text'>
264
+ {filters?.some(f => f.active) ? filterConstants.introText : ''} {visualizationConfig.filterBehavior === 'Apply Button' && filterConstants.applyText}
265
+ </p>
266
+ <div className='filters-section__wrapper'>{props.children}</div>
267
+ </section>
268
+ )
252
269
  )
253
270
  }
254
271
 
@@ -0,0 +1,184 @@
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react'
2
+ import '../styles/editor-utils.scss'
3
+ import '../styles/editor.scss'
4
+
5
+ import Icon from '../../ui/Icon'
6
+
7
+ const breakpoints = [
8
+ '360', // xxs (mobile) 0 - 360
9
+ '480', // xs
10
+ '768', // sm
11
+ '960', // md
12
+ '1170', // lg
13
+ '1280' // xl
14
+ ]
15
+
16
+ const os = navigator.userAgent.indexOf('Win') !== -1 ? 'Win' : navigator.userAgent.indexOf('Mac') !== -1 ? 'MacOS' : null
17
+
18
+ const Responsive = ({ children, isEditor }) => {
19
+ const [displayPanel, setDisplayPanel] = useState(false)
20
+ const [displayGrid, setDisplayGrid] = useState(false)
21
+ const [viewportPreview, setViewportPreview] = useState(null)
22
+ const [rotateAnimation, setRotateAnimation] = useState(false)
23
+ const [showConfirm, setShowConfirm] = useState(false)
24
+ const [previewDimensions, setPreviewDimensions] = useState<{ width: number; height: number }>(null)
25
+
26
+ const resetIcon = useRef(null)
27
+ const editorPanelRef = useRef(null)
28
+ const componentContainerRef = useRef(null)
29
+
30
+ const viewportPreviewController = useCallback(
31
+ breakpoint => {
32
+ return setViewportPreview(prevState => (prevState !== breakpoint ? breakpoint : null))
33
+ },
34
+ [viewportPreview]
35
+ )
36
+
37
+ const onKeypress = key => {
38
+ if (key.code === 'KeyL' && key.ctrlKey) setDisplayPanel(display => !display)
39
+ const viewportCommandKey = os === 'MacOS' ? key.metaKey : key.altKey
40
+ if (viewportCommandKey) {
41
+ let keyIndex = key.key
42
+
43
+ // Validates that the hotkey pressed is a number, and that
44
+ // the number is within the range of the provided breakpoint list range.
45
+ if (!isNaN(keyIndex)) {
46
+ if (keyIndex <= breakpoints.length) {
47
+ key.preventDefault()
48
+ viewportPreviewController(breakpoints[keyIndex - 1])
49
+ }
50
+ }
51
+ }
52
+
53
+ if (!viewportCommandKey) {
54
+ if (key.code === 'KeyG') setDisplayGrid(display => !display)
55
+ if (key.code === 'KeyR') resetPreview()
56
+ }
57
+ }
58
+
59
+ // Set and clean up the event listener for the hotkeys
60
+ useEffect(() => {
61
+ document.addEventListener('keydown', onKeypress)
62
+ return () => document.removeEventListener('keydown', onKeypress)
63
+ }, [])
64
+
65
+ //Reset Viewport Preview
66
+ const resetPreview = useCallback(() => {
67
+ if (!rotateAnimation && resetIcon.current) {
68
+ setViewportPreview(null)
69
+ setRotateAnimation(true)
70
+ setDisplayGrid(false)
71
+ resetIcon.current.style.transition = 'transform 800ms cubic-bezier(0.16, 1, 0.3, 1)'
72
+ resetIcon.current.style.transform = 'rotate(-360deg)'
73
+
74
+ const timeoutShow = setTimeout(() => {
75
+ setRotateAnimation(false)
76
+ resetIcon.current.style.transition = null
77
+ resetIcon.current.style.transform = 'rotate(0deg)'
78
+ resetIcon.current.style.transform = null
79
+ }, 400)
80
+
81
+ return () => clearTimeout(timeoutShow)
82
+ }
83
+ }, [rotateAnimation])
84
+
85
+ // Toggle the grid display with the viewport preview
86
+ useEffect(() => {
87
+ return viewportPreview ? setDisplayGrid(true) : setDisplayGrid(false)
88
+ }, [viewportPreview])
89
+
90
+ // Observe and set editor component widths
91
+ useEffect(() => {
92
+ if (!componentContainerRef.current) return
93
+
94
+ let resizeObserver = new ResizeObserver(entries => {
95
+ for (let entry of entries) {
96
+ let { width, height } = entry.contentRect
97
+ setPreviewDimensions({ width, height })
98
+ }
99
+ })
100
+
101
+ resizeObserver.observe(componentContainerRef.current)
102
+
103
+ return () => {
104
+ if (!resizeObserver) return
105
+ resizeObserver.disconnect()
106
+ resizeObserver = null
107
+ }
108
+ })
109
+
110
+ const onBackClick = () => setDisplayPanel(!displayPanel)
111
+
112
+ if (!isEditor || !displayPanel) return children
113
+
114
+ return (
115
+ <div className='cove-editor__content' data-grid={displayGrid || null}>
116
+ <div className='cove-editor__content-wrap--x' style={viewportPreview ? { maxWidth: viewportPreview + 'px', minWidth: 'unset' } : null}>
117
+ <div className='cove-editor__content-wrap--y'>
118
+ <div className='cove-editor-utils__breakpoints--px'>
119
+ {displayGrid && displayPanel && (
120
+ <>
121
+ {Math.round(previewDimensions.width)}
122
+ <span className='mx-1' style={{ fontSize: '0.675rem' }}>
123
+
124
+ </span>
125
+ {Math.round(previewDimensions.height)}
126
+ </>
127
+ )}
128
+ </div>
129
+ <div className='cove-editor__grid-caret--top' ref={componentContainerRef}>
130
+ <div className='cove-editor__grid-caret--bottom'>{children}</div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ <div className='cove-editor-utils__hotkeys'>
135
+ <div className='cove-editor-utils__hotkeys--left'>
136
+ <p className={displayPanel ? 'hotkey--active' : null}>Editor</p>
137
+ <p className={displayGrid ? 'hotkey--active' : null}>Grid</p>
138
+ <p className={rotateAnimation ? 'hotkey--active' : null}>Reset</p>
139
+ <p className={viewportPreview ? 'hotkey--active' : null}>View</p>
140
+ </div>
141
+ <div className='cove-editor-utils__hotkeys--right'>
142
+ <p className={displayPanel ? 'hotkey--active' : null}>esc</p>
143
+ <p className={displayGrid ? 'hotkey--active' : null}>G</p>
144
+ <p className={rotateAnimation ? 'hotkey--active' : null}>R</p>
145
+ <p className={viewportPreview ? 'hotkey--active' : null}>
146
+ {os === 'MacOS' ? <Icon style={{ marginRight: '0.25rem' }} display='command' size={12} /> : 'Alt'} + {viewportPreview ? breakpoints.indexOf(viewportPreview) + 1 : `[1 - ${breakpoints.length}]`}
147
+ </p>
148
+ </div>
149
+ </div>
150
+
151
+ <div className='cove-editor-utils__breakpoints'>
152
+ <ul className={`cove-editor-utils__breakpoints-list${viewportPreview ? ' has-active' : ''}`}>
153
+ <button
154
+ className='cove-editor-utils__breakpoints-item'
155
+ onClick={() => {
156
+ setDisplayGrid(display => !display)
157
+ }}
158
+ >
159
+ <div className='cove-editor-utils__breakpoints-grid'>
160
+ <Icon display='grid' />
161
+ </div>
162
+ </button>
163
+ {breakpoints.map((breakpoint, index) => (
164
+ <button className={`cove-editor-utils__breakpoints-item${viewportPreview === breakpoint ? ' active' : ''}`} onClick={() => viewportPreviewController(breakpoint)} key={index}>
165
+ {breakpoint}px
166
+ </button>
167
+ ))}
168
+ <button
169
+ className='cove-editor-utils__breakpoints-item'
170
+ onClick={() => {
171
+ resetPreview()
172
+ }}
173
+ >
174
+ <div className='cove-editor-utils__breakpoints-reset' ref={resetIcon}>
175
+ <Icon display='rotateLeft' />
176
+ </div>
177
+ </button>
178
+ </ul>
179
+ </div>
180
+ </div>
181
+ )
182
+ }
183
+
184
+ export default Responsive
@@ -0,0 +1,47 @@
1
+ import React from 'react'
2
+ import './sidebar.styles.scss'
3
+
4
+ type SidebarProps = {
5
+ // whether or not the viz is within a dashboard
6
+ isDashboard: boolean
7
+ // show/hide the sidebar
8
+ displayPanel: boolean
9
+ // sidebarTitle
10
+ title: string
11
+ // inner content
12
+ children: React.ReactNode
13
+ // on arrow toggle
14
+ onBackClick: () => void
15
+ }
16
+
17
+ const Sidebar: React.FC<SidebarProps> = props => {
18
+ const { displayPanel = false, isDashboard = false, title = 'Configure Visualization', children, onBackClick } = props
19
+
20
+ const getSectionClasses = () => {
21
+ const sectionClasses = ['editor-panel', 'cove', 'sidebar']
22
+ if (!displayPanel) sectionClasses.push('hidden')
23
+ if (isDashboard) sectionClasses.push('dashboard')
24
+ return sectionClasses
25
+ }
26
+
27
+ const getButtonClasses = () => {
28
+ const buttonClasses = []
29
+ if (displayPanel) buttonClasses.push('editor-panel__toggle')
30
+ if (!displayPanel) buttonClasses.push('collapsed', 'editor-panel__toggle')
31
+ return buttonClasses
32
+ }
33
+
34
+ return (
35
+ <>
36
+ <button className={getButtonClasses().join(' ')} title={displayPanel ? `Collapse Editor` : `Expand Editor`} onClick={onBackClick}></button>
37
+ <section className={getSectionClasses().join(' ')}>
38
+ <h2 className='editor-panel__title'>{title}</h2>
39
+ <section className='form-container' data-html2canvas-ignore>
40
+ {children}
41
+ </section>
42
+ </section>
43
+ </>
44
+ )
45
+ }
46
+
47
+ export default Sidebar