@cdc/core 4.25.11 → 4.26.2

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 (147) hide show
  1. package/.claude/agents/qa-test-developer.md +126 -0
  2. package/CLAUDE.local.md +67 -0
  3. package/_stories/Gallery.Charts.stories.tsx +300 -0
  4. package/_stories/Gallery.DataBite.stories.tsx +79 -0
  5. package/_stories/Gallery.Maps.stories.tsx +239 -0
  6. package/_stories/Gallery.WaffleChart.stories.tsx +187 -0
  7. package/_stories/PageART.stories.tsx +193 -0
  8. package/_stories/PageBRFSS.stories.tsx +294 -0
  9. package/_stories/PageCancerRegistries.stories.tsx +199 -0
  10. package/_stories/PageEasternEquineEncephalitis.stories.tsx +216 -0
  11. package/_stories/PageExcessiveAlcoholUse.stories.tsx +201 -0
  12. package/_stories/PageMaternalMortality.stories.tsx +193 -0
  13. package/_stories/PageOralHealth.stories.tsx +201 -0
  14. package/_stories/PageRespiratory.stories.tsx +332 -0
  15. package/_stories/PageSmokingTobacco.stories.tsx +200 -0
  16. package/_stories/PageStateDiabetesProfiles.stories.tsx +201 -0
  17. package/_stories/PageWastewater.stories.tsx +477 -0
  18. package/_stories/VegaImport.stories.tsx +401 -0
  19. package/_stories/vega-fixtures/bars-with-line.json +444 -0
  20. package/_stories/vega-fixtures/bars.json +58 -0
  21. package/_stories/vega-fixtures/combo-bar-rolling-mean.json +88 -0
  22. package/_stories/vega-fixtures/combo.json +68 -0
  23. package/_stories/vega-fixtures/grouped-horizontal-bars.json +83 -0
  24. package/_stories/vega-fixtures/grouped-horizontal-bars2.json +231 -0
  25. package/_stories/vega-fixtures/horizontal-bar.json +427 -0
  26. package/_stories/vega-fixtures/horizontal-bars-with-bad-colors.json +197 -0
  27. package/_stories/vega-fixtures/horizontal-bars2.json +58 -0
  28. package/_stories/vega-fixtures/lines.json +227 -0
  29. package/_stories/vega-fixtures/measles-bars.json +348 -0
  30. package/_stories/vega-fixtures/measles-map.json +11101 -0
  31. package/_stories/vega-fixtures/measles-stacked-bars.json +2147 -0
  32. package/_stories/vega-fixtures/multi-dataset.json +255 -0
  33. package/_stories/vega-fixtures/no-data.json +14 -0
  34. package/_stories/vega-fixtures/pie-chart.json +94 -0
  35. package/_stories/vega-fixtures/repeat-spec.json +47 -0
  36. package/_stories/vega-fixtures/stacked-area.json +222 -0
  37. package/_stories/vega-fixtures/stacked-bar-with-rect.json +3412 -0
  38. package/_stories/vega-fixtures/stacked-bars-with-line.json +364 -0
  39. package/_stories/vega-fixtures/stacked-bars.json +212 -0
  40. package/_stories/vega-fixtures/stacked-horizontal-bars.json +140 -0
  41. package/_stories/vega-fixtures/warning-combo.json +59 -0
  42. package/_stories/vega-fixtures/warning-scatter-and-line.json +1182 -0
  43. package/assets/icon-chart-area.svg +1 -0
  44. package/assets/icon-chart-radar.svg +23 -0
  45. package/assets/icon-magnifying-glass.svg +5 -0
  46. package/assets/icon-warming-stripes.svg +13 -0
  47. package/assets/logo2.svg +31 -0
  48. package/components/AdvancedEditor/AdvancedEditor.tsx +4 -0
  49. package/components/AdvancedEditor/EmbedEditor.tsx +513 -0
  50. package/components/ComboBox/ComboBox.tsx +345 -0
  51. package/components/ComboBox/combobox.styles.css +185 -0
  52. package/components/ComboBox/index.ts +1 -0
  53. package/components/CustomColorsEditor/CustomColorsEditor.tsx +3 -10
  54. package/components/DataTable/DataTable.tsx +132 -58
  55. package/components/DataTable/data-table.css +216 -215
  56. package/components/DataTable/helpers/getSeriesName.ts +6 -0
  57. package/components/DataTable/helpers/mapCellMatrix.tsx +14 -6
  58. package/components/EditorPanel/ColumnsEditor.tsx +37 -19
  59. package/components/EditorPanel/DataTableEditor.tsx +51 -25
  60. package/components/EditorPanel/EditorPanel.styles.css +16 -0
  61. package/components/EditorPanel/EditorPanel.tsx +144 -0
  62. package/components/EditorPanel/EditorPanelDispatch.tsx +75 -0
  63. package/components/EditorPanel/FieldSetWrapper.tsx +66 -23
  64. package/components/EditorPanel/Inputs.tsx +33 -7
  65. package/components/EditorPanel/VizFilterEditor/NestedDropdownEditor.tsx +14 -6
  66. package/components/EditorPanel/VizFilterEditor/VizFilterEditor.tsx +240 -175
  67. package/components/EditorPanel/VizFilterEditor/components/FilterOrder.tsx +33 -29
  68. package/components/EditorPanel/sections/VisualSection.tsx +169 -0
  69. package/components/Filters/Filters.tsx +31 -5
  70. package/components/Filters/helpers/getNestedOptions.ts +2 -1
  71. package/components/Filters/helpers/handleSorting.ts +1 -1
  72. package/components/Layout/components/Sidebar/components/sidebar.styles.scss +84 -2
  73. package/components/Layout/components/Visualization/index.tsx +27 -1
  74. package/components/Layout/components/Visualization/visualizations.scss +7 -0
  75. package/components/Legend/Legend.Gradient.tsx +1 -1
  76. package/components/MediaControls.tsx +53 -28
  77. package/components/_stories/CustomColorsEditor.stories.tsx +37 -0
  78. package/components/_stories/DataTable.stories.tsx +1 -0
  79. package/components/ui/Icon.tsx +3 -1
  80. package/components/ui/Title/index.tsx +30 -2
  81. package/components/ui/Title/title.styles.css +42 -0
  82. package/data/colorPalettes.ts +18 -5
  83. package/data/mapColorPalettes.ts +10 -0
  84. package/devTemplate/dev.js +235 -0
  85. package/devTemplate/index.html +30 -0
  86. package/devTemplate/preview.html +1503 -0
  87. package/devTemplate/sidebar.css +151 -0
  88. package/dist/cove-main.css +2803 -4448
  89. package/dist/cove-main.css.map +1 -1
  90. package/generateViteConfig.js +118 -2
  91. package/helpers/DataTransform.ts +1 -5
  92. package/helpers/addValuesToFilters.ts +6 -1
  93. package/helpers/cove/date.ts +33 -1
  94. package/helpers/cove/string.ts +29 -0
  95. package/helpers/coveUpdateWorker.ts +21 -12
  96. package/helpers/embed/embedCodeGenerator.ts +80 -0
  97. package/helpers/embed/embedHelper.js +158 -0
  98. package/helpers/embed/filterUtils.ts +121 -0
  99. package/helpers/embed/index.ts +21 -0
  100. package/helpers/embed/urlValidation.ts +119 -0
  101. package/helpers/filterVizData.ts +6 -1
  102. package/helpers/getFileExtension.ts +0 -6
  103. package/helpers/getUniqueValues.ts +19 -0
  104. package/helpers/hashObj.ts +25 -0
  105. package/helpers/isRightAlignedTableValue.js +5 -0
  106. package/helpers/metrics/helpers.ts +1 -0
  107. package/helpers/metrics/types.ts +3 -0
  108. package/helpers/palettes/colorDistributions.ts +1 -1
  109. package/helpers/palettes/utils.ts +12 -12
  110. package/helpers/parseCsvWithQuotes.ts +15 -14
  111. package/helpers/pivotData.ts +2 -2
  112. package/helpers/prepareScreenshot.ts +288 -0
  113. package/helpers/queryStringUtils.ts +29 -0
  114. package/helpers/testing.ts +44 -0
  115. package/helpers/tests/DataTransform.test.ts +125 -0
  116. package/helpers/tests/date.test.ts +64 -0
  117. package/helpers/tests/prepareScreenshot.test.ts +414 -0
  118. package/helpers/tests/queryStringUtils.test.ts +381 -0
  119. package/helpers/tests/testStandaloneBuild.ts +23 -5
  120. package/helpers/useDataVizClasses.ts +0 -1
  121. package/helpers/vegaConfig.ts +1 -1
  122. package/helpers/vegaConfigImport.ts +160 -0
  123. package/helpers/ver/4.26.1.ts +80 -0
  124. package/helpers/ver/4.26.2.ts +84 -0
  125. package/helpers/ver/tests/4.26.1.test.ts +105 -0
  126. package/helpers/ver/tests/4.26.2.test.ts +298 -0
  127. package/helpers/viewports.ts +2 -0
  128. package/hooks/useDataColumns.ts +63 -0
  129. package/hooks/useFilterManagement.ts +94 -0
  130. package/hooks/useLegendSeparators.ts +26 -0
  131. package/hooks/useListManagement.ts +192 -0
  132. package/package.json +29 -33
  133. package/styles/_button-section.scss +0 -3
  134. package/styles/v2/components/editor.scss +9 -9
  135. package/styles/v2/utils/_grid.scss +8 -3
  136. package/types/Annotation.ts +10 -11
  137. package/types/Axis.ts +1 -0
  138. package/types/ForecastingSeriesKey.ts +1 -0
  139. package/types/General.ts +2 -0
  140. package/types/MarkupInclude.ts +1 -0
  141. package/types/Palette.ts +21 -0
  142. package/types/Series.ts +3 -0
  143. package/types/Table.ts +1 -0
  144. package/types/Visualization.ts +7 -0
  145. package/types/VizFilter.ts +1 -0
  146. package/LICENSE +0 -201
  147. package/_stories/StoryRenderingTests.stories.tsx +0 -164
