@cdc/chart 4.26.5 → 4.26.7

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 (128) hide show
  1. package/CONFIG.md +20 -14
  2. package/dist/{cdcchart-DQ00cQCm.es.js → cdcchart-BWVIUcAF.es.js} +5 -55
  3. package/dist/cdcchart.js +36554 -34320
  4. package/examples/feature/heatmap/average-age-categorical.json +1 -1
  5. package/examples/feature/heatmap/calendar-heatmap.json +1 -1
  6. package/examples/feature/heatmap/sparse-aggregation.json +1 -1
  7. package/examples/private/bad-y-axis.json +543 -0
  8. package/examples/private/bar-chart-negative-value.json +455 -0
  9. package/examples/private/flueview.json +22051 -0
  10. package/package.json +3 -11
  11. package/src/CdcChart.tsx +7 -6
  12. package/src/CdcChartComponent.tsx +71 -18
  13. package/src/_stories/Chart.PieLabels.stories.tsx +47 -0
  14. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +183 -0
  15. package/src/_stories/Chart.YAxisAutoMaxStrategy.stories.tsx +52 -0
  16. package/src/_stories/Chart.YAxisFilterDomain.stories.tsx +90 -0
  17. package/src/_stories/Chart.smoke.stories.tsx +14 -1
  18. package/src/_stories/ChartAnnotation.smoke.stories.tsx +10 -0
  19. package/src/_stories/ChartPrefixSuffix.smoke.stories.tsx +28 -1
  20. package/src/_stories/_mock/annotation_event-line_mock.json +1064 -0
  21. package/src/_stories/_mock/heatmap-average-age-categorical.json +1 -1
  22. package/src/_stories/_mock/heatmap.json +1 -1
  23. package/src/components/Annotations/components/AnnotationDraggable.styles.css +22 -1
  24. package/src/components/Annotations/components/AnnotationDraggable.tsx +219 -119
  25. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +198 -0
  26. package/src/components/Axis/LeftAxis.tsx +3 -0
  27. package/src/components/Axis/LeftAxisGridlines.tsx +8 -1
  28. package/src/components/Axis/RightAxis.tsx +3 -0
  29. package/src/components/Axis/tests/AxisTickValues.test.tsx +86 -0
  30. package/src/components/BarChart/components/BarChart.Horizontal.tsx +1 -2
  31. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -1
  32. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -1
  33. package/src/components/BarChart/components/BarChart.Vertical.tsx +1 -2
  34. package/src/components/BarChart/helpers/getBarData.ts +28 -28
  35. package/src/components/BarChart/helpers/getBarHeights.ts +2 -2
  36. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +98 -0
  37. package/src/components/BarChart/helpers/useBarChart.ts +4 -5
  38. package/src/components/BoxPlot/BoxPlot.Horizontal.tsx +7 -6
  39. package/src/components/BoxPlot/BoxPlot.Vertical.tsx +9 -7
  40. package/src/components/BoxPlot/helpers/index.ts +4 -3
  41. package/src/components/Brush/BrushSelector.tsx +1 -1
  42. package/src/components/EditorPanel/EditorPanel.tsx +457 -165
  43. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +101 -52
  44. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +16 -7
  45. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +3 -3
  46. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +19 -4
  47. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.test.tsx +98 -0
  48. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +10 -0
  49. package/src/components/EditorPanel/components/Panels/Panel.Visual.test.tsx +484 -0
  50. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +3 -0
  51. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +262 -0
  52. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +32 -0
  53. package/src/components/EditorPanel/components/panels.scss +10 -0
  54. package/src/components/EditorPanel/editor-panel.scss +47 -0
  55. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +6 -2
  56. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +20 -2
  57. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +3 -3
  58. package/src/components/EditorPanel/useEditorPermissions.test.tsx +288 -11
  59. package/src/components/EditorPanel/useEditorPermissions.ts +46 -6
  60. package/src/components/Forecasting/Forecasting.tsx +1 -1
  61. package/src/components/HeatMap/components/HeatMap.tsx +22 -17
  62. package/src/components/HeatMap/components/HeatMapGradientLegend.tsx +29 -15
  63. package/src/components/HeatMap/heatmap.css +15 -1
  64. package/src/components/HeatMap/tests/HeatMap.test.tsx +39 -7
  65. package/src/components/Legend/Legend.Component.tsx +56 -61
  66. package/src/components/Legend/Legend.Suppression.tsx +3 -3
  67. package/src/components/Legend/LegendGroup/LegendGroup.tsx +0 -1
  68. package/src/components/Legend/helpers/createFormatLabels.test.ts +88 -0
  69. package/src/components/Legend/helpers/createFormatLabels.tsx +6 -1
  70. package/src/components/LineChart/helpers.test.ts +230 -1
  71. package/src/components/LineChart/helpers.ts +25 -14
  72. package/src/components/LineChart/index.tsx +23 -7
  73. package/src/components/LinearChart/tests/LinearChart.test.tsx +30 -0
  74. package/src/components/LinearChart.tsx +51 -15
  75. package/src/components/PieChart/PieChart.tsx +117 -10
  76. package/src/components/PieChart/helpers/labelPlacement.test.ts +88 -0
  77. package/src/components/PieChart/helpers/labelPlacement.ts +95 -0
  78. package/src/components/ScatterPlot/ScatterPlot.jsx +5 -13
  79. package/src/components/SmallMultiples/SmallMultipleTile.tsx +3 -0
  80. package/src/components/SmallMultiples/SmallMultiples.tsx +38 -9
  81. package/src/components/SmallMultiples/tests/SmallMultiples.test.tsx +170 -0
  82. package/src/data/initial-state.js +3 -0
  83. package/src/helpers/calculateLeftYAxisWidth.ts +9 -3
  84. package/src/helpers/categoryOrder.ts +60 -0
  85. package/src/helpers/colorAssignmentHelpers.ts +102 -0
  86. package/src/helpers/dataHelpers.ts +11 -1
  87. package/src/helpers/getAxisMaxOverride.ts +16 -0
  88. package/src/helpers/getBoxPlotConfig.ts +19 -12
  89. package/src/helpers/getCleanTopTickMax.ts +64 -0
  90. package/src/helpers/getColorScale.ts +18 -9
  91. package/src/helpers/getComboChartConfig.ts +0 -1
  92. package/src/helpers/getExcludedData.ts +0 -1
  93. package/src/helpers/getHasBoundarySuppression.ts +27 -0
  94. package/src/helpers/getMinMax.ts +25 -22
  95. package/src/helpers/getNewRuntime.ts +0 -2
  96. package/src/helpers/getPiePercent.ts +3 -3
  97. package/src/helpers/getYAxisDomainData.ts +38 -0
  98. package/src/helpers/getYAxisFinalizationEligibility.ts +40 -0
  99. package/src/helpers/hasSpacedInlineLabel.ts +10 -0
  100. package/src/helpers/isConvertLineToBarGraph.ts +2 -2
  101. package/src/helpers/seriesColumnSettings.ts +26 -7
  102. package/src/helpers/sizeHelpers.ts +0 -2
  103. package/src/helpers/smallMultiplesHelpers.ts +5 -0
  104. package/src/helpers/tests/calculateLeftYAxisWidth.test.ts +38 -0
  105. package/src/helpers/tests/categoryOrder.test.ts +57 -0
  106. package/src/helpers/tests/dataHelpers.test.ts +135 -0
  107. package/src/helpers/tests/getAxisMaxOverride.test.ts +36 -0
  108. package/src/helpers/tests/getCleanTopTickMax.test.ts +165 -0
  109. package/src/helpers/tests/getColorScale.test.ts +197 -0
  110. package/src/helpers/tests/getMinMax.test.ts +211 -0
  111. package/src/helpers/tests/getYAxisDomainData.test.ts +119 -0
  112. package/src/helpers/tests/getYAxisFinalizationEligibility.test.ts +185 -0
  113. package/src/helpers/tests/{getYAxisAutoPaddingMode.test.ts → hasSpacedInlineLabel.test.ts} +17 -32
  114. package/src/helpers/tests/seriesColumnSettings.test.ts +76 -2
  115. package/src/helpers/tests/smallMultiplesHelpers.test.ts +99 -0
  116. package/src/hooks/tests/useRightAxis.test.tsx +268 -0
  117. package/src/hooks/tests/useScales.test.tsx +302 -27
  118. package/src/hooks/tests/useTooltip.test.tsx +132 -0
  119. package/src/hooks/useRightAxis.ts +35 -25
  120. package/src/hooks/useScales.ts +45 -28
  121. package/src/hooks/useTooltip.tsx +17 -11
  122. package/src/store/chart.reducer.ts +0 -1
  123. package/src/test/CdcChart.dataTable.test.tsx +216 -0
  124. package/src/types/ChartConfig.ts +6 -1
  125. package/src/types/ChartContext.ts +3 -0
  126. package/src/helpers/getYAxisAutoPadding.ts +0 -55
  127. package/src/helpers/getYAxisAutoPaddingMode.ts +0 -17
  128. package/src/helpers/tests/getYAxisAutoPadding.test.ts +0 -37
