@cdc/chart 4.25.10 → 4.25.11

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 (85) hide show
  1. package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
  2. package/dist/cdcchart.js +36258 -34658
  3. package/examples/feature/__data__/planet-example-data.json +1 -1
  4. package/examples/feature/boxplot/valid-boxplot.csv +38 -17
  5. package/examples/private/DEV-11825.json +573 -0
  6. package/examples/private/na.json +913 -0
  7. package/examples/private/test-data.csv +28 -0
  8. package/index.html +2 -121
  9. package/package.json +4 -4
  10. package/src/CdcChart.tsx +8 -11
  11. package/src/CdcChartComponent.tsx +256 -87
  12. package/src/_stories/Chart.Combo.stories.tsx +18 -0
  13. package/src/_stories/Chart.Forecast.stories.tsx +36 -0
  14. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
  15. package/src/_stories/Chart.Patterns.stories.tsx +2 -1
  16. package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
  17. package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
  18. package/src/_stories/ChartAnnotation.stories.tsx +6 -3
  19. package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
  20. package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
  21. package/src/_stories/ChartEditor.stories.tsx +1 -2
  22. package/src/_stories/_mock/combo.json +451 -0
  23. package/src/_stories/_mock/editor-test-configs.json +376 -0
  24. package/src/_stories/_mock/editor-test-datasets.json +477 -0
  25. package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
  26. package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
  27. package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
  28. package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
  29. package/src/_stories/_mock/pie_config.json +257 -62
  30. package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
  31. package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
  32. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
  33. package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
  34. package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
  35. package/src/components/Annotations/components/findNearestDatum.ts +6 -41
  36. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
  37. package/src/components/AreaChart/index.tsx +1 -2
  38. package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -4
  39. package/src/components/BarChart/components/BarChart.Vertical.tsx +3 -2
  40. package/src/components/BoxPlot/helpers/index.ts +3 -3
  41. package/src/components/Brush/BrushChart.tsx +1 -1
  42. package/src/components/EditorPanel/EditorPanel.tsx +199 -190
  43. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
  44. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
  45. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +102 -55
  46. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
  47. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
  48. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +75 -21
  49. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  50. package/src/components/EditorPanel/editor-panel.scss +0 -20
  51. package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
  52. package/src/components/Forecasting/Forecasting.tsx +139 -21
  53. package/src/components/Legend/Legend.Component.tsx +16 -9
  54. package/src/components/Legend/helpers/createFormatLabels.tsx +181 -181
  55. package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
  56. package/src/components/LineChart/LineChartProps.ts +0 -3
  57. package/src/components/LineChart/helpers.ts +1 -1
  58. package/src/components/LineChart/index.tsx +36 -13
  59. package/src/components/LinearChart.tsx +75 -80
  60. package/src/components/Regions/components/Regions.tsx +3 -24
  61. package/src/components/Sankey/types/index.ts +1 -1
  62. package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
  63. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  64. package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
  65. package/src/components/SmallMultiples/index.ts +2 -0
  66. package/src/data/initial-state.js +13 -1
  67. package/src/helpers/buildForecastPaletteOptions.ts +0 -38
  68. package/src/helpers/getColorScale.ts +10 -0
  69. package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
  70. package/src/helpers/getYAxisAutoPadding.ts +53 -0
  71. package/src/helpers/smallMultiplesHelpers.ts +529 -0
  72. package/src/hooks/useProgrammaticTooltip.ts +96 -0
  73. package/src/hooks/useScales.ts +88 -34
  74. package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
  75. package/src/hooks/useTooltip.tsx +60 -15
  76. package/src/scss/main.scss +1 -80
  77. package/src/store/chart.actions.ts +2 -0
  78. package/src/store/chart.reducer.ts +4 -0
  79. package/src/types/ChartConfig.ts +24 -6
  80. package/src/types/ChartContext.ts +3 -0
  81. package/src/_stories/_mock/pie_data.json +0 -218
  82. package/src/components/AreaChart/components/AreaChart.jsx +0 -109
  83. package/src/helpers/sort.ts +0 -7
  84. package/src/hooks/useActiveElement.js +0 -19
  85. package/src/hooks/useChartClasses.js +0 -41
