@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
@@ -0,0 +1,102 @@
1
+ import { type ChartConfig, type VisualizationType } from '../types/ChartConfig'
2
+
3
+ export type SeriesColorAssignment = {
4
+ key: string
5
+ color: string
6
+ }
7
+
8
+ export type SeriesColorAssignmentItem = {
9
+ key: string
10
+ label: string
11
+ }
12
+
13
+ export const SERIES_COLOR_ASSIGNMENT_TYPES: VisualizationType[] = ['Line', 'Bar', 'Combo', 'Area Chart', 'Scatter Plot']
14
+
15
+ export const hasDynamicCategorySeries = (config: ChartConfig): boolean =>
16
+ Array.isArray(config.series) && config.series.some(series => Boolean(series?.dynamicCategory))
17
+
18
+ export const isColorByCategoryBarChart = (config: ChartConfig): boolean =>
19
+ config.visualizationType === 'Bar' &&
20
+ config.visualizationSubType === 'regular' &&
21
+ Boolean(config.legend?.colorCode) &&
22
+ (config.series?.length || 0) === 1
23
+
24
+ export const supportsSeriesColorAssignments = (config: ChartConfig): boolean =>
25
+ SERIES_COLOR_ASSIGNMENT_TYPES.includes(config.visualizationType) &&
26
+ !hasDynamicCategorySeries(config) &&
27
+ !isColorByCategoryBarChart(config)
28
+
29
+ export const isSeriesColorAssignmentModeByValue = (config: ChartConfig): boolean =>
30
+ config.general?.palette?.colorAssignmentMode === 'by-value'
31
+
32
+ export const getSeriesColorAssignments = (config: ChartConfig): SeriesColorAssignment[] =>
33
+ Array.isArray(config.general?.palette?.colorAssignments) ? config.general.palette.colorAssignments : []
34
+
35
+ export const hasSeriesColorAssignmentOverrides = (config: ChartConfig): boolean => {
36
+ if (!supportsSeriesColorAssignments(config) || !isSeriesColorAssignmentModeByValue(config)) return false
37
+
38
+ const currentSeriesKeys = new Set(getSeriesColorAssignmentItems(config).map(({ key }) => key))
39
+
40
+ return getSeriesColorAssignments(config).some(assignment => {
41
+ if (!assignment?.key || !assignment?.color) return false
42
+
43
+ return currentSeriesKeys.has(assignment.key)
44
+ })
45
+ }
46
+
47
+ export const getSeriesDisplayLabel = (config: ChartConfig, key: string): string => {
48
+ const series = config.series?.find(item => item?.dataKey === key)
49
+ return String(config.runtime?.seriesLabels?.[key] || series?.name || (series as any)?.label || key)
50
+ }
51
+
52
+ export const getSeriesColorAssignmentItems = (config: ChartConfig): SeriesColorAssignmentItem[] => {
53
+ if (!supportsSeriesColorAssignments(config) || !Array.isArray(config.series)) return []
54
+
55
+ return config.series
56
+ .filter(series => series?.dataKey && !series.dynamicCategory)
57
+ .map(series => ({
58
+ key: series.dataKey,
59
+ label: getSeriesDisplayLabel(config, series.dataKey)
60
+ }))
61
+ }
62
+
63
+ export const getSeriesKeyByColorScaleLabel = (config: ChartConfig): Map<string, string> => {
64
+ const labelToKey = new Map<string, string>()
65
+ const runtimeKeys = Array.isArray(config.runtime?.seriesKeys) ? config.runtime.seriesKeys : []
66
+
67
+ runtimeKeys.forEach(key => {
68
+ labelToKey.set(String(key), key)
69
+ labelToKey.set(getSeriesDisplayLabel(config, key), key)
70
+ })
71
+
72
+ getSeriesColorAssignmentItems(config).forEach(({ key, label }) => {
73
+ labelToKey.set(String(key), key)
74
+ labelToKey.set(label, key)
75
+ })
76
+
77
+ return labelToKey
78
+ }
79
+
80
+ export const applySeriesColorAssignmentsToRange = (
81
+ config: ChartConfig,
82
+ domain: string[] = [],
83
+ range: string[] = []
84
+ ): string[] => {
85
+ if (!supportsSeriesColorAssignments(config) || !isSeriesColorAssignmentModeByValue(config)) return range
86
+
87
+ const assignmentMap = new Map(
88
+ getSeriesColorAssignments(config)
89
+ .filter(assignment => assignment?.key && assignment?.color)
90
+ .map(assignment => [assignment.key, assignment.color])
91
+ )
92
+
93
+ if (!assignmentMap.size || !range.length) return range
94
+
95
+ const labelToKey = getSeriesKeyByColorScaleLabel(config)
96
+
97
+ return domain.map((label, index) => {
98
+ const key = labelToKey.get(String(label))
99
+ const assignedColor = key ? assignmentMap.get(key) : undefined
100
+ return assignedColor || range[index % range.length]
101
+ })
102
+ }
@@ -1,6 +1,16 @@
1
1
  import { Series } from '@cdc/core/types/Series'
