@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.
Files changed (35) hide show
  1. package/dist/cdceditor-CY9IcPSi.es.js +6 -0
  2. package/dist/cdceditor-DlpiY3fQ.es.js +4 -0
  3. package/dist/cdceditor.js +78238 -74548
  4. package/example/private/dashboard-filter-issue/dashboard-filter-issue.json +957 -0
  5. package/package.json +9 -9
  6. package/src/_stories/Editor.stories.tsx +124 -0
  7. package/src/assets.d.ts +4 -0
  8. package/src/components/ChooseTab.test.tsx +64 -1
  9. package/src/components/ChooseTab.tsx +82 -4
  10. package/src/components/DataImport/components/DataImport.tsx +135 -50
  11. package/src/components/DataImport/components/SampleData.test.tsx +16 -0
  12. package/src/components/DataImport/components/SampleData.tsx +6 -0
  13. package/src/components/DataImport/components/samples/valid-heatmap-varicella-cases.csv +13 -0
  14. package/src/components/DataImport/helpers/applyAutoDetectedDateParseFormat.ts +35 -0
  15. package/src/components/DataImport/tests/applyAutoDetectedDateParseFormat.test.ts +128 -0
  16. package/src/components/PreviewDataTable.test.tsx +184 -0
  17. package/src/components/PreviewDataTable.tsx +18 -7
  18. package/src/components/modal/Confirmation.jsx +5 -4
  19. package/src/scss/choose-vis-tab.scss +7 -1
  20. package/src/scss/main.scss +14 -6
  21. package/dist/cdceditor-vr9HZwRt.es.js +0 -6
  22. package/example/data-horizontal-filters.json +0 -8
  23. package/example/data-horizontal-multiseries-filters.json +0 -18
  24. package/example/data-horizontal-multiseries.json +0 -6
  25. package/example/data-horizontal.json +0 -4
  26. package/example/data-vertical-filters.json +0 -10
  27. package/example/data-vertical-multiseries-filters.json +0 -18
  28. package/example/data-vertical-multiseries-multirow-filters.json +0 -50
  29. package/example/data-vertical-multiseries-multirow.json +0 -14
  30. package/example/data-vertical-multiseries.json +0 -6
  31. package/example/data-vertical.json +0 -6
  32. package/example/region-map.json +0 -33
  33. package/example/test.json +0 -110280
  34. package/example/valid-county-data.json +0 -3049
  35. 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",
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.3",
9
- "@cdc/core": "^4.26.3",
10
- "@cdc/dashboard": "^4.26.3",
11
- "@cdc/data-bite": "^4.26.3",
12
- "@cdc/map": "^4.26.3",
13
- "@cdc/markup-include": "^4.26.3",
14
- "@cdc/waffle-chart": "^4.26.3",
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": "d50e45a074fbefa56cac904917e707d57f237737",
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: {}
@@ -0,0 +1,4 @@
1
+ declare module '*.png' {
2
+ const content: string
3
+ export default content
4
+ }
@@ -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
- <button
278
- className='btn btn-primary px-4 ms-2'
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
- </button>
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: 27,
572
+ id: 29,
495
573
  category: 'Charts',
496
574
  label: 'Radar',
497
575
  type: 'chart',