@cdc/dashboard 4.26.3 → 4.26.4

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 (85) hide show
  1. package/CONFIG.md +172 -0
  2. package/README.md +60 -20
  3. package/dist/cdcdashboard-CY9IcPSi.es.js +6 -0
  4. package/dist/cdcdashboard-DlpiY3fQ.es.js +4 -0
  5. package/dist/cdcdashboard.js +51744 -49003
  6. package/examples/__data__/data-2.json +6 -0
  7. package/examples/__data__/data.json +6 -0
  8. package/examples/legend-issue.json +1 -1
  9. package/examples/minimal-example.json +34 -0
  10. package/examples/private/dengue.json +4640 -0
  11. package/examples/private/link_to_file.json +16662 -0
  12. package/examples/sankey.json +3 -3
  13. package/examples/test-api-filter-reset.json +4 -4
  14. package/examples/tp5-test.json +86 -4
  15. package/package.json +9 -9
  16. package/src/CdcDashboardComponent.tsx +1 -1
  17. package/src/_stories/Dashboard.smoke.stories.tsx +33 -0
  18. package/src/_stories/Dashboard.stories.tsx +43 -2
  19. package/src/_stories/_mock/dashboard-data-driven-colors.json +171 -0
  20. package/src/_stories/_mock/tp5-test.json +86 -5
  21. package/src/components/DashboardEditors.tsx +15 -0
  22. package/src/components/DashboardFilters/DashboardFilters.test.tsx +129 -0
  23. package/src/components/DashboardFilters/DashboardFilters.tsx +10 -7
  24. package/src/components/DashboardFilters/DashboardFiltersEditor/DashboardFiltersEditor.tsx +5 -4
  25. package/src/components/DashboardFilters/DashboardFiltersEditor/components/APIModal.tsx +5 -3
  26. package/src/components/DashboardFilters/DashboardFiltersEditor/components/DeleteFilterModal.tsx +59 -58
  27. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.test.tsx +127 -0
  28. package/src/components/DashboardFilters/DashboardFiltersEditor/components/FilterEditor.tsx +28 -4
  29. package/src/components/DashboardFilters/DashboardFiltersEditor/components/NestedDropDownDashboard.tsx +2 -2
  30. package/src/components/ExpandCollapseButtons.tsx +6 -4
  31. package/src/components/Grid.tsx +4 -3
  32. package/src/components/MultiConfigTabs/MultiConfigTabs.tsx +141 -140
  33. package/src/components/Row.tsx +11 -10
  34. package/src/components/VisualizationRow.tsx +71 -20
  35. package/src/components/VisualizationsPanel/VisualizationsPanel.tsx +1 -1
  36. package/src/components/Widget/Widget.tsx +5 -4
  37. package/src/components/Widget/widget.styles.css +41 -10
  38. package/src/data/initial-state.js +1 -0
  39. package/src/helpers/addVisualization.ts +3 -1
  40. package/src/helpers/tests/addVisualization.test.ts +1 -1
  41. package/src/scss/grid.scss +38 -8
  42. package/src/scss/main.scss +110 -38
  43. package/src/store/dashboard.reducer.ts +1 -0
  44. package/src/test/CdcDashboard.test.jsx +24 -0
  45. package/src/types/SharedFilter.ts +1 -0
  46. package/tests/fixtures/dashboard-config-with-metadata.json +1 -1
  47. package/LICENSE +0 -201
  48. package/dist/cdcdashboard-vr9HZwRt.es.js +0 -6
  49. package/examples/DEV-6574.json +0 -2224
  50. package/examples/api-dashboard-data.json +0 -272
  51. package/examples/api-dashboard-years.json +0 -11
  52. package/examples/api-geographies-data.json +0 -11
  53. package/examples/chart-data.json +0 -5409
  54. package/examples/custom/css/respiratory.css +0 -236
  55. package/examples/custom/js/respiratory.js +0 -242
  56. package/examples/default-data.json +0 -368
  57. package/examples/default-filter-control.json +0 -209
  58. package/examples/default-multi-dataset-shared-filter.json +0 -1729
  59. package/examples/default-multi-dataset.json +0 -506
  60. package/examples/ed-visits-county-file.json +0 -402
  61. package/examples/filters/Alabama.json +0 -72
  62. package/examples/filters/Alaska.json +0 -1737
  63. package/examples/filters/Arkansas.json +0 -4713
  64. package/examples/filters/California.json +0 -212
  65. package/examples/filters/Colorado.json +0 -1500
  66. package/examples/filters/Connecticut.json +0 -559
  67. package/examples/filters/Delaware.json +0 -63
  68. package/examples/filters/DistrictofColumbia.json +0 -63
  69. package/examples/filters/Florida.json +0 -4217
  70. package/examples/filters/States.json +0 -146
  71. package/examples/state-level.json +0 -90136
  72. package/examples/state-points.json +0 -10474
  73. package/examples/temp-example-data.json +0 -130
  74. package/examples/test-dashboard-simple.json +0 -503
  75. package/examples/test-example.json +0 -752
  76. package/examples/test-file.json +0 -147
  77. package/examples/test.json +0 -752
  78. package/examples/testing.json +0 -94456
  79. /package/examples/{data → __data__}/data-with-metadata.json +0 -0
  80. /package/examples/{legend-issue-data.json → __data__/legend-issue-data.json} +0 -0
  81. /package/examples/api-test/{categories.json → __data__/categories.json} +0 -0
  82. /package/examples/api-test/{chart-data.json → __data__/chart-data.json} +0 -0
  83. /package/examples/api-test/{topics.json → __data__/topics.json} +0 -0
  84. /package/examples/api-test/{years.json → __data__/years.json} +0 -0
  85. /package/src/_stories/{Dashboard.Pages.stories.tsx → Dashboard.Pages.smoke.stories.tsx} +0 -0
