@cdc/chart 4.22.11 → 4.23.1

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 (38) hide show
  1. package/dist/495.js +3 -0
  2. package/dist/703.js +1 -0
  3. package/dist/cdcchart.js +723 -6
  4. package/examples/box-plot-data.json +71 -0
  5. package/examples/box-plot.csv +5 -0
  6. package/examples/{private/yaxis-test.json → box-plot.json} +47 -56
  7. package/examples/gallery/bar-chart-vertical/combo-line-chart.json +3 -1
  8. package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +85 -16
  9. package/examples/new-data.csv +17 -0
  10. package/examples/newdata.json +90 -0
  11. package/package.json +3 -2
  12. package/src/CdcChart.tsx +150 -94
  13. package/src/components/BarChart.tsx +156 -226
  14. package/src/components/BoxPlot.js +92 -0
  15. package/src/components/DataTable.tsx +28 -12
  16. package/src/components/EditorPanel.js +151 -104
  17. package/src/components/Filters.js +131 -0
  18. package/src/components/Legend.js +8 -1
  19. package/src/components/LineChart.tsx +64 -13
  20. package/src/components/LinearChart.tsx +120 -81
  21. package/src/components/PairedBarChart.tsx +1 -1
  22. package/src/components/PieChart.tsx +12 -2
  23. package/src/components/useIntersectionObserver.tsx +9 -7
  24. package/src/data/initial-state.js +14 -8
  25. package/src/hooks/useReduceData.ts +8 -5
  26. package/src/index.html +51 -51
  27. package/src/scss/DataTable.scss +1 -1
  28. package/src/scss/main.scss +53 -22
  29. package/examples/private/filters.json +0 -170
  30. package/examples/private/line-test-data.json +0 -22
  31. package/examples/private/line-test-two.json +0 -210
  32. package/examples/private/line-test.json +0 -102
  33. package/examples/private/new.json +0 -48800
  34. package/examples/private/newtest.csv +0 -101
  35. package/examples/private/shawn.json +0 -1106
  36. package/examples/private/test.json +0 -10124
  37. package/examples/private/yaxis-testing.csv +0 -27
  38. package/examples/private/yaxis.json +0 -28
