@cdc/chart 4.23.10 → 4.23.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 (56) hide show
  1. package/dist/cdcchart.js +30989 -29057
  2. package/examples/feature/bar/example-bar-chart.json +1 -46
  3. package/examples/feature/bar/lollipop.json +156 -0
  4. package/examples/feature/combo/planet-combo-example-config.json +99 -9
  5. package/examples/feature/dev-4261.json +399 -0
  6. package/examples/feature/forest-plot/{broken.json → linear.json} +55 -50
  7. package/examples/feature/forest-plot/logarithmic.json +306 -0
  8. package/examples/feature/line/line-points.json +340 -0
  9. package/examples/feature/regions/index.json +462 -0
  10. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +181 -48
  11. package/examples/sparkline-multilple.json +846 -0
  12. package/index.html +10 -6
  13. package/package.json +3 -3
  14. package/src/CdcChart.tsx +75 -63
  15. package/src/_stories/Chart.stories.tsx +188 -0
  16. package/src/_stories/Chart.tooltip.stories.tsx +305 -0
  17. package/src/_stories/ChartBrush.stories.tsx +19 -0
  18. package/src/_stories/ChartSuppress.stories.tsx +19 -0
  19. package/src/_stories/_mock/brush_mock.json +393 -0
  20. package/src/_stories/_mock/suppress_mock.json +911 -0
  21. package/src/components/AreaChart.Stacked.jsx +4 -5
  22. package/src/components/AreaChart.jsx +6 -35
  23. package/src/components/{BarChart.Horizontal.jsx → BarChart.Horizontal.tsx} +106 -29
  24. package/src/components/{BarChart.StackedHorizontal.jsx → BarChart.StackedHorizontal.tsx} +22 -17
  25. package/src/components/{BarChart.StackedVertical.jsx → BarChart.StackedVertical.tsx} +22 -26
  26. package/src/components/{BarChart.Vertical.jsx → BarChart.Vertical.tsx} +175 -31
  27. package/src/components/BarChart.jsx +1 -1
  28. package/src/components/DeviationBar.jsx +4 -3
  29. package/src/components/EditorPanel.jsx +176 -50
  30. package/src/components/ForestPlot/ForestPlotProps.ts +11 -0
  31. package/src/components/ForestPlot/Readme.md +0 -0
  32. package/src/components/ForestPlot/index.scss +1 -0
  33. package/src/components/{ForestPlot.jsx → ForestPlot/index.tsx} +51 -31
  34. package/src/components/ForestPlotSettings.jsx +162 -113
  35. package/src/components/Legend.jsx +36 -3
  36. package/src/components/{LineChart.Circle.tsx → LineChart/LineChart.Circle.tsx} +26 -29
  37. package/src/components/LineChart/LineChartProps.ts +17 -0
  38. package/src/components/LineChart/index.scss +1 -0
  39. package/src/components/{LineChart.tsx → LineChart/index.tsx} +64 -35
  40. package/src/components/LinearChart.jsx +120 -60
  41. package/src/components/PairedBarChart.jsx +2 -2
  42. package/src/components/Series.jsx +22 -3
  43. package/src/components/ZoomBrush.tsx +168 -0
  44. package/src/data/initial-state.js +27 -12
  45. package/src/hooks/useBarChart.js +71 -7
  46. package/src/hooks/useColorScale.ts +50 -0
  47. package/src/hooks/useEditorPermissions.js +22 -4
  48. package/src/hooks/{useMinMax.js → useMinMax.ts} +75 -23
  49. package/src/hooks/{useRightAxis.js → useRightAxis.ts} +10 -2
  50. package/src/hooks/{useScales.js → useScales.ts} +64 -17
  51. package/src/hooks/useTooltip.jsx +68 -41
  52. package/src/scss/main.scss +3 -35
  53. package/src/types/ChartConfig.ts +43 -0
  54. package/src/types/ChartContext.ts +38 -0
  55. package/src/types/ChartProps.ts +7 -0
  56. package/src/types/ForestPlot.ts +60 -0