@@ -1,140 +1,141 @@
1
- import { createRef, useContext, useMemo, useState } from 'react'
2
- import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
3
- import Modal from '@cdc/core/components/ui/Modal'
4
- import { useGlobalContext } from '@cdc/core/components/GlobalContext'
5
- import './multiconfigtabs.styles.css'
6
-
7
- const AreYouSure = deleteCallback => {
8
- return (
9
- <Modal>
10
- <Modal.Content>
11
- <p>Are you sure you want to delete this dashboard? </p>
12
- <button className='btn btn-danger' onClick={deleteCallback}>
13
- DELETE
14
- </button>
15
- </Modal.Content>
16
- </Modal>
17
- )
18
- }
19
-
20
- const Tab = ({ name, handleClick, tabs, index, active }) => {
21
- const [editing, setEditing] = useState(false)
22
- const dispatch = useContext(DashboardDispatchContext)
23
- const { overlay } = useGlobalContext()
24
- const inputRef = createRef<HTMLInputElement>()
25
-
26
- const saveName = e => {
27
- e.stopPropagation()
28
- const newVal = inputRef.current.value
29
- const sameName = newVal === name
30
- const blankName = !newVal
31
- const duplicateName = tabs.includes(newVal)
32
- if (!sameName && !blankName && !duplicateName) {
33
- dispatch({ type: 'RENAME_DASHBOARD_TAB', payload: { current: name, new: newVal } })
34
- }
35
- setEditing(false)
36
- }
37
-
38
- const onClick = e => {
39
- // ignore click on delete button
40
- if (e.target.className === 'remove') return
41
- if (active) {
42
- setEditing(true)
43
- } else {
44
- handleClick()
45
- }
46
- }
47
-
48
- const handleRemove = () => {
49
- const deleteCallback = () => {
50
- dispatch({ type: 'REMOVE_MULTIDASHBOARD_AT_INDEX', payload: index })
51
- overlay?.actions.toggleOverlay(false)
52
- }
53
- overlay?.actions.openOverlay(AreYouSure(deleteCallback))
54
- }
55
-
56
- const handleReorder = (index: number, moveTo: -1 | 1) => {
57
- const newIndex = index + moveTo
58
- const inbounds = newIndex > -1 && newIndex <= tabs.length - 1
59
- if (inbounds) {
60
- dispatch({ type: 'REORDER_MULTIDASHBOARDS', payload: { currentIndex: index, newIndex: index + moveTo } })
61
- }
62
- }
63
-
64
- const canMoveLeft = index !== 0
65
- const canMoveRight = index <= tabs.length - 2
66
-
67
- return (
68
- <li className='nav-item d-flex mt-0'>
69
- {canMoveLeft && editing && (
70
- <button className='border-0' onClick={() => handleReorder(index, -1)}>
71
- {'<'}
72
- </button>
73
- )}
74
- <div
75
- className={`edit nav-link${active ? ' active' : ''}`}
76
- aria-current={active ? 'page' : null}
77
- onClick={onClick}
78
- >
79
- {editing ? (
80
- <div className='d-flex'>
81
- <input type='text' defaultValue={name} onBlur={saveName} ref={inputRef} />
82
- <button className='btn btn-link save' onClick={saveName}>
83
- save
84
- </button>
85
- </div>
86
- ) : (
87
- <>
88
- {name}
89
- <button className='btn btn-danger border-0 ms-1' onClick={handleRemove}>
90
- X
91
- </button>
92
- </>
93
- )}
94
- </div>
95
- {canMoveRight && editing && (
96
- <button className='border-0' onClick={() => handleReorder(index, 1)}>
97
- {'>'}
98
- </button>
99
- )}
100
- </li>
101
- )
102
- }
103
-
104
- const MultiConfigTabs = () => {
105
- const { config } = useContext(DashboardContext)
106
- const dispatch = useContext(DashboardDispatchContext)
107
- const tabs = useMemo<string[]>(
108
- () => (config.multiDashboards || []).map(({ label }) => label),
109
- [config.multiDashboards]
110
- )
111
- const activeTab = useMemo<number>(() => config.activeDashboard, [config.activeDashboard])
112
-
113
- const saveAndLoad = (indexToSwitchTo: number) => {
114
- dispatch({ type: 'SAVE_CURRENT_CHANGES' })
115
- dispatch({ type: 'SWITCH_CONFIG', payload: indexToSwitchTo })
116
- }
117
-
118
- if (!config.multiDashboards) return null
119
- return (
120
- <ul className='nav nav-tabs multi-config-tabs mb-4'>
121
- {tabs.map((tab, index) => (
122
- <Tab
123
- key={tab + index}
124
- name={tab}
125
- tabs={tabs}
126
- index={index}
127
- handleClick={() => saveAndLoad(index)}
128
- active={index === activeTab}
129
- />
130
- ))}
131
- <li className='nav-item'>
132
- <button className='nav-link add' onClick={() => dispatch({ type: 'ADD_NEW_DASHBOARD' })}>
133
- +
134
- </button>
135
- </li>
136
- </ul>
137
- )
138
- }
139
-
140
- export default MultiConfigTabs
1
+ import { createRef, useContext, useMemo, useState } from 'react'
2
+ import { DashboardContext, DashboardDispatchContext } from '../../DashboardContext'
3
+ import Modal from '@cdc/core/components/ui/Modal'
4
+ import { useGlobalContext } from '@cdc/core/components/GlobalContext'
5
+ import Button from '@cdc/core/components/elements/Button'
6
+ import './multiconfigtabs.styles.css'
7
+
8
+ const AreYouSure = deleteCallback => {
9
+ return (
10
+ <Modal>
11
+ <Modal.Content>
12
+ <p>Are you sure you want to delete this dashboard? </p>
13
+ <Button variant='danger' onClick={deleteCallback}>
14
+ DELETE
15
+ </Button>
16
+ </Modal.Content>
17
+ </Modal>
18
+ )
19
+ }
20
+
21
+ const Tab = ({ name, handleClick, tabs, index, active }) => {
22
+ const [editing, setEditing] = useState(false)
23
+ const dispatch = useContext(DashboardDispatchContext)
24
+ const { overlay } = useGlobalContext()
25
+ const inputRef = createRef<HTMLInputElement>()
26
+
27
+ const saveName = e => {
28
+ e.stopPropagation()
29
+ const newVal = inputRef.current.value
30
+ const sameName = newVal === name
31
+ const blankName = !newVal
32
+ const duplicateName = tabs.includes(newVal)
33
+ if (!sameName && !blankName && !duplicateName) {
34
+ dispatch({ type: 'RENAME_DASHBOARD_TAB', payload: { current: name, new: newVal } })
35
+ }
36
+ setEditing(false)
37
+ }
38
+
39
+ const onClick = e => {
40
+ // ignore click on delete button
41
+ if (e.target.className === 'remove') return
42
+ if (active) {
43
+ setEditing(true)
44
+ } else {
45
+ handleClick()
46
+ }
47
+ }
48
+
49
+ const handleRemove = () => {
50
+ const deleteCallback = () => {
51
+ dispatch({ type: 'REMOVE_MULTIDASHBOARD_AT_INDEX', payload: index })
52
+ overlay?.actions.toggleOverlay(false)
53
+ }
54
+ overlay?.actions.openOverlay(AreYouSure(deleteCallback))
55
+ }
56
+
57
+ const handleReorder = (index: number, moveTo: -1 | 1) => {
58
+ const newIndex = index + moveTo
59
+ const inbounds = newIndex > -1 && newIndex <= tabs.length - 1
60
+ if (inbounds) {
61
+ dispatch({ type: 'REORDER_MULTIDASHBOARDS', payload: { currentIndex: index, newIndex: index + moveTo } })
62
+ }
63
+ }
64
+
65
+ const canMoveLeft = index !== 0
66
+ const canMoveRight = index <= tabs.length - 2
67
+
68
+ return (
69
+ <li className='nav-item d-flex mt-0'>
70
+ {canMoveLeft && editing && (
71
+ <button className='border-0' onClick={() => handleReorder(index, -1)}>
72
+ {'<'}
73
+ </button>
74
+ )}
75
+ <div
76
+ className={`edit nav-link${active ? ' active' : ''}`}
77
+ aria-current={active ? 'page' : null}
78
+ onClick={onClick}
79
+ >
80
+ {editing ? (
81
+ <div className='d-flex'>
82
+ <input type='text' defaultValue={name} onBlur={saveName} ref={inputRef} />
83
+ <Button variant='link' className='save' onClick={saveName}>
84
+ save
85
+ </Button>
86
+ </div>
87
+ ) : (
88
+ <>
89
+ {name}
90
+ <Button variant='danger' className='border-0 ms-1' onClick={handleRemove}>
91
+ X
92
+ </Button>
93
+ </>
94
+ )}
95
+ </div>
96
+ {canMoveRight && editing && (
97
+ <button className='border-0' onClick={() => handleReorder(index, 1)}>
98
+ {'>'}
99
+ </button>
100
+ )}
101
+ </li>
102
+ )
103
+ }
104
+
105
+ const MultiConfigTabs = () => {
106
+ const { config } = useContext(DashboardContext)
107
+ const dispatch = useContext(DashboardDispatchContext)
108
+ const tabs = useMemo<string[]>(
109
+ () => (config.multiDashboards || []).map(({ label }) => label),
110
+ [config.multiDashboards]
111
+ )
112
+ const activeTab = useMemo<number>(() => config.activeDashboard, [config.activeDashboard])
113
+
114
+ const saveAndLoad = (indexToSwitchTo: number) => {
115
+ dispatch({ type: 'SAVE_CURRENT_CHANGES' })
116
+ dispatch({ type: 'SWITCH_CONFIG', payload: indexToSwitchTo })
117
+ }
118
+
119
+ if (!config.multiDashboards) return null
120
+ return (
121
+ <ul className='nav nav-tabs multi-config-tabs mb-4'>
122
+ {tabs.map((tab, index) => (
123
+ <Tab
124
+ key={tab + index}
125
+ name={tab}
126
+ tabs={tabs}
127
+ index={index}
128
+ handleClick={() => saveAndLoad(index)}
129
+ active={index === activeTab}
130
+ />
131
+ ))}
132
+ <li className='nav-item'>
133
+ <button className='nav-link add' onClick={() => dispatch({ type: 'ADD_NEW_DASHBOARD' })}>
134
+ +
135
+ </button>
136
+ </li>
137
+ </ul>
138
+ )
139
+ }
140
+
141
+ export default MultiConfigTabs
@@ -15,6 +15,7 @@ import ToggleIcon from '../images/icon-toggle.svg'
15
15
  import { ConfigRow } from '../types/ConfigRow'