2
+ import isNumber from '@cdc/core/helpers/isNumber'
2
3
  import { ChartConfig } from '../types/ChartConfig'
3
4
 
5
+ // For legend suppression (when `legend.unified` is false), treat a series as having
6
+ // data only if the active (filtered) dataset contains at least one numeric, non-zero
7
+ // value for that series. Placeholder strings (e.g. "Not applicable", "<1.0", ""),
8
+ // nullish values, and zeros are treated as "no visible data" for legend purposes.
9
+ const hasGraphableValue = (raw: unknown): boolean => {
10
+ const sanitized = typeof raw === 'string' ? raw.replace(/[,$]/g, '').trim() : raw
11
+ return isNumber(sanitized) && Number(sanitized) !== 0
12
+ }
13
+
4
14
  export const getSeriesWithData = (config: ChartConfig) => {
5
15
  const { filters, data, runtime, legend } = config
6
16
  const { colorCode } = legend
@@ -21,7 +31,7 @@ export const getSeriesWithData = (config: ChartConfig) => {
21
31
  .filter(d => !s.colorCodeSeries || d[colorCode] === s.colorCodeSeries)
22
32
  .filter(d => {
23
33
  const key = s.dynamicCategory ? s.originalDataKey : s.dataKey
24
- return d[key] || d[key] === 0
34
+ return hasGraphableValue(d[key])
25
35
  })
26
36
  }))
27
37
  .filter(s => s.data.length)
@@ -0,0 +1,16 @@
1
+ type AxisMaxOverrideProps = {
2
+ minimumValidMax: number
3
+ value: unknown
4
+ }
5
+
6
+ export const getAxisMaxOverride = ({ minimumValidMax, value }: AxisMaxOverrideProps) => {
7
+ const maxNumber = Number(value)
8
+ const hasEnteredMax = value !== undefined && value !== null && value !== ''
9
+ const hasValidMax = hasEnteredMax && Number.isFinite(maxNumber) && maxNumber >= minimumValidMax
10
+
11
+ return {
12
+ hasEnteredMax,
13
+ hasValidMax,
14
+ maxNumber
15
+ }
16
+ }
@@ -1,11 +1,18 @@
1
- import _ from 'lodash'
1
+ import capitalize from 'lodash/capitalize'
2
+ import chain from 'lodash/chain'
3
+ import filter from 'lodash/filter'
4
+ import flatMap from 'lodash/flatMap'
5
+ import map from 'lodash/map'
6
+ import min from 'lodash/min'
7
+ import round from 'lodash/round'
8
+ import uniq from 'lodash/uniq'
2
9
  import { ChartConfig } from '../types/ChartConfig'
3
10
  import * as d3 from 'd3-array'
4
11
 