@@ -0,0 +1,271 @@
1
+ import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react'
2
+ import SmallMultipleTile from './SmallMultipleTile'
3
+ import ConfigContext from '../../ConfigContext'
4
+ import useReduceData from '../../hooks/useReduceData'
5
+ import useScales from '../../hooks/useScales'
6
+ import { createCombinedDataForYAxis, applyTileOrder, createTileColorScale } from '../../helpers/smallMultiplesHelpers'
7
+ import { isMobileSmallMultiplesViewport } from '@cdc/core/helpers/viewports'
8
+ import './SmallMultiples.css'
9
+ import { ChartConfig } from '../../types/ChartConfig'
10
+
11
+ interface SmallMultiplesProps {
12
+ config: ChartConfig
13
+ data: object[]
14
+ svgRef?: React.RefObject<SVGAElement>
15
+ parentWidth?: number
16
+ parentHeight?: number
17
+ }
18
+
19
+ type TileItem = {
20
+ key: string | number
21
+ mode: 'by-series' | 'by-column'
22
+ seriesKey?: string
23
+ tileValue?: any
24
+ tileColumn?: string
25
+ }
26
+
27
+ type ChartRefWithTooltipMethods = {
28
+ triggerTooltipAtDataValue?: (xAxisValue: any, yCoordinate: number) => void
29
+ hideTooltip?: () => void
30
+ }
31
+
32
+ type TileHeaderRows = Array<Array<HTMLDivElement>>
33
+
34
+ type TileHeaderEntries = Array<[string, HTMLDivElement]>
35
+
36
+ const SmallMultiples: React.FC<SmallMultiplesProps> = ({ config, data, svgRef, parentWidth, parentHeight }) => {
37
+ const { currentViewport, colorScale, parentRef } = useContext(ConfigContext)
38
+ const { mode, tileColumn, tilesPerRowDesktop, tilesPerRowMobile } = config.smallMultiples || {}
39
+
40
+ const isMobile = isMobileSmallMultiplesViewport(currentViewport)
41
+ const tilesPerRow = isMobile ? tilesPerRowMobile || 1 : tilesPerRowDesktop || 3
42
+
43
+ // Figure out what objects to iterate over based on mode - memoized to prevent recalculation
44
+ const tileItems = useMemo<Array<TileItem>>(() => {
45
+ let items: Array<TileItem> = []
46
+
47
+ if (mode === 'by-series') {
48
+ items = config.series.map(series => ({
49
+ key: series.dataKey,
50
+ mode: 'by-series' as const,
51
+ seriesKey: series.dataKey
52
+ }))
53
+ } else if (mode === 'by-column') {
54
+ const uniqueValues = Array.from(new Set(data.map(row => row[tileColumn])))
55
+ .filter(val => val != null)
56
+ .sort()
57
+ items = uniqueValues.map(value => ({
58
+ key: value,
59
+ mode: 'by-column' as const,
60
+ tileValue: value,
61
+ tileColumn: tileColumn
62
+ }))
63
+ }
64
+
65
+ // Apply tile ordering based on user preference
66
+ return applyTileOrder(
67
+ items,
68
+ config.smallMultiples?.tileOrderType || 'asc',
69
+ config.smallMultiples?.tileOrder,
70
+ config
71
+ )
72
+ }, [
73
+ mode,
74
+ config.series,
75
+ data,
76
+ tileColumn,
77
+ config.smallMultiples?.tileOrderType,
78
+ config.smallMultiples?.tileOrder,
79
+ config.smallMultiples?.tileTitles
80
+ ])
81
+
82
+ // Calculate the grid styling based on tiles per row
83
+ const gridGap = isMobile ? '1rem' : '2rem'
84
+ const gridStyle = {
85
+ gridTemplateColumns: `repeat(${tilesPerRow}, 1fr)`,
86
+ gap: gridGap
87
+ }
88
+
89
+ const [tileHeights, setTileHeights] = useState<Record<string, number>>({})
90
+
91
+ // Refs to all LinearChart instances for tooltip coordination
92
+ const chartRefs = useRef<Record<string, ChartRefWithTooltipMethods>>({})
93
+
94
+ // Refs to all tile header elements for height alignment
95
+ const headerRefs = useRef<Record<string, HTMLDivElement | null>>({})
96
+
97
+ // Create combined data and config for consistent Y-axis calculation
98
+ const combinedDataForYAxis = useMemo(
99
+ () => createCombinedDataForYAxis(config, data, tileItems),
100
+ [config, data, tileItems]
101
+ )
102
+
103
+ const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(
104
+ combinedDataForYAxis.config,
105
+ combinedDataForYAxis.data
106
+ )
107
+
108
+ const inlineLabel = config.yAxis?.inlineLabel
109
+ const inlineLabelHasNoSpace = !inlineLabel?.includes(' ')
110
+ const needsYAxisAutoPadding = inlineLabel && !inlineLabelHasNoSpace
111
+
112
+ const { min, max } = useScales({
113
+ config: combinedDataForYAxis.config,
114
+ data: combinedDataForYAxis.data,
115
+ tableData: combinedDataForYAxis.data,
116
+ minValue,
117
+ maxValue,
118
+ existPositiveValue,
119
+ isAllLine,
120
+ xAxisDataMapped: [],
121
+ xMax: parentWidth,
122
+ yMax: parentHeight,
123
+ needsYAxisAutoPadding,
124
+ currentViewport
125
+ })
126
+
127
+ // Use consistent Y-axis if the feature is enabled and we have valid values
128
+ const globalYAxisValues = useMemo(() => {
129
+ if (config.smallMultiples?.independentYAxis) return null
130
+ if (typeof min !== 'number' || typeof max !== 'number') return null
131
+ if (combinedDataForYAxis.data.length === 0) return null
132
+
133
+ return { min, max }
134
+ }, [config.smallMultiples?.independentYAxis, min, max, combinedDataForYAxis.data.length])
135
+
136
+ const numberOfRows = useMemo(() => Math.ceil(tileItems.length / tilesPerRow), [tileItems.length, tilesPerRow])
137
+
138
+ // Handle tile height changes from ResizeObserver
139
+ const handleTileHeightChange = useCallback((tileKey: string, height: number) => {
140
+ setTileHeights(prev => ({ ...prev, [tileKey]: height }))
141
+ }, [])
142
+
143
+ // Handle tooltip synchronization across small multiple tiles
144
+ const handleChartHover = useCallback(
145
+ (sourceTileKey: string, xAxisValue: any, yCoordinate: number) => {
146
+ if (!config.smallMultiples?.synchronizedTooltips) return
147
+
148
+ // If xAxisValue is null, it means mouse left the chart - hide all tooltips
149
+ if (xAxisValue === null) {
150
+ Object.entries(chartRefs.current).forEach(([tileKey, chartRef]) => {
151
+ if (tileKey !== sourceTileKey && chartRef?.hideTooltip) {
152
+ chartRef.hideTooltip()
153
+ }
154
+ })
155
+ return
156
+ }
157
+
158
+ // For each OTHER chart in the grid, trigger tooltip at same X-axis data value and exact Y coordinate
159
+ Object.entries(chartRefs.current).forEach(([tileKey, chartRef]) => {
160
+ if (tileKey === sourceTileKey || !chartRef) return
161
+
162
+ if (chartRef.triggerTooltipAtDataValue) {
163
+ chartRef.triggerTooltipAtDataValue(xAxisValue, yCoordinate)
164
+ }
165
+ })
166
+ },
167
+ [config.smallMultiples?.synchronizedTooltips]
168
+ )
169
+
170
+ // Align tile header heights per row
171
+ useEffect(() => {
172
+ const headerEntries = Object.entries(headerRefs.current).filter(([_, ref]) => ref) as TileHeaderEntries
173
+ if (headerEntries.length === 0) return
174
+
175
+ // Group headers by row based on their index in tileItems
176
+ const headersByRow: TileHeaderRows = []
177
+
178
+ tileItems.forEach((item, index) => {
179
+ const rowIndex = Math.floor(index / tilesPerRow)
180
+ const header = headerRefs.current[String(item.key)]
181
+
182
+ headersByRow[rowIndex] ||= []
183
+ headersByRow[rowIndex].push(header)
184
+ })
185
+
186
+ // For each row, find the header with longest text and align others to it
187
+ headersByRow.forEach(rowHeaders => {
188
+ let longestHeader: HTMLDivElement | null = null
189
+ let maxTextLength = 0
190
+
191
+ rowHeaders.forEach(header => {
192
+ const textLength = header.textContent?.length || 0
193
+ if (textLength > maxTextLength) {
194
+ maxTextLength = textLength
195
+ longestHeader = header
196
+ }
197
+ })
198
+
199
+ if (!longestHeader) return
200
+
201
+ // Get the height of the longest header in this row
202
+ const targetHeight = longestHeader.offsetHeight
203
+
204
+ // Apply that height to all other headers in this row
205
+ rowHeaders.forEach(header => {
206
+ header.style.minHeight = header !== longestHeader ? `${targetHeight}px` : 'auto'
207
+ })
208
+ })
209
+ }, [tileItems, tilesPerRow])
210
+
211
+ // Calculate container height from measured tile heights
212
+ useEffect(() => {
213
+ if (!parentRef.current) return
214
+
215
+ const measuredHeights = Object.values(tileHeights)
216
+ if (measuredHeights.length === 0) {
217
+ return
218
+ }
219
+
220
+ const maxTileHeight = Math.max(...measuredHeights)
221
+ const gapSize = isMobile ? 18 : 36
222
+ const totalGapsHeight = (numberOfRows - 1) * gapSize
223
+ const totalHeight = numberOfRows * maxTileHeight + totalGapsHeight
224
+
225
+ parentRef.current.style.height = `${totalHeight}px`
226
+ }, [tileHeights, numberOfRows, isMobile, parentRef, tilesPerRow])
227
+
228
+ if (tileItems.length === 0) {
229
+ return null
230
+ }
231
+
232
+ return (
233
+ <div className='small-multiples-container'>
234
+ <div className='small-multiples-grid' style={gridStyle}>
235
+ {tileItems.map((item, index) => {
236
+ const customColorScale = createTileColorScale(item, config, colorScale, index, tileItems.length)
237
+
238
+ return (
239
+ <SmallMultipleTile
240
+ key={item.key}
241
+ tileKey={String(item.key)}
242
+ mode={item.mode}
243
+ seriesKey={item.seriesKey}
244
+ tileValue={item.tileValue}
245
+ tileColumn={item.tileColumn}
246
+ customColorScale={customColorScale}
247
+ config={config}
248
+ data={data}
249
+ svgRef={svgRef}
250
+ parentWidth={parentWidth}
251
+ parentHeight={parentHeight}
252
+ globalYAxisMax={globalYAxisValues?.max}
253
+ globalYAxisMin={globalYAxisValues?.min}
254
+ isFirstInRow={index % tilesPerRow === 0}
255
+ onHeightChange={handleTileHeightChange}
256
+ onChartRef={ref => {
257
+ chartRefs.current[String(item.key)] = ref
258
+ }}
259
+ onHeaderRef={ref => {
260
+ headerRefs.current[String(item.key)] = ref
261
+ }}
262
+ onChartHover={(xAxisValue, yCoordinate) => handleChartHover(String(item.key), xAxisValue, yCoordinate)}
263
+ />
264
+ )
265
+ })}
266
+ </div>
267
+ </div>
268
+ )
269
+ }
270
+
271
+ export default SmallMultiples
@@ -0,0 +1,2 @@
1
+ export { default as SmallMultiples } from './SmallMultiples'
2
+ export { default as SmallMultipleTile } from './SmallMultipleTile'
@@ -198,7 +198,19 @@ const createInitialState = () => {
198
198
  patterns: {},
199
199
  patternField: ''
200
200
  },
