@cdc/editor 4.26.4 → 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/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@cdc/editor",
3
- "version": "4.26.4",
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.4",
9
- "@cdc/core": "^4.26.4",
10
- "@cdc/dashboard": "^4.26.4",
11
- "@cdc/data-bite": "^4.26.4",
12
- "@cdc/map": "^4.26.4",
13
- "@cdc/markup-include": "^4.26.4",
14
- "@cdc/waffle-chart": "^4.26.4",
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": "6097de1ff814001880d9ac64bd66becdc092d63c",
26
+ "gitHead": "61c025165d96b45a6002c34582c5a622a9d865a9",
27
27
  "main": "dist/cdceditor",
28
28
  "moduleName": "CdcEditor",
29
29
  "peerDependencies": {
@@ -165,6 +165,68 @@ export const DownloadSingleVizCSV: Story = {
165
165
  }
166
166
  }
167
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
+
168
230
  export const InvalidJsonShowsValidationAlert: Story = {
169
231
  args: {
170
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
  })
@@ -18,6 +18,7 @@ import EpiChartIcon from '@cdc/core/assets/icon-epi-chart.svg'
18
18
  import ForecastIcon from '@cdc/core/assets/icon-chart-forecast.svg'
19
19
  import GaugeChartIcon from '@cdc/core/assets/icon-linear-gauge.svg'
20
20
  import GlobeIcon from '@cdc/core/assets/icon-map-world.svg'
21
+ import HeatMapIconSrc from '@cdc/core/assets/icon-heatmap.png'
21
22
  import HorizonChartIcon from '@cdc/core/assets/icon-chart-area.svg'
22
23
  import HorizontalStackIcon from '@cdc/core/assets/icon-chart-bar-stacked.svg'
23
24
  import Icon from '@cdc/core/components/ui/Icon'
@@ -69,6 +70,11 @@ const VizButton: React.FC<VizButtonProps> = ({ activeVizButtonID, onConfigure, .
69
70
  )
70
71
  }