16
16
  import { DataDesignerModal } from './DataDesignerModal'
17
17
  import { useGlobalContext } from '@cdc/core/components/GlobalContext'
18
+ import Button from '@cdc/core/components/elements/Button'
18
19
  import { iconHash } from '../helpers/iconHash'
19
20
  import _ from 'lodash'
20
21
  import { Visualization } from '@cdc/core/types/Visualization'
@@ -191,7 +192,7 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
191
192
  <nav className='row-menu'>
192
193
  <ul className='row-menu__flyout'>{layoutList}</ul>
193
194
  {isMultiColumn && (
194
- <button
195
+ <Button
195
196
  className={`btn row-menu__btn border-0${row.equalHeight ? ' btn-primary' : ''}`}
196
197
  title={row.equalHeight ? 'Disable Equal Height Rows' : 'Enable Equal Height Rows'}
197
198
  onClick={toggleEqualHeight}
@@ -201,33 +202,33 @@ const RowMenu: React.FC<RowMenuProps> = ({ rowIdx }) => {
201
202
  <rect x='14' y='2' width='9' height='14' rx='1' />
202
203
  <line x1='0' y1='19' x2='24' y2='19' stroke='#fff' strokeWidth='2' strokeDasharray='3 2' />
203
204
  </svg>
204
- </button>
205
+ </Button>
205
206
  )}
206
207
  <div className='spacer'></div>
207
- <button
208
+ <Button
208
209
  className={`btn btn-primary row-menu__btn border-0`}
209
210
  title='Move Row Up'
210
211
  onClick={() => moveRow('up')}
211
212
  disabled={rowIdx === 0}
212
213
  >
213
214
  <Icon display='caretUp' color='#fff' size={25} />
214
- </button>
215
- <button
215
+ </Button>
216
+ <Button
216
217
  className={'btn btn-primary row-menu__btn border-0'}
217
218
  title='Move Row Down'
218
219
  onClick={() => moveRow('down')}
219
220
  disabled={rowIdx + 1 === rows.length}
220
221
  >
221
222
  <Icon display='caretDown' color='#fff' size={25} />
222
- </button>
223
- <button
223
+ </Button>
224
+ <Button
224
225
  className={'btn btn-danger row-menu__btn row-menu__btn--remove border-0'}
225
226
  title='Delete Row'
226
227
  onClick={deleteRow}
227
228
  disabled={rowIdx === 0 && rows.length === 1}
228
229
  >
229
230
  <Icon display='close' color='#fff' size={25} />
230
- </button>
231
+ </Button>
231
232
  </nav>
232
233
  )
