@cdc/editor 4.26.3 → 4.26.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.
- package/dist/cdceditor-CY9IcPSi.es.js +6 -0
- package/dist/cdceditor-DlpiY3fQ.es.js +4 -0
- package/dist/cdceditor.js +78238 -74548
- package/example/private/dashboard-filter-issue/dashboard-filter-issue.json +957 -0
- package/package.json +9 -9
- package/src/_stories/Editor.stories.tsx +124 -0
- package/src/assets.d.ts +4 -0
- package/src/components/ChooseTab.test.tsx +64 -1
- package/src/components/ChooseTab.tsx +82 -4
- package/src/components/DataImport/components/DataImport.tsx +135 -50
- package/src/components/DataImport/components/SampleData.test.tsx +16 -0
- package/src/components/DataImport/components/SampleData.tsx +6 -0
- package/src/components/DataImport/components/samples/valid-heatmap-varicella-cases.csv +13 -0
- package/src/components/DataImport/helpers/applyAutoDetectedDateParseFormat.ts +35 -0
- package/src/components/DataImport/tests/applyAutoDetectedDateParseFormat.test.ts +128 -0
- package/src/components/PreviewDataTable.test.tsx +184 -0
- package/src/components/PreviewDataTable.tsx +18 -7
- package/src/components/modal/Confirmation.jsx +5 -4
- package/src/scss/choose-vis-tab.scss +7 -1
- package/src/scss/main.scss +14 -6
- package/dist/cdceditor-vr9HZwRt.es.js +0 -6
- package/example/data-horizontal-filters.json +0 -8
- package/example/data-horizontal-multiseries-filters.json +0 -18
- package/example/data-horizontal-multiseries.json +0 -6
- package/example/data-horizontal.json +0 -4
- package/example/data-vertical-filters.json +0 -10
- package/example/data-vertical-multiseries-filters.json +0 -18
- package/example/data-vertical-multiseries-multirow-filters.json +0 -50
- package/example/data-vertical-multiseries-multirow.json +0 -14
- package/example/data-vertical-multiseries.json +0 -6
- package/example/data-vertical.json +0 -6
- package/example/region-map.json +0 -33
- package/example/test.json +0 -110280
- package/example/valid-county-data.json +0 -3049
- package/example/valid-scatterplot.csv +0 -17
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cdc/editor",
|
|
3
|
-
"version": "4.26.
|
|
3
|
+
"version": "4.26.5",
|
|
4
4
|
"description": "React component for generating a new component entry",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bugs": "https://github.com/CDCgov/cdc-open-viz/issues",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@cdc/chart": "^4.26.
|
|
9
|
-
"@cdc/core": "^4.26.
|
|
10
|
-
"@cdc/dashboard": "^4.26.
|
|
11
|
-
"@cdc/data-bite": "^4.26.
|
|
12
|
-
"@cdc/map": "^4.26.
|
|
13
|
-
"@cdc/markup-include": "^4.26.
|
|
14
|
-
"@cdc/waffle-chart": "^4.26.
|
|
8
|
+
"@cdc/chart": "^4.26.5",
|
|
9
|
+
"@cdc/core": "^4.26.5",
|
|
10
|
+
"@cdc/dashboard": "^4.26.5",
|
|
11
|
+
"@cdc/data-bite": "^4.26.5",
|
|
12
|
+
"@cdc/map": "^4.26.5",
|
|
13
|
+
"@cdc/markup-include": "^4.26.5",
|
|
14
|
+
"@cdc/waffle-chart": "^4.26.5",
|
|
15
15
|
"axios": "^1.13.2",
|
|
16
16
|
"d3": "^7.9.0",
|
|
17
17
|
"react-dropzone": "^14.3.8",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"vite-plugin-css-injected-by-js": "^2.4.0",
|
|
24
24
|
"vite-plugin-svgr": "^4.2.0"
|
|
25
25
|
},
|
|
26
|
-
"gitHead": "
|
|
26
|
+
"gitHead": "61c025165d96b45a6002c34582c5a622a9d865a9",
|
|
27
27
|
"main": "dist/cdceditor",
|
|
28
28
|
"moduleName": "CdcEditor",
|
|
29
29
|
"peerDependencies": {
|
|
@@ -103,6 +103,130 @@ export const LoadDataTableJsonConfig: Story = {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
export const DownloadDashboardDatasetCSV: Story = {
|
|
107
|
+
args: { config: {} },
|
|
108
|
+
play: async ({ canvasElement }) => {
|
|
109
|
+
const canvas = within(canvasElement)
|
|
110
|
+
const user = userEvent.setup()
|
|
111
|
+
|
|
112
|
+
await loadConfigFromTextArea(canvasElement, DashboardConfig)
|
|
113
|
+
await user.click(canvas.getByText('2. Import Data'))
|
|
114
|
+
await expect(canvas.findByText('Data Sources')).resolves.toBeTruthy()
|
|
115
|
+
|
|
116
|
+
const originalCreateObjectURL = URL.createObjectURL
|
|
117
|
+
let capturedBlob: Blob | null = null
|
|
118
|
+
URL.createObjectURL = (blob: Blob) => {
|
|
119
|
+
capturedBlob = blob
|
|
120
|
+
return 'blob:test-mock'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const downloadBtn = await canvas.findByRole('button', { name: 'Download' })
|
|
125
|
+
await user.click(downloadBtn)
|
|
126
|
+
|
|
127
|
+
expect(capturedBlob).toBeTruthy()
|
|
128
|
+
const text = await capturedBlob!.text()
|
|
129
|
+
expect(text).toContain('Location')
|
|
130
|
+
expect(text).toContain('Rate')
|
|
131
|
+
} finally {
|
|
132
|
+
URL.createObjectURL = originalCreateObjectURL
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const DownloadSingleVizCSV: Story = {
|
|
138
|
+
args: { config: {} },
|
|
139
|
+
play: async ({ canvasElement }) => {
|
|
140
|
+
const canvas = within(canvasElement)
|
|
141
|
+
const user = userEvent.setup()
|
|
142
|
+
|
|
143
|
+
await loadConfigFromTextArea(canvasElement, ChartEditorConfig)
|
|
144
|
+
await user.click(canvas.getByText('2. Import Data'))
|
|
145
|
+
await expect(canvas.findByText('Data Preview')).resolves.toBeTruthy()
|
|
146
|
+
|
|
147
|
+
const originalCreateObjectURL = URL.createObjectURL
|
|
148
|
+
let capturedBlob: Blob | null = null
|
|
149
|
+
URL.createObjectURL = (blob: Blob) => {
|
|
150
|
+
capturedBlob = blob
|
|
151
|
+
return 'blob:test-mock'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const downloadBtn = await canvas.findByRole('button', { name: 'Download CSV' })
|
|
156
|
+
await user.click(downloadBtn)
|
|
157
|
+
|
|
158
|
+
expect(capturedBlob).toBeTruthy()
|
|
159
|
+
const text = await capturedBlob!.text()
|
|
160
|
+
expect(text).toContain('Year')
|
|
161
|
+
expect(text).toContain('Category')
|
|
162
|
+
} finally {
|
|
163
|
+
URL.createObjectURL = originalCreateObjectURL
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const LoadFromApiUrlPreview: Story = {
|
|
169
|
+
args: { config: {} },
|
|
170
|
+
play: async ({ canvasElement }) => {
|
|
171
|
+
const canvas = within(canvasElement)
|
|
172
|
+
const user = userEvent.setup()
|
|
173
|
+
|
|
174
|
+
// Intercept fetch and return JSON with a charset in the Content-Type header.
|
|
175
|
+
// This exercises the MIME type normalisation fix: fetch preserves the full
|
|
176
|
+
// Content-Type value (e.g. 'application/json; charset=utf-8') while the old
|
|
177
|
+
// axios code would strip parameters, so the blob re-typing comparison
|
|
178
|
+
// must use the base type before the semicolon.
|
|
179
|
+
const mockData = [
|
|
180
|
+
{ state: 'Alabama', value: '42' },
|
|
181
|
+
{ state: 'Alaska', value: '37' },
|
|
182
|
+
{ state: 'Arizona', value: '55' }
|
|
183
|
+
]
|
|
184
|
+
const mockBlob = new Blob([JSON.stringify(mockData)], { type: 'application/json; charset=utf-8' })
|
|
185
|
+
const originalFetch = window.fetch
|
|
186
|
+
window.fetch = () => Promise.resolve({ ok: true, blob: () => Promise.resolve(mockBlob) } as Response)
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// Select Dashboard so the dataset name becomes meaningful and the
|
|
190
|
+
// multi-dataset import flow is exercised
|
|
191
|
+
await user.click(await canvas.findByRole('button', { name: 'Dashboard' }))
|
|
192
|
+
|
|
193
|
+
await user.click(canvas.getByText('2. Import Data'))
|
|
194
|
+
|
|
195
|
+
// Switch to the URL tab
|
|
196
|
+
await user.click(await canvas.findByText('Load from URL'))
|
|
197
|
+
|
|
198
|
+
// Both fields are required before the button is enabled
|
|
199
|
+
const nameInput = await canvas.findByLabelText('Enter Dataset Name')
|
|
200
|
+
await user.type(nameInput, 'api-data')
|
|
201
|
+
|
|
202
|
+
const urlInput = await canvas.findByLabelText('Load data from external URL')
|
|
203
|
+
await user.type(urlInput, 'https://example.gov/api/data.json')
|
|
204
|
+
|
|
205
|
+
await user.click(await canvas.findByRole('button', { name: 'Save & Load' }))
|
|
206
|
+
|
|
207
|
+
// After a successful dashboard dataset load the Data Sources table appears
|
|
208
|
+
// and the preview panel should auto-populate (dataset is created with preview: true)
|
|
209
|
+
await expect(canvas.findByText('Data Sources')).resolves.toBeTruthy()
|
|
210
|
+
await expect(canvas.findByText('Data Preview')).resolves.toBeTruthy()
|
|
211
|
+
await expect(canvas.findByText('state')).resolves.toBeTruthy()
|
|
212
|
+
await expect(canvas.findByText('Alabama')).resolves.toBeTruthy()
|
|
213
|
+
|
|
214
|
+
// Navigate away to tab 3 then back to tab 2 — the dataset must survive the round-trip
|
|
215
|
+
await new Promise(r => setTimeout(r, 1500))
|
|
216
|
+
await user.click(canvas.getByText('3. Configure'))
|
|
217
|
+
await new Promise(r => setTimeout(r, 500))
|
|
218
|
+
await user.click(canvas.getByText('2. Import Data'))
|
|
219
|
+
await new Promise(r => setTimeout(r, 500))
|
|
220
|
+
|
|
221
|
+
await expect(canvas.findByText('Data Sources')).resolves.toBeTruthy()
|
|
222
|
+
await expect(canvas.findByText('Data Preview')).resolves.toBeTruthy()
|
|
223
|
+
await expect(canvas.findByText('Alabama')).resolves.toBeTruthy()
|
|
224
|
+
} finally {
|
|
225
|
+
window.fetch = originalFetch
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
106
230
|
export const InvalidJsonShowsValidationAlert: Story = {
|
|
107
231
|
args: {
|
|
108
232
|
config: {}
|
package/src/assets.d.ts
ADDED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { render, waitFor } from '@testing-library/react'
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
3
3
|
|
|
4
4
|
import ConfigContext, { EditorDispatchContext } from '@cdc/core/contexts/EditorContext'
|
|
5
5
|
import ChooseTab from './ChooseTab'
|
|
@@ -33,4 +33,67 @@ describe('ChooseTab', () => {
|
|
|
33
33
|
expect(dispatch).toHaveBeenCalledWith({ type: 'EDITOR_SAVE', payload: tempConfig })
|
|
34
34
|
})
|
|
35
35
|
})
|
|
36
|
+
|
|
37
|
+
it('creates a HeatMap starter config when the HeatMap button is selected', () => {
|
|
38
|
+
const dispatch = vi.fn()
|
|
39
|
+
|
|
40
|
+
render(
|
|
41
|
+
<ConfigContext.Provider
|
|
42
|
+
value={
|
|
43
|
+
{
|
|
44
|
+
config: { type: 'chart' },
|
|
45
|
+
tempConfig: null,
|
|
46
|
+
errors: [],
|
|
47
|
+
currentViewport: 'lg',
|
|
48
|
+
globalActive: 0,
|
|
49
|
+
setTempConfig: vi.fn()
|
|
50
|
+
} as any
|
|
51
|
+
}
|
|
52
|
+
>
|
|
53
|
+
<EditorDispatchContext.Provider value={dispatch}>
|
|
54
|
+
<ChooseTab />
|
|
55
|
+
</EditorDispatchContext.Provider>
|
|
56
|
+
</ConfigContext.Provider>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
expect(
|
|
60
|
+
screen.getByRole('button', { name: 'HeatMap' }).querySelector('.choose-vis__heatmap-icon')
|
|
61
|
+
).toBeInTheDocument()
|
|
62
|
+
|
|
63
|
+
fireEvent.click(screen.getByRole('button', { name: 'HeatMap' }))
|
|
64
|
+
|
|
65
|
+
expect(dispatch).toHaveBeenCalledWith(
|
|
66
|
+
expect.objectContaining({
|
|
67
|
+
type: 'EDITOR_SET_CONFIG',
|
|
68
|
+
payload: expect.objectContaining({
|
|
69
|
+
visualizationType: 'HeatMap',
|
|
70
|
+
type: 'chart',
|
|
71
|
+
title: 'Synthetic Varicella Cases by HHS Region',
|
|
72
|
+
xAxis: expect.objectContaining({
|
|
73
|
+
dataKey: 'Month',
|
|
74
|
+
label: 'Month'
|
|
75
|
+
}),
|
|
76
|
+
yAxis: expect.objectContaining({
|
|
77
|
+
label: 'HHS Region',
|
|
78
|
+
titlePlacement: 'side'
|
|
79
|
+
}),
|
|
80
|
+
series: expect.arrayContaining([
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
dataKey: 'HHS Region 1',
|
|
83
|
+
name: 'Region 1',
|
|
84
|
+
type: 'HeatMap'
|
|
85
|
+
})
|
|
86
|
+
]),
|
|
87
|
+
heatmap: expect.objectContaining({
|
|
88
|
+
cellPadding: 2
|
|
89
|
+
}),
|
|
90
|
+
legend: expect.objectContaining({
|
|
91
|
+
position: 'top',
|
|
92
|
+
style: 'gradient',
|
|
93
|
+
label: 'Reported cases'
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
)
|
|
98
|
+
})
|
|
36
99
|
})
|
|
@@ -3,6 +3,7 @@ import '../scss/choose-vis-tab.scss'
|
|
|
3
3
|
|
|
4
4
|
import ConfigContext, { EditorDispatchContext } from '@cdc/core/contexts/EditorContext'
|
|
5
5
|
import Tooltip from '@cdc/core/components/ui/Tooltip'
|
|
6
|
+
import Button from '@cdc/core/components/elements/Button'
|
|
6
7
|
|
|
7
8
|
import AlabamaGraphic from '@cdc/core/assets/icon-map-alabama.svg'
|
|
8
9
|
import AreaChartIcon from '@cdc/core/assets/icon-area-chart.svg'
|
|
@@ -17,6 +18,7 @@ import EpiChartIcon from '@cdc/core/assets/icon-epi-chart.svg'
|
|
|
17
18
|
import ForecastIcon from '@cdc/core/assets/icon-chart-forecast.svg'
|
|
18
19
|
import GaugeChartIcon from '@cdc/core/assets/icon-linear-gauge.svg'
|
|
19
20
|
import GlobeIcon from '@cdc/core/assets/icon-map-world.svg'
|
|
21
|
+
import HeatMapIconSrc from '@cdc/core/assets/icon-heatmap.png'
|
|
20
22
|
import HorizonChartIcon from '@cdc/core/assets/icon-chart-area.svg'
|
|
21
23
|
import HorizontalStackIcon from '@cdc/core/assets/icon-chart-bar-stacked.svg'
|
|
22
24
|
import Icon from '@cdc/core/components/ui/Icon'
|
|
@@ -68,6 +70,11 @@ const VizButton: React.FC<VizButtonProps> = ({ activeVizButtonID, onConfigure, .
|
|
|
68
70
|
)
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
const HeatMapIcon = () => <img className='choose-vis__heatmap-icon' src={HeatMapIconSrc} alt='' aria-hidden='true' />
|
|
74
|
+
|
|
75
|
+
const heatMapRegionColumns = Array.from({ length: 10 }, (_, index) => `HHS Region ${index + 1}`)
|
|
76
|
+
const heatMapSampleColumns = ['Month', ...heatMapRegionColumns]
|
|
77
|
+
|
|
71
78
|
const ChooseTab: React.FC = (): JSX.Element => {
|
|
72
79
|
const { config, tempConfig } = useContext(ConfigContext)
|
|
73
80
|
|
|
@@ -274,15 +281,16 @@ const ChooseTab: React.FC = (): JSX.Element => {
|
|
|
274
281
|
placeholder='{ }'
|
|
275
282
|
value={pastedConfig}
|
|
276
283
|
/>
|
|
277
|
-
<
|
|
278
|
-
|
|
284
|
+
<Button
|
|
285
|
+
variant='primary'
|
|
286
|
+
className='px-4 ms-2'
|
|
279
287
|
type='submit'
|
|
280
288
|
id='load-data'
|
|
281
289
|
disabled={!pastedConfig}
|
|
282
290
|
onClick={() => importConfig(pastedConfig)}
|
|
283
291
|
>
|
|
284
292
|
Load
|
|
285
|
-
</
|
|
293
|
+
</Button>
|
|
286
294
|
</div>
|
|
287
295
|
</div>
|
|
288
296
|
</div>
|
|
@@ -439,6 +447,76 @@ const buttons = [
|
|
|
439
447
|
icon: <ForecastIcon />,
|
|
440
448
|
content: 'Display a forecasting chart to predict future data trends.'
|
|
441
449
|
},
|
|
450
|
+
{
|
|
451
|
+
id: 28,
|
|
452
|
+
category: 'Charts',
|
|
453
|
+
label: 'HeatMap',
|
|
454
|
+
type: 'chart',
|
|
455
|
+
subType: 'HeatMap',
|
|
456
|
+
title: 'Synthetic Varicella Cases by HHS Region',
|
|
457
|
+
showTitle: true,
|
|
458
|
+
description: 'Example data are synthetic and for demonstration only.',
|
|
459
|
+
orientation: 'vertical',
|
|
460
|
+
xAxis: {
|
|
461
|
+
type: 'categorical',
|
|
462
|
+
dataKey: 'Month',
|
|
463
|
+
label: 'Month',
|
|
464
|
+
size: 75,
|
|
465
|
+
maxTickRotation: 0,
|
|
466
|
+
tickRotation: 0,
|
|
467
|
+
labelOffset: 0
|
|
468
|
+
},
|
|
469
|
+
yAxis: {
|
|
470
|
+
type: 'categorical',
|
|
471
|
+
label: 'HHS Region',
|
|
472
|
+
size: 120,
|
|
473
|
+
titlePlacement: 'side'
|
|
474
|
+
},
|
|
475
|
+
heatmap: {
|
|
476
|
+
cellPadding: 2,
|
|
477
|
+
rowLabelGap: 32,
|
|
478
|
+
columnLabelGap: 48,
|
|
479
|
+
xAxisPosition: 'top',
|
|
480
|
+
showCellValues: false
|
|
481
|
+
},
|
|
482
|
+
series: heatMapRegionColumns.map((region, index) => ({
|
|
483
|
+
dataKey: region,
|
|
484
|
+
name: `Region ${index + 1}`,
|
|
485
|
+
type: 'HeatMap',
|
|
486
|
+
axis: 'Left',
|
|
487
|
+
tooltip: true
|
|
488
|
+
})),
|
|
489
|
+
columns: heatMapSampleColumns.reduce(
|
|
490
|
+
(columns, columnName) => ({
|
|
491
|
+
...columns,
|
|
492
|
+
[columnName]: {
|
|
493
|
+
name: columnName,
|
|
494
|
+
label: columnName.replace('HHS ', ''),
|
|
495
|
+
dataTable: true
|
|
496
|
+
}
|
|
497
|
+
}),
|
|
498
|
+
{}
|
|
499
|
+
),
|
|
500
|
+
dataFormat: {
|
|
501
|
+
commas: true,
|
|
502
|
+
roundTo: 0
|
|
503
|
+
},
|
|
504
|
+
general: {
|
|
505
|
+
palette: {
|
|
506
|
+
isReversed: false,
|
|
507
|
+
version: '2.0',
|
|
508
|
+
name: 'sequential_blue'
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
legend: {
|
|
512
|
+
position: 'top',
|
|
513
|
+
style: 'gradient',
|
|
514
|
+
subStyle: 'smooth',
|
|
515
|
+
label: 'Reported cases'
|
|
516
|
+
},
|
|
517
|
+
icon: <HeatMapIcon />,
|
|
518
|
+
content: 'Display a heatmap to compare intensity across two dimensions.'
|
|
519
|
+
},
|
|
442
520
|
{
|
|
443
521
|
id: 27,
|
|
444
522
|
category: 'Charts',
|
|
@@ -491,7 +569,7 @@ const buttons = [
|
|
|
491
569
|
content: 'Present the numerical proportions of a data series.'
|
|
492
570
|
},
|
|
493
571
|
{
|
|
494
|
-
id:
|
|
572
|
+
id: 29,
|
|
495
573
|
category: 'Charts',
|
|
496
574
|
label: 'Radar',
|
|
497
575
|
type: 'chart',
|