201
-
201
+ smallMultiples: {
202
+ mode: '',
203
+ tileColumn: '',
204
+ tilesPerRowDesktop: 3,
205
+ tilesPerRowMobile: 1,
206
+ tileOrder: [],
207
+ tileOrderType: 'asc',
208
+ tileTitles: {},
209
+ independentYAxis: false,
210
+ colorMode: 'same',
211
+ synchronizedTooltips: true,
212
+ showAreaUnderLine: true
213
+ },
202
214
  exclusions: {
203
215
  active: false,
204
216
  keys: []
@@ -69,41 +69,3 @@ export const buildForecastPaletteOptions = (
69
69
 
70
70
  return paletteOptions
71
71
  }
72
-
73
- /**
74
- * Normalizes a palette value to match the standardized hyphenated format
75
- * and migrates v1 palette names to v2 equivalents
76
- *
77
- * @param value - The palette name to normalize
78
- * @param paletteVersion - The palette version (1 or 2) from the config
79
- * @returns The normalized palette name in lowercase with hyphens, or 'Select' if empty
80
- */
81
- export const normalizePaletteValue = (value: string | undefined, paletteVersion: number = 1): string => {
82
- if (!value) return 'Select'
83
-
84
- // Convert to lowercase with hyphens for consistent matching
85
- const normalized = value.toLowerCase().replace(/ /g, '-').replace(/_/g, '-')
86
-
87
- // If v2, migrate v1-only palette names to their v2 equivalents
88
- if (paletteVersion === 2) {
89
- const V1_TO_V2_MIGRATION: Record<string, string> = {
90
- // Sequential Blue variants → sequential-blue
91
- 'sequential-blue-two': 'sequential-blue',
92
- 'sequential-blue-three': 'sequential-blue',
93
- 'sequential-blue-2-(mpx)': 'sequential-blue',
94
- 'sequential-blue-tworeverse': 'sequential-bluereverse',
95
- 'sequential-blue-threereverse': 'sequential-bluereverse',
96
- 'sequential-blue-2-(mpx)reverse': 'sequential-bluereverse',
97
-
98
- // Sequential Orange variants → sequential-orange
99
- 'sequential-orange-two': 'sequential-orange',
100
- 'sequential-orange-(mpx)': 'sequential-orange',
101
- 'sequential-orange-tworeverse': 'sequential-orangereverse',
102
- 'sequential-orange-(mpx)reverse': 'sequential-orangereverse'
103
- }
104
-
105
- return V1_TO_V2_MIGRATION[normalized] || normalized
106
- }
107
-
108
- return normalized
109
- }
@@ -32,6 +32,16 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
32
32
  // Migrate old palette name if needed
33
33
  const migratedPaletteName = configPalette ? configPalette : getFallbackColorPalette(config)
34
34
 
35
+ // 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
38
+ return scaleOrdinal({
39
+ domain: config.runtime.seriesLabelsAll,
40
+ range: customColorsOrdered,
41
+ unknown: null
42
+ })
43
+ }
44
+
35
45
  let palette =