71
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
+
72
78
  const ChooseTab: React.FC = (): JSX.Element => {
73
79
  const { config, tempConfig } = useContext(ConfigContext)
74
80
 
@@ -441,6 +447,76 @@ const buttons = [
441
447
  icon: <ForecastIcon />,
442
448
  content: 'Display a forecasting chart to predict future data trends.'
443
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
+ },
444
520
  {
445
521
  id: 27,
446
522
  category: 'Charts',
@@ -493,7 +569,7 @@ const buttons = [
493
569
  content: 'Present the numerical proportions of a data series.'
494
570
  },
495
571
  {
496
- id: 27,
572
+ id: 29,
497
573
  category: 'Charts',
498
574
  label: 'Radar',
499
575
  type: 'chart',
@@ -1,6 +1,5 @@
1
1
  import React, { useState, useContext, useEffect } from 'react'
2
2
  import { useDropzone } from 'react-dropzone'
3
- import axios from 'axios'
4
3
  import { csvFormat } from 'd3'
5
4
 
6
5
  import { DataTransform } from '@cdc/core/helpers/DataTransform'
@@ -102,25 +101,27 @@ const DataImport = () => {
102
101
 
103
102
  try {
104
103
  // eslint-disable-next-line no-unused-vars
105
- await axios
106
- .get(dataURL.toString(), {
107
- responseType: 'blob'
108
- })
109
- .then(response => {
110
- responseBlob = response.data
111
-
112
- // Sometimes the files are coming in as plain text types... Maybe when saved from Macs
113
- const csvTypes = ['text/csv', 'text/plain']
114
- if ((fileExtension === '.csv' && csvTypes.includes(responseBlob.type)) || isSolrCsv(externalURL)) {
115
- responseBlob = responseBlob.slice(0, responseBlob.size, 'text/csv')
116
- } else if (
117
- responseBlob.type === 'application/json' ||
118
- (fileExtension === '.json' && responseBlob.type === 'text/plain') ||
119
- isSolrJson(externalURL)
120
- ) {
121
- responseBlob = responseBlob.slice(0, responseBlob.size, 'application/json')
122
- }
123
- })
104
+ const response = await fetch(dataURL.toString())
105
+ if (!response.ok) {
106
+ throw new Error(`HTTP error ${response.status}`)
107
+ }
108
+ responseBlob = await response.blob()
109
+
110
+ // Strip charset/parameters — fetch preserves the full Content-Type (e.g. 'application/json; charset=utf-8')
111
+ // while XHR (axios) would strip them, so normalise before comparing.
112
+ const blobBaseType = responseBlob.type.split(';')[0].trim()
113
+
114
+ // Sometimes the files are coming in as plain text types... Maybe when saved from Macs
115
+ const csvTypes = ['text/csv', 'text/plain']
116
+ if ((fileExtension === '.csv' && csvTypes.includes(blobBaseType)) || isSolrCsv(externalURL)) {
117
+ responseBlob = responseBlob.slice(0, responseBlob.size, 'text/csv')
118
+ } else if (
119
+ blobBaseType === 'application/json' ||
120
+ (fileExtension === '.json' && blobBaseType === 'text/plain') ||
121
+ isSolrJson(externalURL)
122
+ ) {
123
+ responseBlob = responseBlob.slice(0, responseBlob.size, 'application/json')
124
+ }
124
125
  } catch (err) {
125
126
  // eslint-disable-next-line no-console
126
127
  console.error('Error in loadExternal', err)
@@ -0,0 +1,16 @@
1
+ import SampleData from './SampleData'
2
+
3
+ describe('SampleData', () => {
4
+ it('includes a synthetic varicella HeatMap sample dataset', () => {
5
+ const sample = SampleData.data.charts.find(sample => sample.fileName === 'valid-heatmap-varicella-cases.csv')
6
+
7
+ expect(sample).toBeDefined()
8
+ expect(sample).toEqual(
9
+ expect.objectContaining({
10
+ text: 'HeatMap Data (Synthetic Varicella Cases)',
11
+ data: expect.stringContaining('Month,HHS Region 1,HHS Region 2')
12
+ })
13
+ )
14
+ expect(sample?.data).toContain('Apr,55,61,78,69,72,64,58,57,66,50')
15
+ })
16
+ })
@@ -10,6 +10,7 @@ import validBoxPlotData from './samples/valid-boxplot.csv?raw'
10
10
  import validChartData from './samples/valid-data-chart.csv?raw'
11
11
  import validCountyMapData from './samples/valid-county-data.csv?raw'
12
12
  import validForecastData from './samples/valid-forecast-data.csv?raw'
13
+ import validHeatMapData from './samples/valid-heatmap-varicella-cases.csv?raw'
13
14
  import validGeoPoint from './samples/valid-geo-point.csv?raw'
14
15
  import validHorizonData from './samples/valid-horizon-chart.json?raw'
15
16
  import validMapData from './samples/valid-data-map.csv?raw'
@@ -60,6 +61,11 @@ const sampleData = {
60
61
  fileName: 'valid-horizon-data.json',
61
62
  data: validHorizonData
62
63
  },
64
+ {
65
+ text: 'HeatMap Data (Synthetic Varicella Cases)',
66
+ fileName: 'valid-heatmap-varicella-cases.csv',
67
+ data: validHeatMapData
68
+ },
63
69
  {
64
70
  text: 'Sankey Chart Data',
65
71
  fileName: 'valid-sankey-data.json',
@@ -0,0 +1,13 @@
1
+ Month,HHS Region 1,HHS Region 2,HHS Region 3,HHS Region 4,HHS Region 5,HHS Region 6,HHS Region 7,HHS Region 8,HHS Region 9,HHS Region 10
2
+ Jan,18,22,35,28,31,24,19,21,26,16
3
+ Feb,24,29,41,34,38,31,27,28,33,22
4
+ Mar,42,48,63,56,59,51,46,44,53,39
5
+ Apr,55,61,78,69,72,64,58,57,66,50
6
+ May,49,54,70,62,65,58,52,51,60,46
7
+ Jun,31,36,49,43,45,40,35,34,41,30
8
+ Jul,17,21,30,25,28,23,20,19,24,16
9
+ Aug,12,15,22,18,20,16,14,13,17,11
10
+ Sep,14,18,26,21,23,19,16,15,20,13
11
+ Oct,20,24,33,28,30,25,22,21,27,18
12
+ Nov,27,32,44,37,40,34,29,30,35,24
13
+ Dec,34,39,52,45,48,41,36,37,43,31
@@ -84,7 +84,8 @@
84
84
  }
85
85
  }
86
86
 
87
- svg {
87
+ svg,
88
+ .choose-vis__heatmap-icon {
88
89
  display: block;
89
90
  margin: 0 auto;
90
91
  box-sizing: border-box;
@@ -92,6 +93,11 @@
92
93
  height: 80px;
93
94
  flex-shrink: 0;
94
95
  }
96
+
97
+ .choose-vis__heatmap-icon {
98
+ object-fit: contain;
99
+ mix-blend-mode: multiply;
100
+ }
95
101
  }
96
102
  }
97
103
  }
@@ -31,7 +31,6 @@
31
31
  background: #f2f2f2;
32
32
  line-height: 3rem;
33
33
  border-bottom: 1px solid var(--lightGray);
34
- color: #333;
35
34
  flex-grow: 1;
36
35
  flex-shrink: 0;
37
36
  width: 25%;
@@ -225,10 +224,6 @@
225
224
  }
226
225
  }
227
226
 
228
- section.introText {
229
- padding: 15px 0;
230
- }
231
-
232
227
  section.footnotes {
233
228
  border-top: 1px solid #ddd;
234
229
  margin-top: 70px;