@@ -13,11 +13,11 @@ import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
13
13
  import ConfigContext from '../ConfigContext'
14
14
  import { ChartConfig } from '../types/ChartConfig'
15
15
  import { ChartContext } from '../types/ChartContext'
16
- import _ from 'lodash'
17
- import { getYAxisAutoPadding } from '../helpers/getYAxisAutoPadding'
18
- import { YAxisAutoPaddingMode } from '../helpers/getYAxisAutoPaddingMode'
19
16
  import getMinMax from '../helpers/getMinMax'
20
17
  import { countNumOfTicks } from '../helpers/countNumOfTicks'
18
+ import { getFinalTopTickMax } from '../helpers/getCleanTopTickMax'
19
+ import { getYAxisFinalizationEligibility } from '../helpers/getYAxisFinalizationEligibility'
20
+ import { getAxisMaxOverride } from '../helpers/getAxisMaxOverride'
21
21
 
22
22
  const scaleTypes = {
23
23
  TIME: 'time',
@@ -33,6 +33,8 @@ type useScaleProps = {
33
33
  config: ChartConfig // standard chart config
34
34
  data: Object[] // standard data array
35
35
  tableData: Object[] // table data for getMinMax
36
+ yAxisDomainData?: Object[] // data used to calculate Y-axis min/max
37
+ hasBoundarySuppression?: boolean // whether selected-domain boundaries include suppression markers
36
38
  minValue: number // raw minimum value from data
37
39
  maxValue: number // raw maximum value from data
38
40
  existPositiveValue: boolean // whether data contains positive values
@@ -40,7 +42,7 @@ type useScaleProps = {
40
42
  xAxisDataMapped: Object[] // array of x axis date/category items
41
43
  xMax: number // chart svg width
42
44
  yMax: number // chart svg height
43
- yAxisAutoPaddingMode?: YAxisAutoPaddingMode // why Y-axis auto padding should run
45
+ hasSpacedInlineLabel?: boolean
44
46
  currentViewport?: string // current viewport for tick calculation
45
47
  }
46
48
 
@@ -52,11 +54,13 @@ const useScales = (properties: useScaleProps) => {
52
54
  config,
53
55
  data,
54
56
  tableData,
57
+ yAxisDomainData,
58
+ hasBoundarySuppression = false,
55
59
  minValue,
56
60
  maxValue,
57
61
  existPositiveValue,
58
62
  isAllLine,
59
- yAxisAutoPaddingMode = 'none',
63
+ hasSpacedInlineLabel = false,
60
64
  currentViewport
61
65
  } = properties
62
66
 
@@ -66,8 +70,18 @@ const useScales = (properties: useScaleProps) => {
66
70
  const isHorizontal = config.orientation === 'horizontal'
67
71
  const { visualizationType, xAxis, forestPlot, runtime } = config
68
72
  const isForestPlot = visualizationType === 'Forest Plot'
69
- const shouldApplyYAxisAutoPadding = yAxisAutoPaddingMode !== 'none' && !isHorizontal
70
- const minMaxConfig = shouldApplyYAxisAutoPadding
73
+
74
+ const { hasValidMax: hasValidExplicitLeftMax } = getAxisMaxOverride({
75
+ value: config.runtime?.yAxis?.max,
76
+ minimumValidMax: existPositiveValue ? maxValue : 0
77
+ })
78
+ const yAxisFinalizationEligibility = getYAxisFinalizationEligibility({
79
+ config,
80
+ hasSpacedInlineLabel,
81
+ hasValidExplicitLeftMax,
82
+ isHorizontal
83
+ })
84
+ const minMaxConfig = yAxisFinalizationEligibility.shouldUseInlineLabelHeadroom
71
85
  ? { ...config, yAxis: { ...config.yAxis, enablePadding: false, scalePadding: 0 } }
72
86
  : config
73
87
 
@@ -76,10 +90,11 @@ const useScales = (properties: useScaleProps) => {
76
90
  minValue,
77
91
  maxValue,
78
92
  existPositiveValue,
79
- data,
93
+ data: yAxisDomainData || data,
80
94
  isAllLine,
81
95
  tableData,
82
- convertLineToBarGraph
96
+ convertLineToBarGraph,
97
+ hasBoundarySuppression
83
98
  }
84
99
  let { min, max, leftMax, rightMax } = getMinMax(minMaxProps)
85
100
 
@@ -95,24 +110,25 @@ const useScales = (properties: useScaleProps) => {
95
110
  })
96
111
  const handleNumTicks = isForestPlot ? config.data.length : yTickCount
97
112
 
98
- // Apply auto-padding if needed
99
- if (shouldApplyYAxisAutoPadding) {
100
- const maxPasses = yAxisAutoPaddingMode === 'inline-label' ? 3 : 1
101
-
102
- for (let i = 0; i < maxPasses; i++) {
103
- const scale = composeYScale({ min, max, yMax, config: minMaxConfig, leftMax })
104
- const padding = getYAxisAutoPadding(scale, handleNumTicks, maxValue, minValue, minMaxConfig, yAxisAutoPaddingMode)
105
- if (padding > 0 || (maxPasses > 1 && i === 0)) {
106
- const adjustedConfig = {
107
- ...minMaxConfig,
108
- yAxis: { ...minMaxConfig.yAxis, scalePadding: padding, enablePadding: true }
109
- }
110
- const result = getMinMax({ ...minMaxProps, config: adjustedConfig })
111
- min = result.min
112
- max = result.max
113
- leftMax = result.leftMax
114
- rightMax = result.rightMax
115
- }
113
+ let yTickValues: number[] | undefined
114
+
115
+ if (yAxisFinalizationEligibility.shouldFinalizePrimaryYAxis) {
116
+ const yScaleForTicks = composeYScale({ min, max, yMax, config: minMaxConfig, leftMax })
117
+ const ticks = yScaleForTicks?.ticks ? yScaleForTicks.ticks(handleNumTicks) : []
118
+ const currentMax = minMaxConfig.visualizationType === 'Combo' ? leftMax : max
119
+ const { max: finalizedMax, tickValues } = getFinalTopTickMax({
120
+ currentMax,
121
+ rawMax: minMaxConfig.visualizationType === 'Combo' ? currentMax : maxValue,
122
+ ticks,
123
+ shouldUseCleanTopTick: yAxisFinalizationEligibility.shouldUseCleanTopTick,
124
+ headroomMode: yAxisFinalizationEligibility.shouldUseInlineLabelHeadroom ? 'inline-label' : 'none'
125
+ })
126
+ yTickValues = tickValues
127
+
128
+ if (minMaxConfig.visualizationType === 'Combo') {
129
+ leftMax = finalizedMax
130
+ } else {
131
+ max = finalizedMax
116
132
  }
117
133
  }
118
134
 
@@ -364,7 +380,8 @@ const useScales = (properties: useScaleProps) => {
364
380
  min,
365
381
  max,
366
382
  leftMax,
367
- rightMax
383
+ rightMax,
384
+ yTickValues
368
385
  }
369
386
  }
370
387
 
@@ -8,7 +8,11 @@ import { isDateScale } from '@cdc/core/helpers/cove/date'
8
8
  // Third-party library imports
9
9
  import { localPoint } from '@visx/event'
10
10
  import { bisector } from 'd3-array'
11
- import _ from 'lodash'
11
+ import find from 'lodash/find'
12
+ import omit from 'lodash/omit'
13
+ import pick from 'lodash/pick'
14
+ import round from 'lodash/round'
15
+ import uniq from 'lodash/uniq'
12
16
  import { getHorizontalBarHeights } from '../components/BarChart/helpers/getBarHeights'
13
17
  import {
14
18
  findColumnConfigByName,
@@ -148,11 +152,11 @@ export const useTooltip = props => {
148
152
  const columnName = column.name || colKey
149
153
  if (seriesOwnedColumnNames.includes(columnName)) continue
150
154
 
155
+ // Extra columns own their affixes; explicit blanks prevent fallback to the value-axis units.
151
156
  const formattingParams = {
152
- addColPrefix: column.prefix,
153
- addColSuffix: column.suffix,
154
- addColRoundTo: column.roundToPlace || '',
155
- addColCommas: column.commas
157
+ addColPrefix: '',
158
+ addColSuffix: '',
159
+ ...getSeriesColumnFormattingParams(column)
156
160
  }
157
161
 
158
162
  const pieColumnData = additionalChartData?.data?.[column.name]
@@ -328,7 +332,7 @@ export const useTooltip = props => {
328
332
 
329
333
  const runtimeSeries =
330
334
  config.tooltips.singleSeries && visualizationType === 'Line'
331
- ? [_.find(config.runtime.series, d => d.dataKey === singleSeriesValue)]
335
+ ? [find(config.runtime.series, d => d.dataKey === singleSeriesValue)]
332
336
  : config.runtime.series
333
337
 
334
338
  runtimeSeries?.forEach(series => {
@@ -569,7 +573,7 @@ export const useTooltip = props => {
569
573
  const barsWithHeights = getHorizontalBarHeights<{ group }>(config, barGroups)
570
574
 
571
575
  const barGroup = findClosest(
572
- barsWithHeights.map(d => [d, _.round(d.y)]),
576
+ barsWithHeights.map(d => [d, round(d.y)]),
573
577
  mouseY
574
578
  )
575
579
 
@@ -580,7 +584,7 @@ export const useTooltip = props => {
580
584
  const seriesWithY = config.series.map((c, i) => [c, config.barHeight * i]) as [Object, number][]
581
585
  const hoveredSeries = findClosest(seriesWithY, subGroupMouseY)
582
586
  const exludeColumns = config.series.filter(s => s.dataKey !== hoveredSeries.dataKey).map(s => s.dataKey)
583
- const dataColumn = _.omit(columns[0], exludeColumns)
587
+ const dataColumn = omit(columns[0], exludeColumns)
584
588
  return dataColumn
585
589
  } else {
586
590
  const columnsWithY = columns.map((c, i) => [c, config.barHeight * i]) as [Object, number][]
@@ -661,7 +665,7 @@ export const useTooltip = props => {
661
665
  const includedSeries = runtimeSeries.map(item => item.dataKey)
662
666
  includedSeries.push(config.xAxis.dataKey)
663
667
  // get dynamic category series
664
- const dynamicDataCategories = _.uniq(
668
+ const dynamicDataCategories = uniq(
665
669
  config.runtime.series.flatMap(series => {
666
670
  if (series.dynamicCategory) {
667
671
  return [series.dynamicCategory, series.originalDataKey]
@@ -701,7 +705,7 @@ export const useTooltip = props => {
701
705
  }
702
706
  }
703
707
 
704
- return dataToSearch.map(d => _.pick(d, includedSeries))
708
+ return dataToSearch.map(d => pick(d, includedSeries))
705
709
  }
706
710
 
707
711
  /**
@@ -847,7 +851,9 @@ export const useTooltip = props => {
847
851
  return (
848
852
  <li
849
853
  style={style}
850
- className={`tooltip-body mb-1 cove-prose ${shouldRenderMarkerSlot ? 'tooltip-body--marker-layout' : ''}`}
854
+ className={['tooltip-body', 'mb-1', shouldRenderMarkerSlot ? 'tooltip-body--marker-layout' : '']
855
+ .filter(Boolean)
856
+ .join(' ')}
851
857
  >
852
858
  {markerSlot}
853
859
  {shouldRenderMarkerSlot ? <span className='tooltip-body-content'>{content}</span> : content}
@@ -2,7 +2,6 @@ import ChartActions from './chart.actions'
2
2
  import defaults from '../data/initial-state.js'
3
3
  import { ChartConfig, type ViewportSize } from '../types/ChartConfig'
4
4
  import { DimensionsType } from '@cdc/core/types/Dimensions'
5
- import _ from 'lodash'
6
5
 
7
6
  type ChartState = {
8
7
  isLoading: boolean
@@ -0,0 +1,216 @@
1
+ import React from 'react'
2
+ import { render, screen, waitFor } from '@testing-library/react'
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import CdcChart from '../CdcChartComponent'
5
+
6
+ const dataTableProps = vi.hoisted(() => {
7
+ Object.defineProperty((globalThis as any).HTMLCanvasElement.prototype, 'getContext', {
8
+ configurable: true,
9
+ value: () => ({
10
+ measureText: (text = '') => ({ width: String(text).length * 8 })
11
+ })
12
+ })
13
+
14
+ return [] as any[]
15
+ })
16
+
17
+ vi.mock('@cdc/core/components/DataTable', async () => {
18
+ const React = await vi.importActual<typeof import('react')>('react')
19
+
20
+ return {
21
+ default: props => {
22
+ dataTableProps.push(props)
23
+ return React.createElement('div', { 'data-testid': 'data-table-probe' })
24
+ }
25
+ }
26
+ })
27
+
28
+ vi.mock('@visx/responsive/lib/components/ParentSize', () => ({
29
+ default: ({ children }) => children({ width: 640, height: 360 })
30
+ }))
31
+
32
+ vi.mock('../components/LinearChart', async () => {
33
+ const React = await vi.importActual<typeof import('react')>('react')
34
+
35
+ return {
36
+ default: React.forwardRef(() => React.createElement('div', { 'data-testid': 'mock-linear-chart' }))
37
+ }
38
+ })
39
+
40
+ describe('CdcChart data table dataset wiring', () => {
41
+ beforeEach(() => {
42
+ dataTableProps.length = 0
43
+ })
44
+
45
+ it('passes the selected dashboard dataset metadata to DataTable', async () => {
46
+ const dataset = {
47
+ data: [{ category: 'A', value: 1 }],
48
+ dataFileName: 'stale-chart-source.json',
49
+ dataFileSourceType: 'url',
50
+ dataUrl: '/wcms/vizdata/chart-source.json',
51
+ runtimeDataUrl: '/wcms/vizdata/chart-runtime.json'
52
+ }
53
+
54
+ render(
55
+ <CdcChart
56
+ config={
57
+ {
58
+ type: 'chart',
59
+ visualizationType: 'Bar',
60
+ title: 'Dataset-backed Chart',
61
+ dataKey: 'chartDataset',
62
+ data: dataset.data,
63
+ xAxis: { dataKey: 'category' },
64
+ series: [{ dataKey: 'value' }],
65
+ table: {
66
+ show: true,
67
+ expanded: true,
68
+ download: true,
69
+ label: 'Data Table',
70
+ indexLabel: ''
71
+ }
72
+ } as any
73
+ }
74
+ datasets={{ chartDataset: dataset } as any}
75
+ isDashboard={true}
76
+ interactionLabel='chart-dataset-test'
77
+ />
78
+ )
79
+
80
+ await waitFor(() => expect(dataTableProps.length).toBeGreaterThan(0))
81
+
82
+ expect(dataTableProps.at(-1).dataConfig).toMatchObject({
83
+ runtimeDataUrl: '/wcms/vizdata/chart-runtime.json'
84
+ })
85
+ })
86
+
87
+ it('keeps chart footnotes visible when the data table is collapsed by default', async () => {
88
+ render(
89
+ <CdcChart
90
+ config={
91
+ {
92
+ type: 'chart',
93
+ visualizationType: 'Bar',
94
+ title: 'Footnote-backed Chart',
95
+ data: [{ category: 'A', value: 1 }],
96
+ xAxis: { dataKey: 'category' },
97
+ series: [{ dataKey: 'value' }],
98
+ legacyFootnotes: 'Legacy chart footnote',
99
+ footnotes: {
100
+ staticFootnotes: [{ text: 'Structured chart footnote' }]
101
+ },
102
+ table: {
103
+ show: true,
104
+ expanded: false,
105
+ download: true,
106
+ label: 'Data Table',
107
+ indexLabel: ''
108
+ }
109
+ } as any
110
+ }
111
+ interactionLabel='chart-footnote-collapse-test'
112
+ />
113
+ )
114
+
115
+ await waitFor(() => {
116
+ expect(dataTableProps.at(-1)?.expandDataTable).toBe(false)
117
+ })
118
+
119
+ expect(await screen.findByText('Legacy chart footnote')).toBeInTheDocument()
120
+ expect(await screen.findByText('Structured chart footnote')).toBeInTheDocument()
121
+ })
122
+
123
+ it('passes custom category order to the chart-owned data table runtime data', async () => {
124
+ render(
125
+ <CdcChart
126
+ config={
127
+ {
128
+ type: 'chart',
129
+ visualizationType: 'Bar',
130
+ title: 'Custom Category Order Chart',
131
+ data: [
132
+ { category: 'A', value: 1 },
133
+ { category: 'D', value: 4 },
134
+ { category: 'C', value: 3 },
135
+ { category: 'L', value: 12 }
136
+ ],
137
+ xAxis: {
138
+ type: 'categorical',
139
+ dataKey: 'category',
140
+ categoryOrderType: 'custom',
141
+ categoryOrder: ['A', 'C', 'D', 'L']
142
+ },
143
+ series: [{ dataKey: 'value' }],
144
+ table: {
145
+ show: true,
146
+ expanded: true,
147
+ download: true,
148
+ label: 'Data Table',
149
+ indexLabel: ''
150
+ }
151
+ } as any
152
+ }
153
+ interactionLabel='chart-category-order-table-test'
154
+ />
155
+ )
156
+
157
+ await waitFor(() => expect(dataTableProps.length).toBeGreaterThan(0))
158
+
159
+ expect(dataTableProps.at(-1).runtimeData.map(row => row.category)).toEqual(['A', 'C', 'D', 'L'])
160
+ })
161
+
162
+ it('updates metadata-backed chart title and text when dataMetadata changes and data does not', async () => {
163
+ const data = [{ category: 'A', value: 1 }]
164
+ const config = {
165
+ type: 'chart',
166
+ visualizationType: 'Bar',
167
+ title: 'Chart {{source}}',
168
+ introText: 'Intro {{source}}',
169
+ legend: {
170
+ label: 'Legend <strong>{{source}}</strong>',
171
+ description: 'Legend description <strong>{{source}}</strong>'
172
+ },
173
+ data,
174
+ dataMetadata: {},
175
+ enableMarkupVariables: true,
176
+ markupVariables: [
177
+ {
178
+ sourceType: 'metadata',
179
+ name: 'Source',
180
+ tag: '{{source}}',
181
+ metadataKey: 'source',
182
+ conditions: [],
183
+ addCommas: false
184
+ }
185
+ ],
186
+ xAxis: { dataKey: 'category' },
187
+ series: [{ dataKey: 'value' }],
188
+ table: {
189
+ show: false,
190
+ expanded: false,
191
+ label: 'Data Table',
192
+ indexLabel: ''
193
+ }
194
+ } as any
195
+
196
+ const { container, rerender } = render(<CdcChart config={config} interactionLabel='chart-metadata-test' />)
197
+
198
+ expect(await screen.findByText('Chart')).toBeInTheDocument()
199
+
200
+ rerender(
201
+ <CdcChart
202
+ config={{
203
+ ...config,
204
+ dataMetadata: { source: 'June file' }
205
+ }}
206
+ interactionLabel='chart-metadata-test'
207
+ />
208
+ )
209
+
210
+ expect(await screen.findByText('Chart June file')).toBeInTheDocument()
211
+ expect(screen.getByText('Intro June file')).toBeInTheDocument()
212
+ expect(container.textContent).toContain('Legend June file')
213
+ expect(container.textContent).toContain('Legend description June file')
214
+ expect(screen.getAllByText('June file', { selector: 'strong' }).length).toBeGreaterThanOrEqual(2)
215
+ })
216
+ })
@@ -1,4 +1,4 @@
1
- import { Axis } from '@cdc/core/types/Axis'
1
+ import { type Axis } from '@cdc/core/types/Axis'
2
2
  import { MarkupConfig } from '@cdc/core/types/MarkupVariable'
3
3
  import { type ForestPlotConfigSettings } from './ForestPlot'
4
4
  import { type HorizonConfigSettings } from './Horizon'
@@ -18,6 +18,8 @@ type General = CoreGeneral & {
18
18
  isReversed?: boolean
19
19
  customColors?: string[]
20
20
  customColorsOrdered?: string[]
21
+ colorAssignmentMode?: 'ordered' | 'by-value'
22
+ colorAssignments?: { key: string; color: string }[]
21
23
  }
22
24
  useIntelligentLineChartLabels?: boolean
23
25
  }
@@ -83,6 +85,7 @@ export interface PreliminaryDataItem {
83
85
  hideLineStyle: boolean
84
86
  circleSize: number
85
87
  displayGray: boolean
88
+ weight?: number
86
89
  }
87
90
 
88
91
  type DataFormat = {
@@ -191,6 +194,8 @@ export type AllChartsConfig = {
191
194
  footnotes: Footnotes
192
195
  forestPlot: ForestPlotConfigSettings
193
196
  formattedData: Object[] & { urlFiltered: boolean }
197
+ originalFormattedData?: Object[]
198
+ yAxisDomainData?: Object[]
194
199
  heatmap?: HeatMapConfig
195
200
  heights: {
196
201
  vertical: number
@@ -39,6 +39,7 @@ type SharedChartContext = {
39
39
  svgRef?: React.RefObject<SVGSVGElement>
40
40
  handleSmallMultipleHover?: (xAxisValue: any, yCoordinate: number) => void
41
41
  visibleAnnotations?: Annotation[]
42
+ yAxisTickValues?: number[]
42
43
  }
43
44
 
44
45
  // Line Chart Specific Context
@@ -58,6 +59,7 @@ type LineChartContext = SharedChartContext & {
58
59
  transformedData: TransformedData[]
59
60
  updateConfig: Function
60
61
  visualizationType: 'Line'
62
+ yAxisDomainData?: Object[]
61
63
  colorPalettes: Record<string, string[]>
62
64
  twoColorPalette: Record<string, string[]>
63
65
  }
@@ -88,4 +90,5 @@ export type ChartContext =
88
90
  transformedData?: TransformedData[]
89
91
  twoColorPalette: any
90
92
  updateConfig?: Function
93
+ yAxisDomainData?: Object[]
91
94
  })
@@ -1,55 +0,0 @@
1
- import { ChartConfig } from '../types/ChartConfig'
2
- import { YAxisAutoPaddingMode } from './getYAxisAutoPaddingMode'
3
-
4
- /**
5
- * Calculates the Y-axis auto padding to prevent data labels from overlapping with axis tick labels.
6
- * This is used when inline labels are enabled and there's potential for overlap.
7
- *
8
- * @param yScale - The D3 scale object for the Y-axis (must have .ticks() method)
9
- * @param handleNumTicks - The number of ticks to display on the axis
10
- * @param maxValue - The maximum data value (from useReduceData)
11
- * @param minValue - The minimum data value (from useReduceData)
12
- * @param config - The chart configuration object
13
- * @returns The calculated auto padding percentage (0-100+), or 0 if no padding needed
14
- */
15
- export const getYAxisAutoPadding = (
16
- yScale: any,
17
- handleNumTicks: number,
18
- maxValue: number,
19
- minValue: number,
20
- config: ChartConfig,
21
- mode: Exclude<YAxisAutoPaddingMode, 'none'>
22
- ): number => {
23
- // Early returns for cases where auto padding is not needed
24
- if (!yScale?.ticks || config.orientation === 'horizontal' || config.yAxis?.max) {
25
- return 0
26
- }
27
-
28
- const ticks = yScale.ticks(handleNumTicks)
29
-
30
- if (!Array.isArray(ticks) || ticks.length === 0) {
31
- return 0
32
- }
33
-
34
- // minimum percentage of the max value that the distance should be from the top grid line
35
- const MINIMUM_DISTANCE_PERCENTAGE = 0.025
36
-
37
- const topGridLine = Math.max(...ticks)
38
- const needsPaddingThreshold = topGridLine - maxValue * MINIMUM_DISTANCE_PERCENTAGE
39
- const maxValueIsGreaterThanThreshold = maxValue > needsPaddingThreshold
40
-
41
- if (!maxValueIsGreaterThanThreshold) return 0
42
-
43
- const tickGap = ticks.length === 1 ? ticks[0] : ticks[1] - ticks[0]
44
- const nextTick = Math.max(...ticks) + tickGap
45
- const divideBy = minValue < 0 ? maxValue / 2 : maxValue
46
- let calculatedPadding = (nextTick - maxValue) / divideBy
47
-
48
- if (mode === 'inline-label' && calculatedPadding <= MINIMUM_DISTANCE_PERCENTAGE) {
49
- calculatedPadding += tickGap / divideBy
50
- }
51
-
52
- /* sometimes even though the padding is getting to the next tick exactly,
53
- d3 still doesn't show the tick. we add 0.1 to ensure to tip it over the edge */
54
- return calculatedPadding * 100 + 0.1
55
- }
@@ -1,17 +0,0 @@
1
- import { ChartConfig } from '../types/ChartConfig'
2
-
3
- export type YAxisAutoPaddingMode = 'none' | 'inline-label' | 'top-title'
4
-
5
- export const getYAxisAutoPaddingMode = (config: ChartConfig): YAxisAutoPaddingMode => {
6
- const inlineLabel = config.yAxis?.inlineLabel
7
- if (inlineLabel && inlineLabel.includes(' ')) {
8
- return 'inline-label'
9
- }
10
-
11
- const yAxisTitle = String(config.yAxis?.label || config.runtime?.yAxis?.label || '').trim()
12
- if (config.yAxis?.titlePlacement === 'top' && yAxisTitle && !config.hideYAxisLabel) {
13
- return 'top-title'
14
- }
15
-
16
- return 'none'
17
- }
@@ -1,37 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { getYAxisAutoPadding } from '../getYAxisAutoPadding'
3
- import { createMockConfig } from '../../components/LinearChart/tests/mockConfigContext'
4
-
5
- const baseConfig = createMockConfig({
6
- orientation: 'vertical'
7
- })
8
-
9
- describe('getYAxisAutoPadding', () => {
10
- it('preserves the extra-tick escalation for inline-label mode', () => {
11
- const yScale = {
12
- ticks: () => [0, 1, 2]
13
- }
14
-
15
- const padding = getYAxisAutoPadding(yScale, 3, 2.95, 0, baseConfig, 'inline-label')
16
-
17
- expect(padding).toBeCloseTo(35.69, 2)
18
- })
19
-
20
- it('returns the single-pass padding for top-title mode', () => {
21
- const yScale = {
22
- ticks: () => [0, 1, 2]
23
- }
24
-
25
- const padding = getYAxisAutoPadding(yScale, 3, 2.95, 0, baseConfig, 'top-title')
26
-
27
- expect(padding).toBeCloseTo(1.79, 2)
28
- })
29
-
30
- it('returns 0 when the data is not close enough to the top tick', () => {
31
- const yScale = {
32
- ticks: () => [0, 50, 100]
33
- }
34
-
35
- expect(getYAxisAutoPadding(yScale, 3, 80, 0, baseConfig, 'top-title')).toBe(0)
36
- })
37
- })