@@ -48,11 +48,22 @@ export type DataTableProps = {
48
48
  setFilteredCountryCode?: string // used for Maps only
49
49
  tabbingId: string
50
50
  tableTitle: string
51
+ applyLegendToRow?: (
52
+ rowObj: any,
53
+ config: any,
54
+ runtimeLegend: any,
55
+ legendMemo: any,
56
+ legendSpecialClassLastMemo: any
57
+ ) => string[]
58
+ getPatternForRow?: (rowObj: any, config: any) => any
51
59
  viewport: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
52
60
  vizTitle?: string
53
61
  // determines if columns should be wrapped in the table
54
62
  wrapColumns?: boolean
55
63
  interactionLabel?: string
64
+ showDownloadImgButton?: boolean
65
+ showDownloadPdfButton?: boolean
66
+ includeContextInDownload?: boolean
56
67
  // Map-specific props (optional)
57
68
  legendMemo?: React.MutableRefObject<Map<any, any>>
58
69
  legendSpecialClassLastMemo?: React.MutableRefObject<Map<any, any>>
@@ -76,7 +87,11 @@ const DataTable = (props: DataTableProps) => {
76
87
  viewport,
77
88
  vizTitle,
78
89
  wrapColumns,
79
- interactionLabel = ''
90
+ interactionLabel = '',
91
+ showDownloadImgButton,
92
+ showDownloadPdfButton,
93
+ includeContextInDownload = false,
94
+ imageRef
80
95
  } = props
81
96
  const runtimeData = useMemo(() => {
82
97
  const data = removeNullColumns(parentRuntimeData)
@@ -145,23 +160,23 @@ const DataTable = (props: DataTableProps) => {
145
160
  const rows =
146
161
  isVertical && sortBy.column
147
162
  ? rawRows.sort((a, b) => {
148
- let dataA
149
- let dataB
150
- if (config.type === 'map' && config.columns) {
151
- const sortByColName = config.columns[sortBy.column].name
152
- dataA = runtimeData[a][sortByColName]
153
- dataB = runtimeData[b][sortByColName]
154
- }
155
- if (['chart', 'dashboard', 'table'].includes(config.type)) {
156
- dataA = runtimeData[a][sortBy.column]
157
- dataB = runtimeData[b][sortBy.column]
158
- }
159
- if (!dataA && !dataB && config.type === 'chart' && config.xAxis && config.xAxis.type === 'date-time') {
160
- dataA = timeParse(config.runtime.xAxis.dateParseFormat)(runtimeData[a][config.xAxis.dataKey])
161
- dataB = timeParse(config.runtime.xAxis.dateParseFormat)(runtimeData[b][config.xAxis.dataKey])
162
- }
163
- return dataA || dataB ? customSort(dataA, dataB, sortBy, config) : 0
164
- })
163
+ let dataA
164
+ let dataB
165
+ if (config.type === 'map' && config.columns) {
166
+ const sortByColName = config.columns[sortBy.column].name
167
+ dataA = runtimeData[a][sortByColName]
168
+ dataB = runtimeData[b][sortByColName]
169
+ }
170
+ if (['chart', 'dashboard', 'table'].includes(config.type)) {
171
+ dataA = runtimeData[a][sortBy.column]
172
+ dataB = runtimeData[b][sortBy.column]
173
+ }
174
+ if (!dataA && !dataB && config.type === 'chart' && config.xAxis && config.xAxis.type === 'date-time') {
175
+ dataA = timeParse(config.runtime.xAxis.dateParseFormat)(runtimeData[a][config.xAxis.dataKey])
176
+ dataB = timeParse(config.runtime.xAxis.dateParseFormat)(runtimeData[b][config.xAxis.dataKey])
177
+ }
178
+ return dataA || dataB ? customSort(dataA, dataB, sortBy, config) : 0
179
+ })
165
180
  : rawRows
166
181
 
167
182
  const limitHeight = {
@@ -214,18 +229,14 @@ const DataTable = (props: DataTableProps) => {
214
229
  const getClassNames = (): string => {
215
230
  const classes = ['data-table-container']
216
231
 
217
- const hasDownloadLinkAbove = config.table.download && !config.table.showDownloadLinkBelow
232
+ const hasDownloadLinkAbove =
233
+ (config.table.download || showDownloadImgButton || showDownloadPdfButton) && !config.table.showDownloadLinkBelow
218
234
  const isStandaloneTable = config.type === 'table'
219
235
 
220
236
  if (!hasDownloadLinkAbove && !isStandaloneTable) {
221
237
  classes.push('mt-4')
222
238
  }
223
239
 
224
- const isBrushActive = config?.brush?.active && config.legend?.position !== 'bottom'
225
- if (isBrushActive) {
226
- classes.push('brush-active')
227
- }
228
-
229
240
  classes.push(viewport)
230
241
 
231
242
  return classes.join(' ')
@@ -237,33 +248,76 @@ const DataTable = (props: DataTableProps) => {
237
248
  const sharedFilterColumns = config.table?.sharedFilterColumns || []
238
249
  const vizFilterColumns = (config.filters || []).map(filter => filter.columnName)
239
250
  const filterColumns = [...sharedFilterColumns, ...vizFilterColumns]
251
+ const getVisibleColumns = () => {
252
+ if (!config.columns) return []
253
+
254
+ return Object.values(config.columns)
255
+ .filter(col => col.dataTable !== false)
256
+ .map(col => col.name)
257
+ }
258
+
259
+ const visibleColumns = getVisibleColumns()
240
260
  const visibleData =
241
261
  config.type === 'map'
242
262
  ? getMapRowData(
243
- rows,
244
- columns,
245
- config,
246
- formatLegendLocation,
247
- runtimeData as Record<string, Object>,
248
- displayGeoName,
249
- filterColumns
250
- )
263
+ rows,
264
+ columns,
265
+ config,
266
+ formatLegendLocation,
267
+ runtimeData as Record<string, Object>,
268
+ displayGeoName,
269
+ filterColumns
270
+ )
251
271
  : runtimeData.map(d => {
252
- return _.pick(d, [...filterColumns, ...dataSeriesColumns])
253
- })
272
+ const columnsToInclude =
273
+ config.type === 'table'
274
+ ? _.uniq([...filterColumns, ...visibleColumns])
275
+ : _.uniq([...filterColumns, ...dataSeriesColumns])
276
+ return _.pick(d, columnsToInclude)
277
+ })
254
278
  const csvData = config.table?.downloadVisibleDataOnly ? visibleData : rawData
255
279
 
280
+ // Build a map from column name to column config for O(1) lookup
281
+ const columnConfigMap = config.columns
282
+ ? Object.values(config.columns).reduce((acc, col) => {
283
+ acc[col.name] = col
284
+ return acc
285
+ }, {} as Record<string, any>)
286
+ : {}
287
+
288
+ // Map column names to labels
289
+ const csvDataUpdated = csvData.map(row => {
290
+ const newRow: Record<string, any> = {}
291
+ Object.keys(row).forEach(key => {
292
+ // Use the column config map for O(1) lookup
293
+ const columnConfig = columnConfigMap[key]
294
+ // Use label if it exists, otherwise use the original key
295
+ const columnLabel = columnConfig?.label || key
296
+ newRow[columnLabel] = row[key]
297
+ })
298
+ return newRow
299
+ })
300
+
256
301
  // only use fullGeoName on County maps and no other
257
302
  if (config.general?.geoType === 'us-county' || config.table.showFullGeoNameInCSV) {
258
303
  // Add column for full Geo name along with State
259
- return csvData.map(row => {
304
+ return csvDataUpdated.map((row, index) => {
305
+ const originalRow = csvData[index]
306
+ if (!originalRow) {
307
+ console.warn('Data mismatch: originalRow missing.', {
308
+ index,
309
+ csvDataLength: csvData.length,
310
+ csvDataUpdatedLength: csvDataUpdated.length
311
+ })
312
+ return row
313
+ }
260
314
  return {
261
- FullGeoName: formatLegendLocation(row[config.columns.geo.name]),
315
+ FullGeoName: formatLegendLocation(originalRow[config.columns.geo.name]),
262
316
  ...row
263
317
  }
264
318
  })
265
319
  } else {
266
- return csvData
320
+ return csvDataUpdated
267
321
  }
268
322
  }
269
323
 
@@ -273,9 +327,6 @@ const DataTable = (props: DataTableProps) => {
273
327
  if (hasDownloadLink) {
274
328
  classes.push('mt-4', 'mb-2')
275
329
  }
276
- const isLegendOnBottom = config?.legend?.position === 'bottom' || isLegendWrapViewport(viewport)
277
- if (config.brush?.active && !isLegendOnBottom) classes.push('brush-active')
278
- if (config.brush?.active && config.legend.hide) classes.push('brush-active')
279
330
  } else {
280
331
  if (hasDownloadLink) {
281
332
  classes.push('mt-2')
@@ -287,15 +338,15 @@ const DataTable = (props: DataTableProps) => {
287
338
  const childrenMatrix =
288
339
  config.type === 'map'
289
340
  ? mapCellMatrix({
290
- ...props,
291
- rows,
292
- wrapColumns,
293
- runtimeData,
294
- viewport,
295
- legendMemo: props.legendMemo || defaultLegendMemo,
296
- legendSpecialClassLastMemo: props.legendSpecialClassLastMemo || defaultLegendSpecialClassLastMemo,
297
- runtimeLegend: props.runtimeLegend || defaultRuntimeLegend
298
- })
341
+ ...props,
342
+ rows,
343
+ wrapColumns,
344
+ runtimeData,
345
+ viewport,
346
+ legendMemo: props.legendMemo || defaultLegendMemo,
347
+ legendSpecialClassLastMemo: props.legendSpecialClassLastMemo || defaultLegendSpecialClassLastMemo,
348
+ runtimeLegend: props.runtimeLegend || defaultRuntimeLegend
349
+ })
299
350
  : chartCellMatrix({ rows, ...props, runtimeData, isVertical, sortBy, hasRowType, viewport })
300
351
 
301
352
  const useBottomExpandCollapse = config.table.showBottomCollapse && expanded && Array.isArray(childrenMatrix)
@@ -303,19 +354,41 @@ const DataTable = (props: DataTableProps) => {
303
354
  // If every value in a column is a number, record the column index so the header and cells can be right-aligned
304
355
  const rightAlignedCols = childrenMatrix.length
305
356
  ? Object.fromEntries(
306
- Object.keys(childrenMatrix[0])
307
- .filter(
308
- i => childrenMatrix.filter(row => isRightAlignedTableValue(row[i])).length === childrenMatrix.length
309
- )
310
- .map(x => [x, true])
311
- )
357
+ Object.keys(childrenMatrix[0])
358
+ .filter(
359
+ i => childrenMatrix.filter(row => isRightAlignedTableValue(row[i])).length === childrenMatrix.length
360
+ )
361
+ .map(x => [x, true])
362
+ )
312
363
  : {}
313
364
 
314
365
  const showCollapseButton = config.table.collapsible !== false && useBottomExpandCollapse
315
366
  const TableMediaControls = ({ belowTable }) => {
316
367
  const hasDownloadLink = config.table.download
368
+ const hasImageDownloads = showDownloadImgButton || showDownloadPdfButton
369
+
317
370
  return (
318
- <MediaControls.Section classes={getMediaControlsClasses(belowTable, hasDownloadLink)}>
371
+ <MediaControls.Section classes={getMediaControlsClasses(belowTable, hasDownloadLink || hasImageDownloads)}>
372
+ {showDownloadImgButton && (
373
+ <MediaControls.DownloadLink
374
+ type='image'
375
+ title='Download Chart as Image'
376
+ state={config}
377
+ elementToCapture={imageRef}
378
+ interactionLabel={interactionLabel}
379
+ includeContextInDownload={includeContextInDownload}
380
+ />
381
+ )}
382
+ {showDownloadPdfButton && (
383
+ <MediaControls.DownloadLink
384
+ type='pdf'
385
+ title='Download Chart as PDF'
386
+ state={config}
387
+ elementToCapture={imageRef}
388
+ interactionLabel={interactionLabel}
389
+ includeContextInDownload={includeContextInDownload}
390
+ />
391
+ )}
319
392
  <MediaControls.Link config={config} dashboardDataConfig={dataConfig} interactionLabel={interactionLabel} />
320
393
  {hasDownloadLink && (
321
394
  <DownloadButton
@@ -384,8 +457,9 @@ const DataTable = (props: DataTableProps) => {
384
457
  )
385
458
  }
386
459
  tableOptions={{
387
- className: `table table-striped table-width-unset ${expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'
388
- }${isVertical ? '' : ' horizontal'}`,
460
+ className: `table table-striped table-width-unset ${
461
+ expanded ? 'data-table' : 'data-table cdcdataviz-sr-only'
462
+ }${isVertical ? '' : ' horizontal'}`,
389
463
  'aria-live': 'assertive',
390
464
  'aria-rowcount': config?.data?.length ? config.data.length : -1,
391
465
  hidden: !expanded,