@@ -0,0 +1,131 @@
1
+ import React, { useEffect, useState, useContext } from 'react'
2
+ import Context from './../context'
3
+ import Button from '@cdc/core/components/elements/Button'
4
+
5
+ const useFilters = () => {
6
+ const { config, setConfig, filteredData, setFilteredData, excludedData, filterData, runtimeFilters } = useContext(Context)
7
+ const [showApplyButton, setShowApplyButton] = useState(false)
8
+
9
+ const sortAsc = (a, b) => {
10
+ return a.toString().localeCompare(b.toString(), 'en', { numeric: true })
11
+ }
12
+
13
+ const sortDesc = (a, b) => {
14
+ return b.toString().localeCompare(a.toString(), 'en', { numeric: true })
15
+ }
16
+
17
+ const announceChange = text => {}
18
+
19
+ const changeFilterActive = (index, value) => {
20
+ let newFilters = config.filters
21
+ newFilters[index].active = value
22
+ setConfig({
23
+ ...config,
24
+ filters: newFilters
25
+ })
26
+ setShowApplyButton(true)
27
+ }
28
+
29
+ const handleApplyButton = newFilters => {
30
+ setConfig({ ...config, filters: newFilters })
31
+ setFilteredData(filterData(newFilters, excludedData))
32
+ setShowApplyButton(false)
33
+ }
34
+
35
+ const handleReset = () => {
36
+ let newFilters = config.filters
37
+
38
+ // reset to first item in values array.
39
+ newFilters.map(filter => {
40
+ filter.active = filter.values[0]
41
+ })
42
+
43
+ setFilteredData(filterData(newFilters, excludedData))
44
+ setConfig({ ...config, filters: newFilters })
45
+ }
46
+
47
+ return { handleApplyButton, changeFilterActive, announceChange, sortAsc, sortDesc, showApplyButton, handleReset }
48
+ }
49
+
50
+ const Filters = () => {
51
+ const { config } = useContext(Context)
52
+ const { handleApplyButton, changeFilterActive, announceChange, sortAsc, sortDesc, showApplyButton, handleReset } = useFilters()
53
+ const { filters } = config
54
+ const buttonText = 'Apply Filters'
55
+ const resetText = 'Reset All'
56
+
57
+ // A List of Dropdowns
58
+ const FilterList = () => {
59
+ if (config.filters) {
60
+ return config.filters.map((singleFilter, index) => {
61
+ const values = []
62
+
63
+ if (!singleFilter.order || singleFilter.order === '') {
64
+ singleFilter.order = 'asc'
65
+ }
66
+
67
+ if (singleFilter.order === 'desc') {
68
+ singleFilter.values = singleFilter.values.sort(sortDesc)
69
+ }
70
+
71
+ if (singleFilter.order === 'asc') {
72
+ singleFilter.values = singleFilter.values.sort(sortAsc)
73
+ }
74
+
75
+ singleFilter.values.forEach((filterOption, index) => {
76
+ values.push(
77
+ <option key={index} value={filterOption}>
78
+ {filterOption}
79
+ </option>
80
+ )
81
+ })
82
+
83
+ return (
84
+ <div className='single-filter' key={index}>
85
+ <label htmlFor={`filter-${index}`}>{singleFilter.label}</label>
86
+ <select
87
+ id={`filter-${index}`}
88
+ className='filter-select'
89
+ data-index='0'
90
+ value={singleFilter.active}
91
+ onChange={e => {
92
+ changeFilterActive(index, e.target.value)
93
+ announceChange(`Filter ${singleFilter.label} value has been changed to ${e.target.value}, please reference the data table to see updated values.`)
94
+ }}
95
+ >
96
+ {values}
97
+ </select>
98
+ </div>
99
+ )
100
+ })
101
+ } else {
102
+ return null
103
+ }
104
+ }
105
+
106
+ return (
107
+ <section className={`filters-section`} style={{ display: 'block', width: '100%' }}>
108
+ {config.filters.length > 0 && (
109
+ <>
110
+ <h3 className='filters-section__title'>Filters</h3>
111
+ <hr />
112
+ </>
113
+ )}
114
+ <div className='filters-section__wrapper' style={{ flexWrap: 'wrap', display: 'flex', gap: '7px 15px' }}>
115
+ <FilterList />
116
+ {config.filters.length > 0 && (
117
+ <div className='filter-section__buttons' style={{ width: '100%' }}>
118
+ <Button onClick={() => handleApplyButton(filters)} disabled={!showApplyButton} style={{ marginRight: '10px' }}>
119
+ {buttonText}
120
+ </Button>
121
+ <a href='#!' role='button' onClick={handleReset}>
122
+ {resetText}
123
+ </a>
124
+ </div>
125
+ )}
126
+ </div>
127
+ </section>
128
+ )
129
+ }
130
+
131
+ export default Filters
@@ -111,7 +111,14 @@ const Legend = () => {
111
111
 
112
112
  if (!legend.dynamicLegend)
113
113
  return (
114
- <aside id='legend' className={containerClasses.join(' ')} role='region' aria-label='legend' tabIndex={0}>
114
+ <aside
115
+ style={{ marginTop: config.legend.position === 'bottom' && config.orientation === 'horizontal' ? `${config.runtime.xAxis.size}px` : '0px', marginBottom: config.legend.position === 'bottom' ? '15px' : '0px' }}
116
+ id='legend'
117
+ className={containerClasses.join(' ')}
118
+ role='region'
119
+ aria-label='legend'
120
+ tabIndex={0}
121
+ >
115
122
  {legend.label && <h2>{parse(legend.label)}</h2>}
116
123
  {legend.description && <p>{parse(legend.description)}</p>}
117
124
  <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
@@ -28,6 +28,14 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
28
28
  }
29
29
  }
30
30
 