@@ -0,0 +1,168 @@
1
+ import { Brush } from '@visx/brush'
2
+ import { Group } from '@visx/group'
3
+ import { Text } from '@visx/text'
4
+ import { useBarChart } from '../hooks/useBarChart'
5
+ import { FC, useContext, useEffect, useRef, useState } from 'react'
6
+ import ConfigContext from '../ConfigContext'
7
+ import { ScaleLinear, ScaleBand } from 'd3-scale'
8
+
9
+ interface Props {
10
+ xScaleBrush: ScaleLinear<number, number>
11
+ yScale: ScaleBand<string>
12
+ xMax: number
13
+ yMax: number
14
+ }
15
+ const ZoomBrush: FC<Props> = props => {
16
+ const { transformedData: data, config, parseDate, formatDate, updateConfig } = useContext(ConfigContext)
17
+ const { fontSize } = useBarChart()
18
+
19
+ const [filteredData, setFilteredData] = useState([...data])
20
+ const brushRef = useRef(null)
21
+ const radius = 15
22
+
23
+ const [textProps, setTextProps] = useState({
24
+ startPosition: 0,
25
+ endPosition: 0,
26
+ startValue: '',
27
+ endValue: ''
28
+ })
29
+
30
+ const initialPosition = {
31
+ start: { x: 0 },
32
+ end: { x: props.xMax }
33
+ }
34
+
35
+ const style = {
36
+ fill: '#ddd',
37
+ stroke: 'blue',
38
+ fillOpacity: 0.8,
39
+ strokeOpacity: 0,
40
+ rx: radius
41
+ }
42
+
43
+ const onBrushChange = event => {
44
+ if (!event) return
45
+
46
+ const { xValues } = event
47
+
48
+ const dataKey = config.xAxis?.dataKey
49
+
50
+ const filteredData = data.filter(item => xValues.includes(item[dataKey]))
51
+
52
+ const endValue = xValues
53
+ .slice()
54
+ .reverse()
55
+ .find(item => item !== undefined)
56
+ const startValue = xValues.find(item => item !== undefined)
57
+
58
+ const formatIfDate = value => (config.runtime.xAxis.type === 'date' ? formatDate(parseDate(value)) : value)
59
+
60
+ setTextProps(prev => ({
61
+ ...prev,
62
+ startPosition: brushRef.current?.state.start.x,
63
+ endPosition: brushRef.current?.state.end.x,
64
+ endValue: formatIfDate(endValue),
65
+ startValue: formatIfDate(startValue)
66
+ }))
67
+
68
+ setFilteredData(filteredData)
69
+ }
70
+
71
+ useEffect(() => {
72
+ updateConfig({
73
+ ...config,
74
+ brush: {
75
+ ...config.brush,
76
+ data: filteredData
77
+ }
78
+ })
79
+ }, [filteredData])
80
+
81
+ //reset filters if brush is off
82
+ useEffect(() => {
83
+ if (!config.brush.active) {
84
+ setFilteredData(data)
85
+ }
86
+ }, [config.brush.active])
87
+
88
+ const calculateTop = (): number => {
89
+ const tickRotation = Number(config.xAxis.tickRotation) > 0 ? Number(config.xAxis.tickRotation) : 0
90
+ let top = 0
91
+ const offSet = 20
92
+ if (!config.xAxis.label) {
93
+ if (!config.isResponsiveTicks && tickRotation) {
94
+ top = Number(tickRotation + config.xAxis.tickWidthMax) / 1.6
95
+ }
96
+ if (!config.isResponsiveTicks && !tickRotation) {
97
+ top = Number(config.xAxis.labelOffset) - offSet
98
+ }
99
+ if (config.isResponsiveTicks && config.dynamicMarginTop) {
100
+ top = Number(config.xAxis.labelOffset + config.xAxis.tickWidthMax / 1.6)
101
+ }
102
+ if (config.isResponsiveTicks && !config.dynamicMarginTop) {
103
+ top = Number(config.xAxis.labelOffset - offSet)
104
+ }
105
+ }
106
+ if (config.xAxis.label) {
107
+ if (!config.isResponsiveTicks && tickRotation) {
108
+ top = Number(config.xAxis.tickWidthMax + tickRotation)
109
+ }
110
+
111
+ if (!config.isResponsiveTicks && !tickRotation) {
112
+ top = config.xAxis.labelOffset + offSet
113
+ }
114
+
115
+ if (config.isResponsiveTicks && !tickRotation) {
116
+ top = Number(config.dynamicMarginTop ? config.dynamicMarginTop : config.xAxis.labelOffset) + offSet
117
+ }
118
+ }
119
+
120
+ return top
121
+ }
122
+ if (!['Line', 'Bar', 'Area Chart', 'Combo'].includes(config.visualizationType)) {
123
+ return
124
+ }
125
+ return (
126
+ <Group display={config.brush.active ? 'block' : 'none'} top={Number(props.yMax) + calculateTop()} left={Number(config.runtime.yAxis.size)} pointerEvents='fill'>
127
+ <rect fill='#eee' width={props.xMax} height={config.brush.height} rx={radius} />
128
+ <Brush
129
+ renderBrushHandle={props => <BrushHandle textProps={textProps} fontSize={fontSize[config.fontSize]} {...props} isBrushing={brushRef.current?.state.isBrushing} />}
130
+ innerRef={brushRef}
131
+ useWindowMoveEvents={true}
132
+ selectedBoxStyle={style}
133
+ xScale={props.xScaleBrush}
134
+ yScale={props.yScale}
135
+ width={props.xMax}
136
+ resizeTriggerAreas={['left', 'right']}
137
+ height={config.brush.height}
138
+ handleSize={8}
139
+ brushDirection='horizontal'
140
+ initialBrushPosition={initialPosition}
141
+ onChange={onBrushChange}
142
+ />
143
+ </Group>
144
+ )
145
+ }
146
+
147
+ const BrushHandle = props => {
148
+ const { x, isBrushActive, isBrushing, className, textProps } = props
149
+ const pathWidth = 8
150
+ if (!isBrushActive) {
151
+ return null
152
+ }
153
+ // Flip the SVG path horizontally for the left handle
154
+ const isLeft = className.includes('left')
155
+ const transform = isLeft ? 'scale(-1, 1)' : 'translate(0,0)'
156
+ const textAnchor = isLeft ? 'end' : 'start'
157
+
158
+ return (
159
+ <Group left={x + pathWidth / 2} top={-2}>
160
+ <Text pointerEvents='visiblePainted' dominantBaseline='hanging' x={0} verticalAnchor='start' textAnchor={textAnchor} fontSize={props.fontSize / 1.4} dy={10} y={15}>
161
+ {isLeft ? textProps.startValue : textProps.endValue}
162
+ </Text>
163
+ <path cursor='ew-resize' d='M0.5,10A6,6 0 0 1 6.5,16V14A6,6 0 0 1 0.5,20ZM2.5,18V12M4.5,18V12' fill={!isBrushing ? '#666' : '#297EF1'} strokeWidth='1' transform={transform}></path>
164
+ </Group>
165
+ )
166
+ }
167
+
168
+ export default ZoomBrush
@@ -7,11 +7,11 @@ export default {
7
7
  title: '',
8
8
  showTitle: true,
9
9
  showDownloadMediaButton: false,
10
- showChartBrush: false,
11
10
  theme: 'theme-blue',
12
11
  animate: false,
13
12
  fontSize: 'medium',
14
13
  lineDatapointStyle: 'hover',
14
+ lineDatapointColor: 'Same as Line',
15
15
  barHasBorder: 'false',
16
16
  isLollipopChart: false,
17
17
  lollipopShape: 'circle',
@@ -28,6 +28,8 @@ export default {
28
28
  left: 5,
29
29
  right: 5
30
30
  },
31
+ suppressedData: [],
32
+
31
33
  yAxis: {
32
34
  hideAxis: false,
33
35
  displayNumbersOnBar: false,
@@ -134,6 +136,7 @@ export default {
134
136
  // start with a blank list
135
137
  },
136
138
  legend: {
139
+ hide: false,
137
140
  behavior: 'isolate',
138
141
  singleRow: false,
139
142
  colorCode: '',
@@ -145,7 +148,13 @@ export default {
145
148
  dynamicLegendItemLimitMessage: 'Dynamic Legend Item Limit Hit.',
146
149
  dynamicLegendChartMessage: 'Select Options from the Legend',
147
150
  lineMode: false,
148
- verticalSorted: false
151
+ verticalSorted: false,
152
+ highlightOnHover: false
153
+ },
154
+ brush: {
155
+ height: 25,
156
+ data: [],
157
+ active: false
149
158
  },
150
159
  exclusions: {
151
160
  active: false,
@@ -180,22 +189,27 @@ export default {
180
189
  highlightedBarValues: [],
181
190
  series: [],
182
191
  tooltips: {
183
- opacity: 90
192
+ opacity: 90,
193
+ singleSeries: false
184
194
  },
185
195
  forestPlot: {
186
196
  startAt: 0,
187
- width: 'auto',
188
197
  colors: {
189
198
  line: '',
190
199
  shape: ''
191
200
  },
201
+ lineOfNoEffect: {
202
+ show: true
203
+ },
204
+ type: '',
205
+ pooledResult: {
206
+ diamondHeight: 5,
207
+ column: ''
208
+ },
192
209
  estimateField: '',
193
210
  estimateRadius: '',
194
- lowerCiField: '',
195
- upperCiField: '',
196
211
  shape: '',
197
212
  rowHeight: 20,
198
- showZeroLine: false,
199
213
  description: {
200
214
  show: true,
201
215
  text: 'description',
@@ -217,12 +231,13 @@ export default {
217
231
  estimateField: 0
218
232
  },
219
233
  leftWidthOffset: 0,
220
- rightWidthOffset: 0
221
- },
222
- brush: {
223
- pattern_id: 'brush_pattern',
224
- accent_color: '#ddd'
234
+ rightWidthOffset: 0,
235
+ showZeroLine: false,
236
+ hideDateCategoryCol: false,
237
+ leftLabel: '',
238
+ rightLabel: ''
225
239
  },
240
+
226
241
  area: {
227
242
  isStacked: false
228
243
  }
@@ -1,9 +1,10 @@
1
- import React, { useContext, useEffect } from 'react'
1
+ import React, { useContext, useEffect, useState } from 'react'
2
2
  import ConfigContext from '../ConfigContext'
3
-
3
+ import { formatNumber as formatColNumber } from '@cdc/core/helpers/cove/number'
4
4
  export const useBarChart = () => {
5
- const { config, colorPalettes, tableData, updateConfig, parseDate, formatDate } = useContext(ConfigContext)
5
+ const { config, colorPalettes, tableData, updateConfig, parseDate, formatDate, setSeriesHighlight } = useContext(ConfigContext)
6
6
  const { orientation } = config
7
+ const [hoveredBar, setHoveredBar] = useState(null)
7
8
 
8
9
  const isHorizontal = orientation === 'horizontal'
9
10
  const barBorderWidth = 1
@@ -80,7 +81,7 @@ export const useBarChart = () => {
80
81
  if (!config.legend.colorCode && config.series.length > 1) {
81
82
  return currentBarColor
82
83
  }
83
- const palettesArr = colorPalettes[config.palette]
84
+ const palettesArr = config.customColors ?? colorPalettes[config.palette]
84
85
  const values = tableData.map(d => {
85
86
  return d[config.legend.colorCode]
86
87
  })
@@ -144,10 +145,10 @@ export const useBarChart = () => {
144
145
  }
145
146
 
146
147
  const getHighlightedBarColorByValue = value => {
147
- const match = config?.highlightedBarValues.filter(item => {
148
+ const match = config?.highlightedBarValues.find(item => {
148
149
  if (!item.value) return
149
150
  return config.xAxis.type === 'date' ? formatDate(parseDate(item.value)) === value : item.value === value
150
- })[0]
151
+ })
151
152
 
152
153
  if (!match?.color) return `rgba(255, 102, 1)`
153
154
  return match.color
@@ -161,8 +162,66 @@ export const useBarChart = () => {
161
162
  if (!match?.color) return false
162
163
  return match
163
164
  }
165
+ const generateIconSize = barWidth => {
166
+ if (barWidth < 4) {
167
+ return 1
168
+ }
169
+ if (barWidth < 5) {
170
+ return 4
171
+ }
172
+ if (barWidth < 10) {
173
+ return 6
174
+ }
175
+ if (barWidth < 15) {
176
+ return 7
177
+ }
178
+ if (barWidth < 20) {
179
+ return 8
180
+ }
181
+ if (barWidth < 90) {
182
+ return 8
183
+ }
184
+ return 0
185
+ }
186
+
187
+ const getAdditionalColumn = xAxisDataValue => {
188
+ if (!xAxisDataValue) return ''
189
+ const columns = config.columns
190
+ const columnsWithTooltips = []
191
+ let additionalTooltipItems = ''
192
+ const closestVal =
193
+ tableData.find(d => {
194
+ return d[config.xAxis.dataKey] === xAxisDataValue
195
+ }) || {}
196
+ for (const [colKeys, colVals] of Object.entries(columns)) {
197
+ const formattingParams = {
198
+ addColPrefix: config.columns[colKeys].prefix,
199
+ addColSuffix: config.columns[colKeys].suffix,
200
+ addColRoundTo: config.columns[colKeys].roundToPlace ? config.columns[colKeys].roundToPlace : '',
201
+ addColCommas: config.columns[colKeys].commas
202
+ }
203
+
204
+ const formattedValue = formatColNumber(closestVal[colVals?.name], 'left', true, config, formattingParams)
205
+ if (colVals.tooltips) {
206
+ columnsWithTooltips.push([colVals.label, formattedValue])
207
+ }
208
+ }
209
+ columnsWithTooltips.forEach(columnData => {
210
+ additionalTooltipItems += `${columnData[0]} : ${columnData[1]} <br/>`
211
+ })
212
+ return additionalTooltipItems
213
+ }
214
+
215
+ const onMouseOverBar = (categoryValue, barKey) => {
216
+ if (config.legend.highlightOnHover && config.legend.behavior === 'highlight' && barKey) setSeriesHighlight([barKey])
217
+ setHoveredBar(categoryValue)
218
+ }
219
+ const onMouseLeaveBar = () => {
220
+ if (config.legend.highlightOnHover && config.legend.behavior === 'highlight') setSeriesHighlight([])
221
+ }
164
222
 
165
223
  return {
224
+ generateIconSize,
166
225
  isHorizontal,
167
226
  barBorderWidth,
168
227
  lollipopBarWidth,
@@ -181,6 +240,11 @@ export const useBarChart = () => {
181
240
  updateBars,
182
241
  assignColorsToValues,
183
242
  getHighlightedBarColorByValue,
184
- getHighlightedBarByValue
243
+ getHighlightedBarByValue,
244
+ getAdditionalColumn,
245
+ hoveredBar,
246
+ setHoveredBar,
247
+ onMouseOverBar,
248
+ onMouseLeaveBar
185
249
  }
186
250
  }
@@ -0,0 +1,50 @@
1
+ import { colorPalettesChart as colorPalettes, twoColorPalette } from '@cdc/core/data/colorPalettes'
2
+ import { scaleOrdinal } from '@visx/scale'
3
+ import { useContext } from 'react'
4
+ import ConfigContext from '../ConfigContext'
5
+
6
+ const useColorScale = () => {
7
+ const { config, data } = useContext(ConfigContext)
8
+ const { visualizationSubType, visualizationType, series, legend } = config
9
+
10
+ const generatePalette = colorsCount => {
11
+ if (!series?.length) return []
12
+ const isSpecialType = ['Paired Bar', 'Deviation Bar'].includes(visualizationType)
13
+ const chosenPalette = isSpecialType ? config.twoColor.palette : config.palette
14
+ const allPalettes = { ...colorPalettes, ...twoColorPalette }
15
+ let palette = config.customColors || allPalettes[chosenPalette]
16
+ while (colorsCount > palette.length) palette = palette.concat(palette)
17
+ return palette.slice(0, colorsCount)
18
+ }
19
+
20
+ let colorScale = scaleOrdinal({
21
+ domain: config?.runtime?.seriesLabelsAll,
22
+ range: generatePalette(series.length)
23
+ })
24
+
25
+ if (visualizationType === 'Deviation Bar') {
26
+ const { targetLabel } = config.xAxis
27
+ colorScale = scaleOrdinal({
28
+ domain: [`Below ${targetLabel}`, `Above ${targetLabel}`],
29
+ range: generatePalette(2)
30
+ })
31
+ }
32
+ if (visualizationType === 'Bar' && visualizationSubType === 'regular' && series?.length === 1 && legend?.colorCode) {
33
+ const set = new Set(data.map(d => d[legend.colorCode]))
34
+ colorScale = scaleOrdinal({
35
+ domain: [...set],
36
+ range: generatePalette([...set].length)
37
+ })
38
+ }
39
+ if (config.series.some(s => s.name)) {
40
+ const set = new Set(series.map(d => d.name || d.dataKey))
41
+ colorScale = colorScale = scaleOrdinal({
42
+ domain: [...set],
43
+ range: generatePalette(series.length)
44
+ })
45
+ }
46
+
47
+ return { colorScale }
48
+ }
49
+
50
+ export default useColorScale
@@ -3,7 +3,7 @@ import ConfigContext from '../ConfigContext'
3
3
 
4
4
  export const useEditorPermissions = () => {
5
5
  const { config } = useContext(ConfigContext)
6
- const { visualizationType, series, orientation } = config
6
+ const { visualizationType, series, orientation, visualizationSubType } = config
7
7
 
8
8
  // Overall support for the chart types
9
9
  // prettier-ignore
@@ -84,6 +84,15 @@ export const useEditorPermissions = () => {
84
84
  }
85
85
  }
86
86
 
87
+ const visHasDataSuppression = () => {
88
+ if ((visualizationType === 'Bar' || 'Combo') && visualizationSubType === 'regular') {
89
+ return true
90
+ }
91
+ }
92
+ const visHasBrushChart = () => {
93
+ return ['Line', 'Bar', 'Area Chart', 'Combo'].includes(visualizationType) && orientation === 'vertical'
94
+ }
95
+
87
96
  const visHasBarBorders = () => {
88
97
  const disabledCharts = ['Box Plot', 'Scatter Plot', 'Pie']
89
98
  if (disabledCharts.includes(visualizationType)) return false
@@ -154,13 +163,13 @@ export const useEditorPermissions = () => {
154
163
  }
155
164
 
156
165
  const visSupportsDateCategoryTickRotation = () => {
157
- const disabledCharts = ['Forest Plot', 'Spark Line']
166
+ const disabledCharts = ['Spark Line']
158
167
  if (disabledCharts.includes(visualizationType)) return false
159
168
  return true
160
169
  }
161
170
 
162
171
  const visSupportsDateCategoryNumTicks = () => {
163
- const disabledCharts = ['Forest Plot', 'Spark Line']
172
+ const disabledCharts = ['Spark Line']
164
173
  if (disabledCharts.includes(visualizationType)) return false
165
174
  return true
166
175
  }
@@ -242,6 +251,12 @@ export const useEditorPermissions = () => {
242
251
  return true
243
252
  }
244
253
 
254
+ const visSupportsReactTooltip = () => {
255
+ if (['Deviation Bar', 'Box Plot', 'Scatter Plot', 'Paired Bar'].includes(visualizationType) || (visualizationType === 'Bar' && config.tooltips.singleSeries)) {
256
+ return true
257
+ }
258
+ }
259
+
245
260
  return {
246
261
  enabledChartTypes,
247
262
  headerColors,
@@ -250,7 +265,9 @@ export const useEditorPermissions = () => {
250
265
  visHasBarBorders,
251
266
  visHasDataCutoff,
252
267
  visHasLabelOnData,
268
+ visHasDataSuppression,
253
269
  visHasLegend,
270
+ visHasBrushChart,
254
271
  visHasNumbersOnBars,
255
272
  visSupportsBarSpace,
256
273
  visSupportsBarThickness,
@@ -276,6 +293,7 @@ export const useEditorPermissions = () => {
276
293
  visSupportsValueAxisGridLines,
277
294
  visSupportsValueAxisLabels,
278
295
  visSupportsValueAxisLine,
279
- visSupportsValueAxisTicks
296
+ visSupportsValueAxisTicks,
297
+ visSupportsReactTooltip
280
298
  }
281
299
  }
@@ -1,14 +1,36 @@
1
- const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAllLine }) => {
1
+ import { ChartConfig } from '../types/ChartConfig'
2
+
3
+ type UseMinMaxProps = {
4
+ /** config - standard chart config */
5
+ config: ChartConfig
6
+ /** minValue - starting minimum value */
7
+ minValue: number
8
+ /** maxValue - starting maximum value before transformations */
9
+ maxValue: number
10
+ /** existsPositiveValue - determines if axis should show values above/below 0 */
11
+ existPositiveValue: boolean
12
+ /** data - standard data array */
13
+ data: Object[]
14
+ /** isAllLine: if all series are line type including dashed lines */
15
+ isAllLine: boolean
16
+ }
17
+
18
+ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAllLine }: UseMinMaxProps) => {
2
19
  let min = 0
3
20
  let max = 0
4
21
 
22
+ // Implementation for left and right axis
23
+ let leftMax = 0
24
+ let rightMax = 0
25
+
5
26
  if (!data) {
6
27
  return { min, max }
7
28
  }
8
29
 
30
+ const { visualizationType, series } = config
9
31
  const { max: enteredMaxValue, min: enteredMinValue } = config.runtime.yAxis
10
32
  const minRequiredCIPadding = 1.15 // regardless of Editor if CI data, there must be 10% padding added
11
-
33
+
12
34
  // do validation bafore applying t0 charts
13
35
  const isMaxValid = existPositiveValue ? enteredMaxValue >= maxValue : enteredMaxValue >= 0
14
36
  const isMinValid = config.useLogScale ? enteredMinValue >= 0 : (enteredMinValue <= 0 && minValue >= 0) || (enteredMinValue <= minValue && minValue < 0)
@@ -16,31 +38,19 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
16
38
  min = enteredMinValue && isMinValid ? enteredMinValue : minValue
17
39
  max = enteredMaxValue && isMaxValid ? enteredMaxValue : Number.MIN_VALUE
18
40
 
19
- let ciYMin = 0
20
- if (config.visualizationType === 'Bar' || config.visualizationType === 'Combo' || config.visualizationType === 'Deviation Bar') {
21
- let ciYMax = 0
22
- if (config.hasOwnProperty('confidenceKeys')) {
23
- let upperCIValues = data.map(function (d) {
24
- return d[config.confidenceKeys.upper]
25
- })
26
- ciYMax = Math.max.apply(Math, upperCIValues)
27
- if (ciYMax > max) max = ciYMax * minRequiredCIPadding // bump up the max plus some padding always
41
+ const { lower, upper } = config?.confidenceKeys || {}
28
42
 
29
- // check the min if lower confidence
30
- let lowerCIValues = data.map(function (d) {
31
- // if no lower CI then we need lowerCIValues to have nothing in it
32
- return d[config.confidenceKeys.lower] !== undefined ? d[config.confidenceKeys.lower] : ''
33
- })
34
- ciYMin = Math.min.apply(Math, lowerCIValues)
35
- if (ciYMin < min) min = ciYMin * minRequiredCIPadding // adjust the min + 10% padding for negative numbers to separate from axis
36
- }
43
+ if (lower && upper && config.visualizationType === 'Bar') {
44
+ const buffer = min < 0 ? 1.1 : 0
45
+ max = Math.max(maxValue, Math.max(...data.flatMap(d => [d[upper], d[lower]])) * 1.15)
46
+ min = Math.min(minValue, Math.min(...data.flatMap(d => [d[upper], d[lower]])) * 1.15) * buffer
37
47
  }
38
48
 
39
- if (config.visualizationType === 'Forecasting') {
49
+ if (config.series.filter(s => s?.type === 'Forecasting')) {
40
50
  const {
41
51
  runtime: { forecastingSeriesKeys }
42
52
  } = config
43
- if (forecastingSeriesKeys.length > 0) {
53
+ if (forecastingSeriesKeys?.length > 0) {
44
54
  // push all keys into an array
45
55
  let columnNames = []
46
56
 
@@ -73,10 +83,52 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
73
83
  }
74
84
  }
75
85
 
86
+ if (visualizationType === 'Combo') {
87
+ try {
88
+ if (!data) throw new Error('COVE: missing data while getting min/max for combo chart.')
89
+ // seperate the left and right axis items & get each sides series keys
90
+ let leftAxisSeriesItems = series.filter(s => s.axis === 'Left')
91
+ let rightAxisSeriesItems = series.filter(s => s.axis === 'Right')
92
+
93
+ const findMaxFromSeriesKeys = (data, seriesData, max, axis = 'left') => {
94
+ let stackedBarMax = 0
95
+ let axisSeriesKeys = seriesData.map(i => i.dataKey) || []
96
+
97
+ axisSeriesKeys.forEach(key => {
98
+ let _seriesData = seriesData.find(s => s.dataKey === key)
99
+ let _data = data.map(d => d[key])
100
+ let seriesMax = Math.max.apply(null, _data)
101
+ if (config.visualizationSubType === 'stacked' && axis === 'left' && _seriesData.type === 'Bar') {
102
+ stackedBarMax += seriesMax
103
+ }
104
+ if (seriesMax > max) {
105
+ max = seriesMax
106
+ }
107
+
108
+ if (max < stackedBarMax) {
109
+ max = stackedBarMax
110
+ }
111
+ })
112
+ return max
113
+ }
114
+ leftMax = findMaxFromSeriesKeys(data, leftAxisSeriesItems, leftMax, 'left')
115
+ rightMax = findMaxFromSeriesKeys(data, rightAxisSeriesItems, rightMax, 'right')
116
+
117
+ if (leftMax < enteredMaxValue) {
118
+ leftMax = enteredMaxValue
119
+ }
120
+ } catch (e) {
121
+ console.error(e.message)
122
+ }
123
+ }
124
+
76
125
  // this should not apply to bar charts if there is negative CI data
77
- if (((config.visualizationType === 'Bar' && ciYMin >= 0) || (config.visualizationType === 'Combo' && !isAllLine)) && min > 0) {
126
+ if ((visualizationType === 'Bar' || (visualizationType === 'Combo' && !isAllLine)) && min > 0) {
78
127
  min = 0
79
128
  }
129
+ if ((config.visualizationType === 'Bar' || (config.visualizationType === 'Combo' && !isAllLine)) && min < 0) {
130
+ min = min * 1.1
131
+ }
80
132
 
81
133
  if (config.visualizationType === 'Combo' && isAllLine) {
82
134
  if ((enteredMinValue === undefined || enteredMinValue === null || enteredMinValue === '') && min > 0) {
@@ -136,6 +188,6 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
136
188
  }
137
189
  }
138
190
 
139
- return { min, max }
191
+ return { min, max, leftMax, rightMax }
140
192
  }
141
193
  export default useMinMax
@@ -1,5 +1,5 @@
1
1
  import { scaleLinear } from '@visx/scale'
2
- import useReduceData from '../hooks/useReduceData'
2
+ import useReduceData from './useReduceData'
3
3
 
4
4
  export default function useRightAxis({ config, yMax = 0, data = [], updateConfig }) {
5
5
  const hasRightAxis = config.visualizationType === 'Combo' && config.orientation === 'vertical'
@@ -15,7 +15,15 @@ export default function useRightAxis({ config, yMax = 0, data = [], updateConfig
15
15
  return rightAxisData
16
16
  }
17
17
 
18
- const max = Math.max.apply(null, allRightAxisData(rightSeriesKeys))
18
+ let max = Math.max.apply(null, allRightAxisData(rightSeriesKeys))
19
+
20
+ if (config.yAxis.rightMax > max) {
21
+ max = config.yAxis.rightMax
22
+ }
23
+
24
+ if (config.yAxis.rightMin < minValue) {
25
+ minValue = config.yAxis.rightMin
26
+ }
19
27
 
20
28
  // if there is a bar series & the right axis doesn't include a negative number, default to zero
21
29
  const hasBarSeries = config.runtime?.barSeriesKeys?.length > 0