5
12
  export const getBoxPlotConfig = (newConfig: ChartConfig, data: object[]) => {
6
13
  const combinedData = data
7
- const groups = _.uniq(_.map(combinedData, newConfig.xAxis.dataKey))
8
- const seriesKeys = _.map(newConfig.series, 'dataKey')
14
+ const groups = uniq(map(combinedData, newConfig.xAxis.dataKey))
15
+ const seriesKeys = map(newConfig.series, 'dataKey')
9
16
  const plots: any[] = []
10
17
 
11
18
  groups.forEach(g => {
@@ -14,13 +21,13 @@ export const getBoxPlotConfig = (newConfig: ChartConfig, data: object[]) => {
14
21
  if (!g) throw new Error('No groups resolved in box plots')
15
22
 
16
23
  // Start handle operations on combinedData
17
- const { count, sortedData } = _.chain(combinedData)
24
+ const { count, sortedData } = chain(combinedData)
18
25
  // Filter by xAxis data key
19
26
  .filter(item => item[newConfig.xAxis.dataKey] === g)
20
27
  // perform multiple operations on the filtered data
21
28
  .thru(filteredData => ({
22
29
  count: filteredData.length,
23
- sortedData: _.map(filteredData, item => Number(item[seriesKey])).sort()
30
+ sortedData: map(filteredData, item => Number(item[seriesKey])).sort()
24
31
  }))
25
32
  // get the results from the chain
26
33
  .value()
@@ -38,19 +45,19 @@ export const getBoxPlotConfig = (newConfig: ChartConfig, data: object[]) => {
38
45
  plots.push({
39
46
  columnCategory: g,
40
47
  columnMax: d3.max(nonOutliers),
41
- columnThirdQuartile: _.round(q3, newConfig.dataFormat.roundTo),
48
+ columnThirdQuartile: round(q3, newConfig.dataFormat.roundTo),
42
49
  columnMedian: Number(d3.median(sortedData)).toFixed(newConfig.dataFormat.roundTo),
43
- columnFirstQuartile: _.round(q1, newConfig.dataFormat.roundTo),
44
- columnMin: _.min(nonOutliers),
50
+ columnFirstQuartile: round(q1, newConfig.dataFormat.roundTo),
51
+ columnMin: min(nonOutliers),
45
52
  columnCount: count,
46
53
  columnSd: Number(d3.deviation(sortedData)).toFixed(newConfig.dataFormat.roundTo),
47
54
  columnMean: Number(d3.mean(sortedData)).toFixed(newConfig.dataFormat.roundTo),
48
- columnIqr: _.round(iqr, newConfig.dataFormat.roundTo),
55
+ columnIqr: round(iqr, newConfig.dataFormat.roundTo),
49
56
  values: sortedData,
50
57
  columnLowerBounds: lowerBounds,
51
58
  columnUpperBounds: upperBounds,
52
- columnOutliers: _.filter(sortedData, value => value < lowerBounds || value > upperBounds),
53
- columnNonOutliers: _.filter(sortedData, value => value >= lowerBounds && value <= upperBounds)
59
+ columnOutliers: filter(sortedData, value => value < lowerBounds || value > upperBounds),
60
+ columnNonOutliers: filter(sortedData, value => value >= lowerBounds && value <= upperBounds)
54
61
  })
55
62
  } catch (e) {
56
63
  console.error('COVE: ', e.message) // eslint-disable-line
@@ -61,7 +68,7 @@ export const getBoxPlotConfig = (newConfig: ChartConfig, data: object[]) => {
61
68
  // Generate a flat list of categories based on seriesKeys and groups
62
69
  const categories =
63
70
  seriesKeys.length > 1
64
- ? _.flatMap(groups, value => _.map(seriesKeys, key => `${_.capitalize(key)} - ${_.capitalize(value)}`))
71
+ ? flatMap(groups, value => map(seriesKeys, key => `${capitalize(key)} - ${capitalize(value)}`))
65
72
  : groups
66
73
 
67
74
  return [plots, categories]
@@ -0,0 +1,64 @@
1
+ const NICE_MANTISSAS = [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10] as const
2
+
3
+ type FinalizeTopTickMaxProps = {
4
+ currentMax: number
5
+ rawMax: number
6
+ ticks: number[]
7
+ shouldUseCleanTopTick: boolean
8
+ headroomMode?: 'none' | 'inline-label'
9
+ }
10
+
11
+ const getTickComparisonEpsilon = (tickGap: number) => Math.abs(tickGap) * 1e-10
12
+
13
+ export const getCleanTopTickMax = (value: number) => {
14
+ if (!Number.isFinite(value) || value <= 0) return value
15
+
16
+ const powerOfTenStep = Math.pow(10, Math.floor(Math.log10(value)))
17
+ const normalizedValue = value / powerOfTenStep
18
+ const mantissa = NICE_MANTISSAS.find(candidate => normalizedValue <= candidate)
19
+
20
+ return (mantissa ?? 10) * powerOfTenStep
21
+ }
22
+
23
+ export const getCleanTopTickMaxCandidates = () => [...NICE_MANTISSAS]
24
+
25
+ export const getFinalTopTickMax = ({
26
+ currentMax,
27
+ rawMax,
28
+ ticks,
29
+ shouldUseCleanTopTick,
30
+ headroomMode = 'none'
31
+ }: FinalizeTopTickMaxProps) => {
32
+ if (!shouldUseCleanTopTick || !Number.isFinite(currentMax)) return { max: currentMax }
33
+ if (!Array.isArray(ticks) || ticks.length === 0) return { max: currentMax }
34
+
35
+ const sortedTicks = Array.from(new Set(ticks.filter(Number.isFinite))).sort((a, b) => a - b)
36
+ if (sortedTicks.length === 0) return { max: currentMax }
37
+
38
+ const tickGap = sortedTicks.slice(1).reduce((smallestGap, tick, index) => {
39
+ const gap = tick - sortedTicks[index]
40
+ return gap > 0 ? Math.min(smallestGap, gap) : smallestGap
41
+ }, Infinity)
42
+
43
+ if (!Number.isFinite(tickGap) || tickGap <= 0) return { max: currentMax }
44
+
45
+ const targetMax = Math.max(currentMax, Number.isFinite(rawMax) ? rawMax : currentMax)
46
+ const tickComparisonEpsilon = getTickComparisonEpsilon(tickGap)
47
+ let finalMax = sortedTicks[sortedTicks.length - 1]
48
+
49
+ while (finalMax + tickComparisonEpsilon < targetMax) {
50
+ finalMax = Number((finalMax + tickGap).toPrecision(15))
51
+ sortedTicks.push(finalMax)
52
+ }
53
+
54
+ if (
55
+ headroomMode === 'inline-label' &&
56
+ Number.isFinite(rawMax) &&
57
+ finalMax - rawMax <= tickGap * 0.25 + tickComparisonEpsilon
58
+ ) {
59
+ finalMax = Number((finalMax + tickGap).toPrecision(15))
60
+ sortedTicks.push(finalMax)
61
+ }
62
+
63
+ return { max: finalMax, tickValues: sortedTicks }
64
+ }
@@ -10,6 +10,7 @@ import {
10
10
  divergentColorDistribution,
11
11
  colorblindColorDistribution
12
12
  } from '@cdc/core/helpers/palettes/colorDistributions'
13
+ import { applySeriesColorAssignmentsToRange } from './colorAssignmentHelpers'
13
14
 
14
15
  export const getColorScale = (config: ChartConfig): ((value: string) => string) => {
15
16
  const configPalette = ['Paired Bar', 'Deviation Bar'].includes(config.visualizationType)
@@ -32,18 +33,24 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
32
33
  // Migrate old palette name if needed
33
34
  const migratedPaletteName = configPalette ? configPalette : getFallbackColorPalette(config)
34
35
 
36
+ const domain = config.runtime.seriesLabelsAll
37
+
38
+ const customColorsOrdered = config.general?.palette?.customColorsOrdered
39
+
35
40
  // Check for customColorsOrdered first (direct 1-to-1 mapping, no distribution needed)
36
- if (config.general?.palette?.customColorsOrdered && Array.isArray(config.general.palette.customColorsOrdered)) {
37
- const customColorsOrdered = config.general.palette.customColorsOrdered
41
+ if (Array.isArray(customColorsOrdered) && customColorsOrdered.length > 0) {
42
+ const range = applySeriesColorAssignmentsToRange(config, domain, customColorsOrdered)
38
43
  return scaleOrdinal({
39
- domain: config.runtime.seriesLabelsAll,
40
- range: customColorsOrdered,
44
+ domain,
45
+ range,
41
46
  unknown: null
42
47
  })
43
48
  }
44
49
 
50
+ const customColors = config.general?.palette?.customColors
51
+ const isUsingCustomColors = Array.isArray(customColors) && customColors.length > 0
45
52
  let palette =
46
- config.general?.palette?.customColors ||
53
+ (isUsingCustomColors ? customColors : undefined) ||
47
54
  palettesSource[migratePaletteWithMap(migratedPaletteName, paletteMigrationMap, false)] ||
48
55
  palettesSource[configPalette]
49
56
 
@@ -58,8 +65,8 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
58
65
  // Apply enhanced color distribution (same logic as pie charts)
59
66
  const paletteVersion = getColorPaletteVersion(config)
60
67
 
61
- // Skip enhanced distribution if not v2, too many keys, or wrong palette length
62
- if (paletteVersion !== 2 || numberOfKeys > 9 || palette.length !== 9) {
68
+ // Skip enhanced distribution if using custom colors, not v2, too many keys, or wrong palette length
69
+ if (isUsingCustomColors || paletteVersion !== 2 || numberOfKeys > 9 || palette.length !== 9) {
63
70
  // Use existing logic for v1 palettes and other cases
64
71
  while (numberOfKeys > palette.length) {
65
72
  palette = palette.concat(palette)
@@ -90,9 +97,11 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
90
97
  }
91
98
  }
92
99
 
100
+ const range = applySeriesColorAssignmentsToRange(config, domain, palette)
101
+
93
102
  return scaleOrdinal({
94
- domain: config.runtime.seriesLabelsAll,
95
- range: palette,
103
+ domain,
104
+ range,
96
105
  unknown: null
97
106
  })
98
107
  }
@@ -1,4 +1,3 @@
1
- import _ from 'lodash'
2
1
  import { ChartConfig } from '../types/ChartConfig'
3
2
  import * as d3 from 'd3-array'
4
3
 
@@ -1,5 +1,4 @@
1
1
  import { isDateScale } from '@cdc/core/helpers/cove/date'
2
- import _ from 'lodash'
3
2
  import { ChartConfig } from '../types/ChartConfig'
4
3
  export const getExcludedData = (newConfig: ChartConfig, data: object[]) => {
5
4
  if (!Array.isArray(data)) {
@@ -0,0 +1,27 @@
1
+ import pick from 'lodash/pick'
2
+ import values from 'lodash/values'
3
+ import { ChartConfig } from '../types/ChartConfig'
4
+
5
+ type GetHasBoundarySuppressionProps = {
6
+ rows?: Object[]
7
+ config: ChartConfig
8
+ }
9
+
10
+ export const getHasBoundarySuppression = ({ rows, config }: GetHasBoundarySuppressionProps) => {
11
+ if (!Array.isArray(rows) || rows.length === 0) return false
12
+ const suppressionRules = config.preliminaryData?.filter(({ type, style }) => type === 'suppression' && style) || []
13
+ if (!suppressionRules.length) return false
14
+
15
+ const boundaryRows = [rows[0], rows[rows.length - 1]].filter(Boolean)
16
+
17
+ return boundaryRows.some(item =>
18
+ suppressionRules.some(({ column, value }) => {
19
+ const seriesValues = values(pick(item, config.runtime?.seriesKeys))
20
+ const dynamicCategory = config.series[0]?.dynamicCategory
21
+ const match = column ? item[column] === value : seriesValues.includes(value)
22
+ const dynamic = dynamicCategory && (item[dynamicCategory] === column || !column)
23
+
24
+ return Boolean(match || dynamic)
25
+ })
26
+ )
27
+ }
@@ -1,5 +1,6 @@
1
1
  import { ChartConfig } from '../types/ChartConfig'
2
- import _ from 'lodash'
2
+ import { getCleanTopTickMax } from './getCleanTopTickMax'
3
+ import { getAxisMaxOverride } from './getAxisMaxOverride'
3
4
 
4
5
  type GetMinMaxProps = {
5
6
  /** config - standard chart config */
@@ -18,6 +19,8 @@ type GetMinMaxProps = {
18
19
  isAllLine: boolean
19
20
  /** convertLineToBarGraph - whether line charts should be rendered as bar graphs */
20
21
  convertLineToBarGraph?: boolean
22
+ /** whether selected-domain boundary rows include suppression tokens */
23
+ hasBoundarySuppression?: boolean
21
24
  }
22
25
 
23
26
  const getMinMax = ({
@@ -28,7 +31,8 @@ const getMinMax = ({
28
31
  data,
29
32
  isAllLine,
30
33
  tableData,
31
- convertLineToBarGraph
34
+ convertLineToBarGraph,
35
+ hasBoundarySuppression = false
32
36
  }: GetMinMaxProps) => {
33
37
  let min = 0
34
38
  let max = 0
@@ -45,14 +49,18 @@ const getMinMax = ({
45
49
  const { max: enteredMaxValue, min: enteredMinValue } = config.runtime.yAxis
46
50
  const paddingAddedToAxis = config.yAxis.enablePadding ? 1 + config.yAxis.scalePadding / 100 : 1
47
51
  const isLogarithmicAxis = config.yAxis.type === 'logarithmic'
48
- // do validation bafore applying t0 charts
49
- const isMaxValid = existPositiveValue ? Number(enteredMaxValue) >= maxValue : Number(enteredMaxValue) >= 0
52
+ // Validate before applying chart-specific domain rules.
53
+ const { hasValidMax: hasValidExplicitLeftMax, maxNumber: enteredMaxNumber } = getAxisMaxOverride({
54
+ value: enteredMaxValue,
55
+ minimumValidMax: existPositiveValue ? maxValue : 0
56
+ })
57
+ const shouldCleanAutoLeftMax = config.yAxis.autoMaxStrategy === 'clean-top-tick' && !hasValidExplicitLeftMax
50
58
  const isMinValid = isLogarithmicAxis
51
59
  ? Number(enteredMinValue) >= 0
52
60
  : (Number(enteredMinValue) <= 0 && minValue >= 0) || (Number(enteredMinValue) <= minValue && minValue < 0)
53
61
 
54
62
  min = enteredMinValue && isMinValid ? Number(enteredMinValue) : minValue
55
- max = enteredMaxValue && isMaxValid ? Number(enteredMaxValue) : Number.MIN_VALUE
63
+ max = hasValidExplicitLeftMax ? enteredMaxNumber : Number.MIN_VALUE
56
64
  const { lower, upper } = config?.confidenceKeys || {}
57
65
 
58
66
  const useBrushFullRange = config.xAxis.brushActive && !config.xAxis.brushDynamicYAxis
@@ -135,8 +143,8 @@ const getMinMax = ({
135
143
  leftMax = findMaxFromSeriesKeys(dataForMinMax, leftAxisSeriesItems, leftMax, 'left')
136
144
  rightMax = findMaxFromSeriesKeys(dataForMinMax, rightAxisSeriesItems, rightMax, 'right')
137
145
 
138
- if (leftMax < Number(enteredMaxValue)) {
139
- leftMax = Number(enteredMaxValue)
146
+ if (hasValidExplicitLeftMax && leftMax < enteredMaxNumber) {
147
+ leftMax = enteredMaxNumber
140
148
  }
141
149
  } catch (e) {
142
150
  console.error(e.message)
@@ -180,23 +188,10 @@ const getMinMax = ({
180
188
  const numEnteredMin = Number(enteredMinValue)
181
189
  const isMinValid = isLogarithmicAxis ? numEnteredMin >= 0 && numEnteredMin < minValue : numEnteredMin < minValue
182
190
 
183
- const suppressedMinValue = tableData?.some((item, i, arr) =>
184
- config.preliminaryData?.some(({ type, style, column, value }) => {
185
- if (type !== 'suppression' || !style) return false
186
-
187
- const values = _.values(_.pick(item, config.runtime?.seriesKeys))
188
- const dynamicCategory = config.series[0].dynamicCategory
189
-
190
- const match = column ? item[column] === value : values.includes(value)
191
- const dynamic = dynamicCategory && (item[dynamicCategory] === column || !column)
192
-
193
- return (match || dynamic) && (i === 0 || i === arr.length - 1)
194
- })
195
- )
196
-
197
191
  const isCategorical = config.yAxis.type === 'categorical'
198
192
 
199
- min = enteredMinValue !== '' && isMinValid ? numEnteredMin : suppressedMinValue ? 0 : isCategorical ? 0 : minValue
193
+ min =
194
+ enteredMinValue !== '' && isMinValid ? numEnteredMin : hasBoundarySuppression ? 0 : isCategorical ? 0 : minValue
200
195
  }
201
196
 
202
197
  //If data value max wasn't provided, calculate it
@@ -205,6 +200,14 @@ const getMinMax = ({
205
200
  max = existPositiveValue ? maxValue : 0
206
201
  }
207
202
 
203
+ if (shouldCleanAutoLeftMax) {
204
+ if (config.visualizationType === 'Combo') {
205
+ leftMax = getCleanTopTickMax(leftMax)
206
+ } else {
207
+ max = getCleanTopTickMax(max)
208
+ }
209
+ }
210
+
208
211
  //Adds Y Axis data padding if applicable
209
212
  if (config.runtime.yAxis.paddingPercent) {
210
213
  let paddingValue = (max - min) * config.runtime.yAxis.paddingPercent
@@ -1,5 +1,3 @@
1
- import _ from 'lodash'
2
-
3
1
  export const getNewRuntime = (visualizationConfig, newFilteredData) => {
4
2
  const runtime = visualizationConfig.runtime ? { ...visualizationConfig.runtime } : {}
5
3
  runtime.series = []
@@ -1,13 +1,13 @@
1
- import _ from 'lodash'
1
+ import toNumber from 'lodash/toNumber'
2
2
 
3
3
  export const getPiePercent = (data: Record<string, any>[], seriesKey: string): Record<string, any>[] => {
4
4
  // getonly the numeric values for each seriesKey
5
- const numericValues = data.map(row => _.toNumber(row[seriesKey])).filter(v => !Number.isNaN(v))
5
+ const numericValues = data.map(row => toNumber(row[seriesKey])).filter(v => !Number.isNaN(v))
6
6
 
7
7
  const total = numericValues.reduce((sum, v) => sum + v, 0)
8
8
 
9
9
  return data.map(row => {
10
- const raw = _.toNumber(row[seriesKey])
10
+ const raw = toNumber(row[seriesKey])
11
11
  if (Number.isNaN(raw)) {
12
12
  // skip non-numeric / undefined
13
13
  return row
@@ -0,0 +1,38 @@
1
+ import { ChartConfig } from '../types/ChartConfig'
2
+ import { AxisFilterDomainBehavior } from '@cdc/core/types/Axis'
3
+
4
+ const hasRows = (data?: Object[]) => Array.isArray(data) && data.length > 0
5
+
6
+ export const getYAxisFilterDomainBehavior = (config: ChartConfig): AxisFilterDomainBehavior => {
7
+ return config.yAxis?.filterDomainBehavior === 'stable' ? 'stable' : 'dynamic'
8
+ }
9
+
10
+ export const getYAxisDomainData = ({
11
+ config,
12
+ data,
13
+ tableData,
14
+ fullEligibleDomainData
15
+ }: {
16
+ config: ChartConfig
17
+ data: Object[]
18
+ tableData?: Object[]
19
+ fullEligibleDomainData?: Object[]
20
+ }): Object[] => {
21
+ const brushActive = Boolean(config.xAxis?.brushActive)
22
+ const brushDynamicYAxis = Boolean(config.xAxis?.brushDynamicYAxis)
23
+
24
+ if (brushActive && brushDynamicYAxis) {
25
+ return data
26
+ }
27
+
28
+ const behavior = getYAxisFilterDomainBehavior(config)
29
+ if (behavior === 'stable' && hasRows(fullEligibleDomainData)) {
30
+ return fullEligibleDomainData
31
+ }
32
+
33
+ if (brushActive && !brushDynamicYAxis && hasRows(tableData)) {
34
+ return tableData
35
+ }
36
+
37
+ return data
38
+ }
@@ -0,0 +1,40 @@
1
+ import { type ChartConfig } from '../types/ChartConfig'
2
+
3
+ type YAxisFinalizationEligibilityProps = {
4
+ config: ChartConfig
5
+ hasSpacedInlineLabel: boolean
6
+ hasValidExplicitLeftMax: boolean
7
+ isHorizontal: boolean
8
+ }
9
+
10
+ const chartUsesStandardPrimaryYScale = (config: ChartConfig) => {
11
+ return !['Box Plot', 'Bump Chart', 'Forest Plot'].includes(config.visualizationType)
12
+ }
13
+
14
+ const isLinearOrLegacyYAxis = (config: ChartConfig) => {
15
+ const yAxisType = config.yAxis.type
16
+
17
+ return yAxisType === undefined || yAxisType === '' || yAxisType === 'linear'
18
+ }
19
+
20
+ export const getYAxisFinalizationEligibility = ({
21
+ config,
22
+ hasSpacedInlineLabel,
23
+ hasValidExplicitLeftMax,
24
+ isHorizontal
25
+ }: YAxisFinalizationEligibilityProps) => {
26
+ const hasStandardLinearPrimaryYAxis =
27
+ !isHorizontal && isLinearOrLegacyYAxis(config) && chartUsesStandardPrimaryYScale(config)
28
+ const supportsInlineLabelHeadroom = hasStandardLinearPrimaryYAxis && config.visualizationType !== 'Combo'
29
+ const shouldUseInlineLabelHeadroom = hasSpacedInlineLabel && supportsInlineLabelHeadroom
30
+ const shouldUseCleanTopTick =
31
+ !hasValidExplicitLeftMax && (config.yAxis.autoMaxStrategy === 'clean-top-tick' || shouldUseInlineLabelHeadroom)
32
+
33
+ return {
34
+ hasStandardLinearPrimaryYAxis,
35
+ supportsInlineLabelHeadroom,
36
+ shouldUseCleanTopTick,
37
+ shouldUseInlineLabelHeadroom,
38
+ shouldFinalizePrimaryYAxis: shouldUseCleanTopTick && hasStandardLinearPrimaryYAxis
39
+ }
40
+ }
@@ -0,0 +1,10 @@
1
+ import { ChartConfig } from '../types/ChartConfig'
2
+
3
+ export const hasSpacedInlineLabel = (config: ChartConfig): boolean => {
4
+ const inlineLabel = config.yAxis?.inlineLabel
5
+ if (inlineLabel && inlineLabel.includes(' ')) {
6
+ return true
7
+ }
8
+
9
+ return false
10
+ }
@@ -1,4 +1,4 @@
1
- import _ from 'lodash'
1
+ import uniq from 'lodash/uniq'
2
2
 
3
3
  export const isConvertLineToBarGraph = (configObj, filteredData) => {
4
4
  const { allowLineToBarGraph, series, visualizationType, xAxis } = configObj
@@ -6,6 +6,6 @@ export const isConvertLineToBarGraph = (configObj, filteredData) => {
6
6
  const lineWithLessThanThreePoints = visualizationType === 'Line' && filteredData?.length < 3
7
7
  const isDynamicSeries = series?.some(series => series.dynamicCategory)
8
8
  const isDynamicWithLessThanThreePoints =
9
- isDynamicSeries && _.uniq(filteredData?.map(data => data[xAxis.dataKey])).length <= 2
9
+ isDynamicSeries && uniq(filteredData?.map(data => data[xAxis.dataKey])).length <= 2
10
10
  return lineWithLessThanThreePoints || isDynamicWithLessThanThreePoints
11
11
  }