233
234
  }
@@ -241,7 +242,7 @@ const Row: React.FC<RowProps> = ({ row, idx: rowIdx, uuid }) => {
241
242
  <div className='builder-row' data-row-id={rowIdx}>
242
243
  <RowMenu rowIdx={rowIdx} />
243
244
  <span className='ms-2 mt-n3'>Row - {rowIdx + 1}</span>
244
- <button
245
+ <Button
245
246
  title='Configure Data'
246
247
  className='btn btn-configure-row'
247
248
  onClick={() => {
@@ -249,7 +250,7 @@ const Row: React.FC<RowProps> = ({ row, idx: rowIdx, uuid }) => {
249
250
  }}
250
251
  >
251
252
  {iconHash['gearMulti']}
252
- </button>
253
+ </Button>
253
254
  <div className='column-container'>
254
255
  {row.columns
255
256
  .filter(column => column.width)
@@ -1,5 +1,5 @@
1
1
  import DataTableStandAlone from '@cdc/core/components/DataTable/DataTableStandAlone'
2
- import React, { useContext, useEffect, useMemo, useState } from 'react'
2
+ import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
3
3
  import Toggle from './Toggle'
4
4
  import cloneDeep from 'lodash/cloneDeep'
5
5
  import { ConfigRow } from '../types/ConfigRow'
@@ -88,6 +88,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
88
88
  }) => {
89
89
  const { config, filteredData: dashboardFilteredData, data: rawData } = useContext(DashboardContext)
90
90
  const [toggledRow, setToggled] = React.useState<number>(0)
91
+ const rowRef = useRef<HTMLDivElement>(null)
91
92
 
92
93
  useEffect(() => {
93
94
  if (row.toggle) setToggled(0)
@@ -103,15 +104,18 @@ const VisualizationRow: React.FC<VizRowProps> = ({
103
104
  }
104
105
  }, [toggledRow, row.toggle])
105
106
 
106
- const setupTP5MinHeightEqualizer = (rowElement: Element, itemSelector: string) => {
107
- const items = Array.from(rowElement.querySelectorAll(itemSelector)) as HTMLElement[]
108
- if (items.length <= 1) return undefined
107
+ const setupMinHeightEqualizer = (rowElement: HTMLElement, selectors: string[]) => {
108
+ let items: HTMLElement[] = []
109
+ const selector = selectors.join(', ')
110
+ const resizeObserver = new ResizeObserver(() => equalizeHeights())
109
111
 
110
112
  const equalizeHeights = () => {
111
113
  items.forEach(item => {
112
114
  item.style.minHeight = ''
113
115
  })
114
116
 
117
+ if (items.length <= 1) return
118
+
115
119
  let maxHeight = 0
116
120
  items.forEach(item => {
117
121
  const height = item.offsetHeight
@@ -125,42 +129,86 @@ const VisualizationRow: React.FC<VizRowProps> = ({
125
129
  }
126
130
  }
127
131
 
128
- equalizeHeights()
132
+ const refreshItems = () => {
133
+ const nextItems = Array.from(rowElement.querySelectorAll(selector)) as HTMLElement[]
134
+ const nextSet = new Set(nextItems)
135
+
136
+ items.forEach(item => {
137
+ if (!nextSet.has(item)) {
138
+ resizeObserver.unobserve(item)
139
+ item.style.minHeight = ''
140
+ }
141
+ })
142
+
143
+ nextItems.forEach(item => {
144
+ if (!items.includes(item)) {
145
+ resizeObserver.observe(item)
146
+ }
147
+ })
129
148
 
130
- const resizeObserver = new ResizeObserver(() => {
149
+ items = nextItems
131
150
  equalizeHeights()
132
- })
151
+ }
133
152
 
134
- items.forEach(item => {
135
- resizeObserver.observe(item)
153
+ const mutationObserver = new MutationObserver(() => {
154
+ refreshItems()
136
155
  })
137
156
 
138
- return () => resizeObserver.disconnect()
157
+ mutationObserver.observe(rowElement, { childList: true, subtree: true })
158
+ refreshItems()
159
+
160
+ return () => {
161
+ mutationObserver.disconnect()
162
+ resizeObserver.disconnect()
163
+ items.forEach(item => {
164
+ item.style.minHeight = ''
165
+ })
166
+ }
139
167
  }
140
168
 
141
- // Equalize TP5 callout title heights and TP5 gauge message blocks for like visualizations in the same row
169
+ const tp5CountInRow = row.columns.reduce((count, col) => {
170
+ if (!col.widget) return count
171
+ const viz = config.visualizations[col.widget]
172
+ if (!viz) return count
173
+
174
+ const isTp5DataBite = viz.type === 'data-bite' && (viz as any).biteStyle === 'tp5'
175
+ const isTp5WaffleOrGauge =
176
+ viz.type === 'waffle-chart' && (viz.visualizationType === 'TP5 Waffle' || viz.visualizationType === 'TP5 Gauge')
177
+ const isTp5MarkupInclude = viz.type === 'markup-include' && (viz as any).contentEditor?.style === 'tp5'
178
+
179
+ return isTp5DataBite || isTp5WaffleOrGauge || isTp5MarkupInclude ? count + 1 : count
180
+ }, 0)
181
+ const needsTP5AutoEqualization = tp5CountInRow > 1
182
+ const shouldEqualizeRow = !!row.equalHeight || needsTP5AutoEqualization
183
+
184
+ // Layer TP5 equalization for row-level title consistency and same-type internals.
142
185
  useEffect(() => {
143
- if (!row.equalHeight) return
186
+ if (!shouldEqualizeRow) return
144
187
 
145
- const rowElement = document.querySelector(`[data-row-index="${index}"]`)
188
+ const rowElement = rowRef.current
146
189
  if (!rowElement) return
147
190
 
148
191
  const cleanups = [
149
- setupTP5MinHeightEqualizer(rowElement, '.bite__style--tp5 .cdc-callout__heading'),
150
- setupTP5MinHeightEqualizer(rowElement, '.waffle__style--tp5 .cdc-callout__heading'),
151
- setupTP5MinHeightEqualizer(rowElement, '.gauge__style--tp5 .cdc-callout__heading'),
152
- setupTP5MinHeightEqualizer(rowElement, '.gauge__style--tp5 .cove-gauge-chart__content')
153
- ].filter(Boolean) as Array<() => void>
192
+ // Cross-type TP5 title alignment in the row.
193
+ setupMinHeightEqualizer(rowElement, [
194
+ '.bite__style--tp5 .cdc-callout__heading',
195
+ '.waffle__style--tp5 .cdc-callout__heading',
196
+ '.gauge__style--tp5 .cdc-callout__heading',
197
+ '.markup-include__style--tp5 .cdc-callout__heading'
198
+ ]),
199
+ // Same-type gauge internals alignment so gauge meters line up despite content variance.
200
+ setupMinHeightEqualizer(rowElement, ['.gauge__style--tp5 .cove-gauge-chart__content'])
201
+ ]
154
202
 
155
203
  return () => {
156
204
  cleanups.forEach(cleanup => cleanup())
157
205
  }
158
- }, [index, row, config, filteredDataOverride])
206
+ }, [shouldEqualizeRow, row.columns, config.activeDashboard, filteredDataOverride, dashboardFilteredData[index]])
159
207
 
160
208
  const isFilterRow = row.columns.some(
161
209
  col => col.widget && config.visualizations[col.widget]?.type === 'dashboardFilters'
162
210
  )
163
- const needsEqualHeight = !!row.equalHeight && !isFilterRow
211
+ const needsEqualHeight = shouldEqualizeRow && !isFilterRow
164
212
 
165
213
  const show = useMemo(() => {
166
214
  if (row.toggle) {
@@ -236,6 +284,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
236
284
 
237
285
  return (
238
286
  <div
287
+ ref={rowRef}
239
288
  className={`row${needsEqualHeight ? ' equal-height' : ''}${row.toggle ? ' toggle' : ''}`}
240
289
  key={`row__${index}`}
241
290
  data-row-index={index}
@@ -355,6 +404,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
355
404
  <CdcDataBite
356
405
  key={col.widget}
357
406
  config={visualizationConfig}
407
+ rawData={rawData?.[visualizationConfig.dataKey] || []}
358
408
  setConfig={newConfig => {
359
409
  updateChildConfig(col.widget, newConfig)
360
410
  }}
@@ -367,6 +417,7 @@ const VisualizationRow: React.FC<VizRowProps> = ({
367
417
  <CdcWaffleChart
368
418
  key={col.widget}
369
419
  config={visualizationConfig}
420
+ rawData={rawData?.[visualizationConfig.dataKey] || []}
370
421
  setConfig={newConfig => {
371
422
  updateChildConfig(col.widget, newConfig)
372
423
  }}
@@ -43,7 +43,7 @@ const VisualizationsPanel = () => {
43
43
  <span className='subheading-3'>Misc.</span>
44
44
  <div className='drag-grid'>
45
45
  <Widget addVisualization={() => addVisualization('data-bite', '')} type='data-bite' />
46
- <Widget addVisualization={() => addVisualization('waffle-chart', '')} type='waffle-chart' />
46
+ <Widget addVisualization={() => addVisualization('waffle-chart', 'Waffle')} type='waffle-chart' />
47
47
  <Widget addVisualization={() => addVisualization('markup-include', '')} type='markup-include' />
48
48
  <Widget addVisualization={() => addVisualization('filtered-text', '')} type='filtered-text' />
49
49
  <Widget addVisualization={() => addVisualization('dashboardFilters', '')} type='dashboardFilters' />
@@ -5,6 +5,7 @@ import { DashboardContext, DashboardDispatchContext } from '../../DashboardConte
5
5
  import { DataTransform } from '@cdc/core/helpers/DataTransform'
6
6
  import fetchRemoteData from '@cdc/core/helpers/fetchRemoteData'
7
7
  import Icon from '@cdc/core/components/ui/Icon'
8
+ import Button from '@cdc/core/components/elements/Button'
8
9
  import { AnyVisualization } from '@cdc/core/types/Visualization'
9
10
  import { iconHash } from '../../helpers/iconHash'
10
11
  import _ from 'lodash'
@@ -167,12 +168,12 @@ const Widget = ({
167
168
  {widgetConfig?.rowIdx !== undefined && (
168
169
  <div className='widget-menu'>
169
170
  {isConfigurationReady && (
170
- <button title='Configure Visualization' className='btn btn-configure' onClick={editWidget}>
171
+ <Button title='Configure Visualization' className='btn btn-configure' onClick={editWidget}>
171
172
  {iconHash['tools']}
172
- </button>
173
+ </Button>
173
174
  )}
174
175
  {needsDataConfiguration && (
175
- <button
176
+ <Button
176
177
  title='Configure Data'
177
178
  className='btn btn-configure'
178
179
  onClick={() => {
@@ -182,7 +183,7 @@ const Widget = ({
182
183
  }}
183
184
  >
184
185
  {iconHash['gear']}
185
- </button>
186
+ </Button>
186
187
  )}
187
188
  <div className='widget-menu-item' onClick={deleteWidget}>
188
189
  <Icon display='close' base />
@@ -33,25 +33,56 @@
33
33
  .widget-menu {
34
34
  align-items: center;
35
35
  display: flex;
36
- justify-content: space-between;
36
+ gap: 6px;
37
+ justify-content: flex-end;
38
+ padding: 2px;
37
39
  }
38
40
 
39
- .btn-configure {
40
- background: none;
41
- height: 20px;
42
- margin: 0 5px;
43
- padding: 0;
44
- width: 20px;
41
+ .widget-menu .cove-button.btn-configure {
42
+ align-items: center;
43
+ background: transparent !important;
44
+ border: 0;
45
+ border-radius: 0;
46
+ box-shadow: none;
47
+ color: var(--mediumGray);
48
+ display: inline-flex;
49
+ height: 28px;
50
+ justify-content: center;
51
+ margin: 0;
52
+ min-height: 28px;
53
+ padding: 4px;
54
+ transform: none;
55
+ width: 28px;
56
+ }
57
+
58
+ .widget-menu .cove-button.btn-configure:hover:not(:disabled),
59
+ .widget-menu .cove-button.btn-configure:active:not(:disabled) {
60
+ background: transparent !important;
61
+ box-shadow: none;
62
+ transform: none;
63
+ }
64
+
65
+ .widget-menu .cove-button.btn-configure svg {
66
+ margin-bottom: 0;
67
+ top: 0;
68
+ vertical-align: middle;
45
69
  }
46
70
 
47
71
  .widget-menu-item {
72
+ align-items: center;
48
73
  cursor: pointer;
49
- display: block;
50
- height: 20px;
74
+ display: inline-flex;
75
+ height: 28px;
76
+ justify-content: center;
77
+ line-height: 1;
78
+ padding: 4px;
51
79
  user-select: none;
52
- width: 20px;
80
+ width: 28px;
53
81
  }
54
82
 
55
83
  .widget-menu-item svg {
56
84
  fill: var(--mediumGray);
85
+ margin-bottom: 0;
86
+ top: 0;
87
+ vertical-align: middle;
57
88
  }
@@ -10,6 +10,7 @@ export default {
10
10
  label: 'Data Table',
11
11
  show: true,
12
12
  showDownloadUrl: false,
13
+ downloadUrlLabel: '',
13
14
  showDownloadLinkBelow: true,
14
15
  showVertical: true
15
16
  }
@@ -35,10 +35,12 @@ export const addVisualization = (type, subType) => {
35
35
  }
36
36
  break
37
37
  case 'data-bite':
38
- case 'waffle-chart':
39
38
  case 'filtered-text':
40
39
  newVisualizationConfig.visualizationType = type
41
40
  break
41
+ case 'waffle-chart':
42
+ newVisualizationConfig.visualizationType = subType
43
+ break
42
44
  case 'table': {
43
45
  const tableConfig: Table = {
44
46
  label: 'Data Table',