36
46
  config.general?.palette?.customColors ||
37
47
  palettesSource[migratePaletteWithMap(migratedPaletteName, paletteMigrationMap, false)] ||
@@ -1,9 +1,7 @@
1
1
  import { ChartConfig } from '../types/ChartConfig'
2
2
  import _ from 'lodash'
3
- import ConfigContext from '../ConfigContext'
4
- import { useContext } from 'react'
5
3
 
6
- type UseMinMaxProps = {
4
+ type GetMinMaxProps = {
7
5
  /** config - standard chart config */
8
6
  config: ChartConfig
9
7
  /** minValue - starting minimum value */
@@ -18,9 +16,20 @@ type UseMinMaxProps = {
18
16
  tableData: Object[]
19
17
  /** isAllLine: if all series are line type including dashed lines */
20
18
  isAllLine: boolean
19
+ /** convertLineToBarGraph - whether line charts should be rendered as bar graphs */
20
+ convertLineToBarGraph?: boolean
21
21
  }
22
22
 
23
- const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAllLine, tableData }: UseMinMaxProps) => {
23
+ const getMinMax = ({
24
+ config,
25
+ minValue,
26
+ maxValue,
27
+ existPositiveValue,
28
+ data,
29
+ isAllLine,
30
+ tableData,
31
+ convertLineToBarGraph
32
+ }: GetMinMaxProps) => {
24
33
  let min = 0
25
34
  let max = 0
26
35
 
@@ -28,8 +37,6 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
28
37
  let leftMax = 0
29
38
  let rightMax = 0
30
39
 
31
- const { convertLineToBarGraph } = useContext(ConfigContext)
32
-
33
40
  if (!data) {
34
41
  return { min, max }
35
42
  }
@@ -238,4 +245,4 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
238
245
 
239
246
  return { min, max, leftMax, rightMax }
240
247
  }
241
- export default useMinMax
248
+ export default getMinMax
@@ -0,0 +1,53 @@
1
+ import { ChartConfig } from '../types/ChartConfig'
2
+
3
+ /**
4
+ * Calculates the Y-axis auto padding to prevent data labels from overlapping with axis tick labels.
5
+ * This is used when inline labels are enabled and there's potential for overlap.
6
+ *
7
+ * @param yScale - The D3 scale object for the Y-axis (must have .ticks() method)
8
+ * @param handleNumTicks - The number of ticks to display on the axis
9
+ * @param maxValue - The maximum data value (from useReduceData)
10
+ * @param minValue - The minimum data value (from useReduceData)
11
+ * @param config - The chart configuration object
12
+ * @returns The calculated auto padding percentage (0-100+), or 0 if no padding needed
13
+ */
14
+ export const getYAxisAutoPadding = (
15
+ yScale: any,
16
+ handleNumTicks: number,
17
+ maxValue: number,
18
+ minValue: number,
19
+ config: ChartConfig
20
+ ): number => {
21
+ // Early returns for cases where auto padding is not needed
22
+ if (!yScale?.ticks || config.orientation === 'horizontal' || config.yAxis?.max) {
23
+ return 0
24
+ }
25
+
26
+ const ticks = yScale.ticks(handleNumTicks)
27
+
28
+ if (!Array.isArray(ticks) || ticks.length === 0) {
29
+ return 0
30
+ }
31
+
32
+ // minimum percentage of the max value that the distance should be from the top grid line
33
+ const MINIMUM_DISTANCE_PERCENTAGE = 0.025
34
+
35
+ const topGridLine = Math.max(...ticks)
36
+ const needsPaddingThreshold = topGridLine - maxValue * MINIMUM_DISTANCE_PERCENTAGE
37
+ const maxValueIsGreaterThanThreshold = maxValue > needsPaddingThreshold
38
+
39
+ if (!maxValueIsGreaterThanThreshold) return 0
40
+
41
+ const tickGap = ticks.length === 1 ? ticks[0] : ticks[1] - ticks[0]
42
+ const nextTick = Math.max(...ticks) + tickGap
43
+ const divideBy = minValue < 0 ? maxValue / 2 : maxValue
44
+ const calculatedPadding = (nextTick - maxValue) / divideBy
45
+
46
+ // if auto padding is too close to next tick, add one more ticks worth of padding
47
+ const newPadding =
48
+ calculatedPadding > MINIMUM_DISTANCE_PERCENTAGE ? calculatedPadding : calculatedPadding + tickGap / divideBy
49
+
50
+ /* sometimes even though the padding is getting to the next tick exactly,
51
+ d3 still doesn't show the tick. we add 0.1 to ensure to tip it over the edge */
52
+ return newPadding * 100 + 0.1
53
+ }