31
+ const handleAxisFormating = (axis = 'left', label, value) => {
32
+ axis = String(axis).toLocaleLowerCase()
33
+ if (label) {
34
+ return `${label}: ${formatNumber(value, axis)}`
35
+ }
36
+ return `${formatNumber(value, axis)}`
37
+ }
38
+
31
39
  return (
32
40
  <ErrorBoundary component='LineChart'>
33
41
  <Group left={config.runtime.yAxis.size}>
@@ -43,20 +51,26 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
43
51
  display={config.legend.behavior === 'highlight' || (seriesHighlight.length === 0 && !config.legend.dynamicLegend) || seriesHighlight.indexOf(seriesKey) !== -1 ? 'block' : 'none'}
44
52
  >
45
53
  {data.map((d, dataIndex) => {
46
- let seriesAxis = config.series.filter(s => s.dataKey === seriesKey)[0].axis
54
+ // Find the series object from the config.series array that has a dataKey matching the seriesKey variable.
55
+ const series = config.series.find(({ dataKey }) => dataKey === seriesKey)
56
+ const { axis } = series
57
+
47
58
  const xAxisValue = config.runtime.xAxis.type === 'date' ? formatDate(parseDate(d[config.runtime.xAxis.dataKey])) : d[config.runtime.xAxis.dataKey]
48
- const yAxisValue = formatNumber(getYAxisData(d, seriesKey));
49
- let yAxisTooltip = config.runtime.yAxis.isLegendValue ? `${seriesKey}: ${yAxisValue} ` : config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue;
50
- let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
51
- if (seriesAxis === 'Left') {
52
- yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${formatNumber(getYAxisData(d, seriesKey))}` : formatNumber(getYAxisData(d, seriesKey))
53
- } else {
54
- yAxisTooltip = config.runtime.yAxis.rightLabel ? `${config.runtime.yAxis.rightLabel}: ${formatNumber(getYAxisData(d, seriesKey))}` : formatNumber(getYAxisData(d, seriesKey))
55
- }
59
+ const yAxisValue = getYAxisData(d, seriesKey)
60
+
61
+ const hasMultipleSeries = Object.keys(config.runtime.seriesLabels).length > 1
62
+ const labeltype = axis === 'Right' ? 'rightLabel' : 'label'
63
+ let label = config.runtime.yAxis[labeltype]
64
+ // if has muiltiple series dont show legend value on tooltip
65
+ if (!hasMultipleSeries) label = config.isLegendValue ? config.runtime.seriesLabels[seriesKey] : label
66
+
67
+ let yAxisTooltip = handleAxisFormating(axis, label, yAxisValue)
68
+ let xAxisTooltip = handleAxisFormating(axis, config.runtime.xAxis.label, xAxisValue)
69
+
56
70
  const tooltip = `<div>
71
+ ${config.legend.showLegendValuesTooltip && config.runtime.seriesLabels && Object.keys(config.runtime.seriesLabels).length > 1 ? `${config.runtime.seriesLabels[seriesKey] || ''}<br/>` : ''}
57
72
  ${yAxisTooltip}<br />
58
- ${xAxisTooltip}<br />
59
- ${config.seriesLabel ? `${config.seriesLabel}: ${seriesKey}` : ''}
73
+ ${xAxisTooltip}
60
74
  </div>`
61
75
  let circleRadii = 4.5
62
76
  return (
@@ -107,10 +121,47 @@ export default function LineChart({ xScale, yScale, getXAxisData, getYAxisData,
107
121
  strokeOpacity={1}
108
122
  shapeRendering='geometricPrecision'
109
123
  strokeDasharray={lineType ? handleLineType(lineType) : 0}
110
- defined={(item,i) => {
111
- return item[config.runtime.seriesLabels[seriesKey]] !== "" && item[config.runtime.seriesLabels[seriesKey]] !== null;
124
+ defined={(item, i) => {
125
+ return item[config.runtime.seriesLabels[seriesKey]] !== '' && item[config.runtime.seriesLabels[seriesKey]] !== null
112
126
  }}
113
127
  />
128
+ {config.animate && (
129
+ <LinePath
130
+ className='animation'
131
+ curve={allCurves.curveLinear}
132
+ data={data}
133
+ x={d => xScale(getXAxisData(d))}
134
+ y={d => (seriesAxis === 'Right' ? yScaleRight(getYAxisData(d, seriesKey)) : yScale(getYAxisData(d, seriesKey)))}
135
+ stroke='#fff'
136
+ strokeWidth={3}
137
+ strokeOpacity={1}
138
+ shapeRendering='geometricPrecision'
139
+ strokeDasharray={lineType ? handleLineType(lineType) : 0}
140
+ defined={(item, i) => {
141
+ return item[config.runtime.seriesLabels[seriesKey]] !== '' && item[config.runtime.seriesLabels[seriesKey]] !== null
142
+ }}
143
+ />
144
+ )}
145
+
146
+ {/* Render series labels at end if each line if selected in the editor */}
147
+ {config.showLineSeriesLabels &&
148
+ (config.runtime.lineSeriesKeys || config.runtime.seriesKeys).map(seriesKey => {
149
+ let lastDatum
150
+ for (let i = data.length - 1; i >= 0; i--) {
151
+ if (data[i][seriesKey]) {
152
+ lastDatum = data[i]
153
+ break
154
+ }
155
+ }
156
+ if (!lastDatum) {
157
+ return <></>
158
+ }
159
+ return (
160
+ <text x={xScale(getXAxisData(lastDatum)) + 5} y={yScale(getYAxisData(lastDatum, seriesKey))} alignmentBaseline='middle' fill={config.colorMatchLineSeriesLabels && colorScale ? colorScale(config.runtime.seriesLabels[seriesKey] || seriesKey) : 'black'}>
161
+ {config.runtime.seriesLabels[seriesKey] || seriesKey}
162
+ </text>
163
+ )
164
+ })}
114
165
  </Group>
115
166
  )
116
167
  })}
@@ -4,7 +4,7 @@ import ReactTooltip from 'react-tooltip'
4
4
  import { Group } from '@visx/group'
5
5
  import { Line } from '@visx/shape'
6
6
  import { Text } from '@visx/text'
7
- import { scaleLinear, scalePoint } from '@visx/scale'
7
+ import { scaleLinear, scalePoint, scaleBand } from '@visx/scale'
8
8
  import { AxisLeft, AxisBottom, AxisRight, AxisTop } from '@visx/axis'
9
9
 
10
10
  import BarChart from './BarChart'
@@ -12,31 +12,34 @@ import LineChart from './LineChart'
12
12
  import Context from '../context'
13
13
  import PairedBarChart from './PairedBarChart'
14
14
  import useIntersectionObserver from './useIntersectionObserver'
15
- import SparkLine from './SparkLine'
15
+ import CoveBoxPlot from './BoxPlot'
16
16
 
17
17
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
18
- import numberFromString from '@cdc/core/helpers/numberFromString'
19
18
  import '../scss/LinearChart.scss'
20
19
  import useReduceData from '../hooks/useReduceData'
21
20
  import useRightAxis from '../hooks/useRightAxis'
22
21
  import useTopAxis from '../hooks/useTopAxis'
23
22
 
24
23
  // TODO: Move scaling functions into hooks to manage complexity
25
-
26
- // TODO: remove unused imports/variables
27
- // TODO: consider moving logic into hooks
28
- // TODO: formatting
29
24
  export default function LinearChart() {
30
25
  const { transformedData: data, dimensions, config, parseDate, formatDate, currentViewport, formatNumber, handleChartAriaLabels, updateConfig } = useContext<any>(Context)
31
26
  let [width] = dimensions
32
- const { minValue, maxValue, existPositiveValue } = useReduceData(config, data)
27
+ const { minValue, maxValue, existPositiveValue, isAllLine } = useReduceData(config, data)
33
28
  const [animatedChart, setAnimatedChart] = useState<boolean>(false)
34
- const [animatedChartPlayed, setAnimatedChartPlayed] = useState<boolean>(false)
35
29
 
36
30
  const triggerRef = useRef()
37
31
  const dataRef = useIntersectionObserver(triggerRef, {
38
32
  freezeOnceVisible: false
39
33
  })
34
+ // Make sure the chart is visible if in the editor
35
+ useEffect(() => {
36
+ const element = document.querySelector('.isEditor')
37
+ if (element) {
38
+ // parent element is visible
39
+ setAnimatedChart(prevState => true)
40
+ }
41
+ })
42
+
40
43
  // If the chart is in view and set to animate and it has not already played
41
44
  useEffect(() => {
42
45
  if (dataRef?.isIntersecting === true && config.animate) {
@@ -49,10 +52,10 @@ export default function LinearChart() {
49
52
  if (config && config.legend && !config.legend.hide && config.legend.position !== 'bottom' && (currentViewport === 'lg' || currentViewport === 'md')) {
50
53
  width = width * 0.73
51
54
  }
52
-
53
- const height = config.aspectRatio ? width * config.aspectRatio : config.height
55
+ const { horizontal: heightHorizontal } = config.heights
56
+ const height = config.aspectRatio ? width * config.aspectRatio : config.heights[config.orientation]
54
57
  const xMax = width - config.runtime.yAxis.size - config.yAxis.rightAxisSize
55
- const yMax = height - config.runtime.xAxis.size
58
+ const yMax = height - (config.orientation === 'horizontal' ? 0 : config.runtime.xAxis.size)
56
59
 
57
60
  const { yScaleRight, hasRightAxis } = useRightAxis({ config, yMax, data, updateConfig })
58
61
  const { hasTopAxis } = useTopAxis(config)
@@ -65,19 +68,29 @@ export default function LinearChart() {
65
68
  let seriesScale
66
69
 
67
70
  const { max: enteredMaxValue, min: enteredMinValue } = config.runtime.yAxis
68
- const isMaxValid = existPositiveValue ? numberFromString(enteredMaxValue) >= numberFromString(maxValue) : numberFromString(enteredMaxValue) >= 0
69
- const isMinValid = (numberFromString(enteredMinValue) <= 0 && numberFromString(minValue) >= 0) || (numberFromString(enteredMinValue) <= minValue && minValue < 0)
71
+ const isMaxValid = existPositiveValue ? enteredMaxValue >= maxValue : enteredMaxValue >= 0
72
+ const isMinValid = (enteredMinValue <= 0 && minValue >= 0) || (enteredMinValue <= minValue && minValue < 0)
70
73
 
71
74
  if (data) {
72
75
  let min = enteredMinValue && isMinValid ? enteredMinValue : minValue
73
76
  let max = enteredMaxValue && isMaxValid ? enteredMaxValue : Number.MIN_VALUE
74
77
 
75
- if ((config.visualizationType === 'Bar' || config.visualizationType === 'Combo') && min > 0) {
78
+ if ((config.visualizationType === 'Bar' || (config.visualizationType === 'Combo' && !isAllLine)) && min > 0) {
76
79
  min = 0
77
80
  }
81
+ if (config.visualizationType === 'Combo' && isAllLine) {
82
+ if ((enteredMinValue === undefined || enteredMinValue === null || enteredMinValue === '') && min > 0) {
83
+ min = 0
84
+ }
85
+ if (enteredMinValue) {
86
+ const isMinValid = +enteredMinValue < minValue
87
+ min = +enteredMinValue && isMinValid ? enteredMinValue : minValue
88
+ }
89
+ }
90
+
78
91
  if (config.visualizationType === 'Line') {
79
- const isMinValid = Number(enteredMinValue) < Number(minValue)
80
- min = enteredMinValue && isMinValid ? Number(enteredMinValue) : minValue
92
+ const isMinValid = enteredMinValue < minValue
93
+ min = enteredMinValue && isMinValid ? enteredMinValue : minValue
81
94
  }
82
95
  //If data value max wasn't provided, calculate it
83
96
  if (max === Number.MIN_VALUE) {
@@ -171,32 +184,72 @@ export default function LinearChart() {
171
184
  }
172
185
  }
173
186
 
174
-
175
- const countNumOfTicks = (axis)=>{
187
+ const handleLeftTickFormatting = tick => {
188
+ if (config.runtime.yAxis.type === 'date') return formatDate(parseDate(tick))
189
+ if (config.orientation === 'vertical') return formatNumber(tick, 'left')
190
+ return tick
191
+ }
192
+
193
+ const handleBottomTickFormatting = tick => {
194
+ if (config.runtime.xAxis.type === 'date') return formatDate(tick)
195
+ if (config.orientation === 'horizontal') return formatNumber(tick, 'bottom')
196
+ return tick
197
+ }
198
+
199
+ const countNumOfTicks = axis => {
176
200
  // function get number of ticks based on bar type & users value
177
- const isHorizontal = config.orientation ==='horizontal';
178
- const {numTicks} = config.runtime[axis];
179
- let tickCount = undefined;
180
-
181
- if(axis === 'yAxis'){
182
- tickCount = (
183
- (isHorizontal && !numTicks) ? data.length
184
- : (isHorizontal && numTicks) ? numTicks
185
- :(!isHorizontal && !numTicks) ? undefined
186
- :(!isHorizontal && numTicks) && numTicks
187
- );
188
- };
189
-
190
- if(axis === 'xAxis'){
191
- tickCount = (
192
- (isHorizontal && !numTicks) ? undefined
193
- : (isHorizontal && numTicks) ? numTicks
194
- :(!isHorizontal && !numTicks) ? undefined
195
- :(!isHorizontal && numTicks) && numTicks
196
- );
197
- };
198
- return tickCount;
199
- };
201
+ const isHorizontal = config.orientation === 'horizontal'
202
+ const { numTicks } = config.runtime[axis]
203
+ let tickCount = undefined
204
+
205
+ if (axis === 'yAxis') {
206
+ tickCount = isHorizontal && !numTicks ? data.length : isHorizontal && numTicks ? numTicks : !isHorizontal && !numTicks ? undefined : !isHorizontal && numTicks && numTicks
207
+ }
208
+
209
+ if (axis === 'xAxis') {
210
+ tickCount = isHorizontal && !numTicks ? undefined : isHorizontal && numTicks ? numTicks : !isHorizontal && !numTicks ? undefined : !isHorizontal && numTicks && numTicks
211
+ }
212
+ return tickCount
213
+ }
214
+
215
+ // Handle Box Plots
216
+ if (config.visualizationType === 'Box Plot') {
217
+ let minYValue
218
+ let maxYValue
219
+ let allOutliers = []
220
+ let allLowerBounds = config.boxplot.map(plot => plot.columnMin)
221
+ let allUpperBounds = config.boxplot.map(plot => plot.columnMax)
222
+
223
+ minYValue = Math.min(...allLowerBounds)
224
+ maxYValue = Math.max(...allUpperBounds)
225
+
226
+ const hasOutliers = config.boxplot.map(b => b.columnOutliers.map(outlier => allOutliers.push(outlier)))
227
+
228
+ if (hasOutliers) {
229
+ let outlierMin = Math.min(...allOutliers)
230
+ let outlierMax = Math.max(...allOutliers)
231
+
232
+ // check if outliers exceed standard bounds
233
+ if (outlierMin < minYValue) minYValue = outlierMin
234
+ if (outlierMax > maxYValue) maxYValue = outlierMax
235
+ }
236
+
237
+ const seriesNames = data.map(d => d[config.xAxis.dataKey])
238
+
239
+ // Set Scales
240
+ yScale = scaleLinear({
241
+ range: [yMax, 0],
242
+ round: true,
243
+ domain: [minYValue, maxYValue]
244
+ })
245
+
246
+ xScale = scaleBand({
247
+ range: [0, xMax],
248
+ round: true,
249
+ domain: config.boxplot.categories,
250
+ padding: 0.4
251
+ })
252
+ }
200
253
 
201
254
  useEffect(() => {
202
255
  ReactTooltip.rebuild()
@@ -217,7 +270,7 @@ export default function LinearChart() {
217
270
  const width = to - from
218
271
 
219
272
  return (
220
- <Group className='regions' left={config.runtime.yAxis.size} key={region.label}>
273
+ <Group className='regions' left={Number(config.runtime.yAxis.size)} key={region.label}>
221
274
  <path
222
275
  stroke='#333'
223
276
  d={`M${from} -5
@@ -238,22 +291,16 @@ export default function LinearChart() {
238
291
 
239
292
  {/* Y axis */}
240
293
  {config.visualizationType !== 'Spark Line' && (
241
- <AxisLeft
242
- scale={yScale}
243
- left={config.runtime.yAxis.size}
244
- label={config.runtime.yAxis.label}
245
- stroke='#333'
246
- tickFormat={tick => (config.runtime.yAxis.type === 'date' ? formatDate(parseDate(tick)) : config.orientation === 'vertical' ? formatNumber(tick) : tick)}
247
- numTicks={countNumOfTicks('yAxis')}
248
- >
294
+ <AxisLeft scale={yScale} left={Number(config.runtime.yAxis.size)} label={config.runtime.yAxis.label} stroke='#333' tickFormat={tick => handleLeftTickFormatting(tick)} numTicks={countNumOfTicks('yAxis')}>
249
295
  {props => {
250
- const lollipopShapeSize = config.lollipopSize === 'large' ? 14 : config.lollipopSize === 'medium' ? 12 : 10
251
296
  const axisCenter = config.runtime.horizontal ? (props.axisToPoint.y - props.axisFromPoint.y) / 2 : (props.axisFromPoint.y - props.axisToPoint.y) / 2
252
297
  const horizontalTickOffset = yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
253
- const belowBarPaddingFromTop = 9
254
298
  return (
255
299
  <Group className='left-axis'>
256
300
  {props.ticks.map((tick, i) => {
301
+ const minY = props.ticks[0].to.y
302
+ const barMinHeight = 15 // 15 is the min height for bars by default
303
+
257
304
  return (
258
305
  <Group key={`vx-tick-${tick.value}-${i}`} className={'vx-axis-tick'}>
259
306
  {!config.runtime.yAxis.hideTicks && <Line from={tick.from} to={tick.to} stroke={config.yAxis.tickColor} display={config.runtime.horizontal ? 'none' : 'block'} />}
@@ -261,23 +308,22 @@ export default function LinearChart() {
261
308
  {config.runtime.yAxis.gridLines ? <Line from={{ x: tick.from.x + xMax, y: tick.from.y }} to={tick.from} stroke='rgba(0,0,0,0.3)' /> : ''}
262
309
 
263
310
  {config.orientation === 'horizontal' && config.visualizationSubType !== 'stacked' && config.yAxis.labelPlacement === 'On Date/Category Axis' && !config.yAxis.hideLabel && (
264
- // 17 is a magic number from the offset in barchart.
265
- <Fragment>
266
- <Text transform={`translate(${tick.to.x - 5}, ${config.isLollipopChart ? tick.from.y : tick.from.y - 17}) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation : 0})`} verticalAnchor={config.isLollipopChart ? 'middle' : 'middle'} textAnchor={'end'}>
267
- {tick.formattedValue}
268
- </Text>
269
- </Fragment>
311
+ <Text
312
+ transform={`translate(${tick.to.x - 5}, ${config.isLollipopChart ? tick.to.y - minY : tick.to.y - minY + (Number(config.barHeight * config.series.length) - barMinHeight) / 2}) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation : 0})`}
313
+ verticalAnchor={'start'}
314
+ textAnchor={'end'}
315
+ >
316
+ {tick.formattedValue}
317
+ </Text>
270
318
  )}
271
319
 
272
320
  {config.orientation === 'horizontal' && config.visualizationSubType === 'stacked' && config.yAxis.labelPlacement === 'On Date/Category Axis' && !config.yAxis.hideLabel && (
273
- // 17 is a magic number from the offset in barchart.
274
- <Text transform={`translate(${tick.to.x - 5}, ${tick.from.y - config.barHeight / 2 - 3}) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation : 0})`} verticalAnchor={config.isLollipopChart ? 'middle' : 'middle'} textAnchor={'end'}>
321
+ <Text transform={`translate(${tick.to.x - 5}, ${tick.to.y - minY + (Number(config.barHeight) - barMinHeight) / 2}) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation : 0})`} verticalAnchor={'start'} textAnchor={'end'}>
275
322
  {tick.formattedValue}
276
323
  </Text>
277
324
  )}
278
325
 
279
326
  {config.orientation === 'horizontal' && config.visualizationType === 'Paired Bar' && !config.yAxis.hideLabel && (
280
- // 17 is a magic number from the offset in barchart.
281
327
  <Text transform={`translate(${-15}, ${tick.from.y}) rotate(-${config.runtime.horizontal ? config.runtime.yAxis.tickRotation : 0})`} verticalAnchor={config.isLollipopChart ? 'middle' : 'middle'} textAnchor={'end'}>
282
328
  {tick.formattedValue}
283
329
  </Text>
@@ -304,7 +350,7 @@ export default function LinearChart() {
304
350
  </Group>
305
351
  )
306
352
  })}
307
- {!config.yAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
353
+ {!config.yAxis.hideAxis && <Line from={props.axisFromPoint} to={config.runtime.horizontal ? { x: 0, y: Number(heightHorizontal) } : props.axisToPoint} stroke='#000' />}
308
354
  {yScale.domain()[0] < 0 && <Line from={{ x: props.axisFromPoint.x, y: yScale(0) }} to={{ x: xMax, y: yScale(0) }} stroke='#333' />}
309
355
  <Text className='y-label' textAnchor='middle' verticalAnchor='start' transform={`translate(${-1 * config.runtime.yAxis.size}, ${axisCenter}) rotate(-90)`} fontWeight='bold' fill={config.yAxis.labelColor}>
310
356
  {props.label}
@@ -317,7 +363,7 @@ export default function LinearChart() {
317
363
 
318
364
  {/* Right Axis */}
319
365
  {hasRightAxis && (
320
- <AxisRight scale={yScaleRight} left={width - config.yAxis.rightAxisSize} label={config.yAxis.rightLabel} tickFormat={tick => formatNumber(tick, 'right')} numTicks={config.runtime.yAxis.rightNumTicks || undefined} labelOffset={45}>
366
+ <AxisRight scale={yScaleRight} left={Number(width - config.yAxis.rightAxisSize)} label={config.yAxis.rightLabel} tickFormat={tick => formatNumber(tick, 'right')} numTicks={config.runtime.yAxis.rightNumTicks || undefined} labelOffset={45}>
321
367
  {props => {
322
368
  const axisCenter = config.runtime.horizontal ? (props.axisToPoint.y - props.axisFromPoint.y) / 2 : (props.axisFromPoint.y - props.axisToPoint.y) / 2
323
369
  const horizontalTickOffset = yMax / props.ticks.length / 2 - (yMax / props.ticks.length) * (1 - config.barThickness) + 5
@@ -351,7 +397,7 @@ export default function LinearChart() {
351
397
  {hasTopAxis && config.topAxis.hasLine && (
352
398
  <AxisTop
353
399
  stroke='#333'
354
- left={config.runtime.yAxis.size}
400
+ left={Number(config.runtime.yAxis.size)}
355
401
  scale={xScale}
356
402
  hideTicks
357
403
  hideZero
@@ -363,16 +409,7 @@ export default function LinearChart() {
363
409
 
364
410
  {/* X axis */}
365
411
  {config.visualizationType !== 'Paired Bar' && config.visualizationType !== 'Spark Line' && (
366
- <AxisBottom
367
- top={yMax}
368
- left={config.runtime.yAxis.size}
369
- label={config.runtime.xAxis.label}
370
- tickFormat={tick => (config.runtime.xAxis.type === 'date' ? formatDate(tick) : config.orientation === 'horizontal' ? formatNumber(tick) : tick)}
371
- scale={xScale}
372
- stroke='#333'
373
- tickStroke='#333'
374
- numTicks={countNumOfTicks('xAxis')}
375
- >
412
+ <AxisBottom top={yMax} left={Number(config.runtime.yAxis.size)} label={config.runtime.xAxis.label} tickFormat={handleBottomTickFormatting} scale={xScale} stroke='#333' tickStroke='#333' numTicks={countNumOfTicks('xAxis')}>
376
413
  {props => {
377
414
  const axisCenter = (props.axisToPoint.x - props.axisFromPoint.x) / 2
378
415
  return (
@@ -397,7 +434,7 @@ export default function LinearChart() {
397
434
  )
398
435
  })}
399
436
  {!config.xAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
400
- <Text x={axisCenter} y={config.runtime.xAxis.size} textAnchor='middle' verticalAnchor='end' fontWeight='bold' fill={config.xAxis.labelColor}>
437
+ <Text x={axisCenter} y={config.orientation === 'horizontal' ? config.xAxis.labelOffset : config.xAxis.size} textAnchor='middle' fontWeight='bold' fill={config.xAxis.labelColor}>
401
438
  {props.label}
402
439
  </Text>
403
440
  </Group>
@@ -408,7 +445,7 @@ export default function LinearChart() {
408
445
 
409
446
  {config.visualizationType === 'Paired Bar' && (
410
447
  <>
411
- <AxisBottom top={yMax} left={config.runtime.yAxis.size} label={config.runtime.xAxis.label} tickFormat={config.runtime.xAxis.type === 'date' ? formatDate : formatNumber} scale={g1xScale} stroke='#333' tickStroke='#333' numTicks={config.runtime.xAxis.numTicks || undefined}>
448
+ <AxisBottom top={yMax} left={Number(config.runtime.yAxis.size)} label={config.runtime.xAxis.label} tickFormat={config.runtime.xAxis.type === 'date' ? formatDate : formatNumber} scale={g1xScale} stroke='#333' tickStroke='#333' numTicks={config.runtime.xAxis.numTicks || undefined}>
412
449
  {props => {
413
450
  const axisCenter = (props.axisToPoint.x - props.axisFromPoint.x) / 2
414
451
  return (
@@ -433,7 +470,7 @@ export default function LinearChart() {
433
470
  </AxisBottom>
434
471
  <AxisBottom
435
472
  top={yMax}
436
- left={config.runtime.yAxis.size}
473
+ left={Number(config.runtime.yAxis.size)}
437
474
  label={config.runtime.xAxis.label}
438
475
  tickFormat={config.runtime.xAxis.type === 'date' ? formatDate : config.runtime.xAxis.dataKey !== 'Year' ? formatNumber : tick => tick}
439
476
  scale={g2xScale}
@@ -462,7 +499,7 @@ export default function LinearChart() {
462
499
  {!config.runtime.yAxis.hideAxis && <Line from={props.axisFromPoint} to={props.axisToPoint} stroke='#333' />}
463
500
  </Group>
464
501
  <Group>
465
- <Text transform={`translate(${xMax / 2}, ${config.height - yMax + 20}) rotate(-${0})`} verticalAnchor='start' textAnchor={'middle'} stroke='#333'>
502
+ <Text transform={`translate(${xMax / 2}, ${yMax + 20}) rotate(-${0})`} verticalAnchor='start' textAnchor={'middle'} stroke='#333'>
466
503
  {config.runtime.xAxis.label}
467
504
  </Text>
468
505
  </Group>
@@ -475,18 +512,20 @@ export default function LinearChart() {
475
512
  {config.visualizationType === 'Paired Bar' && <PairedBarChart width={xMax} height={yMax} />}
476
513
 
477
514
  {/* Bar chart */}
478
- {config.visualizationType !== 'Line' && config.visualizationType !== 'Paired Bar' && (
515
+ {config.visualizationType !== 'Line' && config.visualizationType !== 'Paired Bar' && config.visualizationType !== 'Box Plot' && (
479
516
  <>
480
517
  <BarChart xScale={xScale} yScale={yScale} seriesScale={seriesScale} xMax={xMax} yMax={yMax} getXAxisData={getXAxisData} getYAxisData={getYAxisData} animatedChart={animatedChart} visible={animatedChart} />
481
518
  </>
482
519
  )}
483
520
 
484
521
  {/* Line chart */}
485
- {config.visualizationType !== 'Bar' && config.visualizationType !== 'Paired Bar' && (
522
+ {config.visualizationType !== 'Bar' && config.visualizationType !== 'Paired Bar' && config.visualizationType !== 'Box Plot' && (
486
523
  <>
487
524
  <LineChart xScale={xScale} yScale={yScale} getXAxisData={getXAxisData} getYAxisData={getYAxisData} xMax={xMax} yMax={yMax} seriesStyle={config.series} />
488
525
  </>
489
526
  )}
527
+
528
+ {config.visualizationType === 'Box Plot' && <CoveBoxPlot xScale={xScale} yScale={yScale} />}
490
529
  </svg>
491
530
  <ReactTooltip id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`} html={true} type='light' arrowColor='rgba(0,0,0,0)' className='tooltip' />
492
531
  <div className='animation-trigger' ref={triggerRef} />
@@ -97,7 +97,7 @@ const PairedBarChart: React.FC<PairedBarChartProps> = ({ width, height }) => {
97
97
  `}
98
98
  </style>
99
99
  <svg id='cdc-visualization__paired-bar-chart' width={width} height={height} viewBox={`0 0 ${width} ${height}`} role='img' tabIndex={0}>
100
- <Group top={0} left={config.xAxis.size}>
100
+ <Group top={0} left={Number(config.xAxis.size)}>
101
101
  {data
102
102
  .filter(item => config.series[0].dataKey === groupOne.dataKey)
103
103
  .map((d, index) => {