@cdc/chart 4.24.12-2 → 4.25.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 (70) hide show
  1. package/dist/cdcchart.js +79411 -78816
  2. package/examples/feature/boxplot/boxplot.json +2 -157
  3. package/examples/feature/boxplot/testing.csv +23 -38
  4. package/examples/feature/tests-non-numerics/example-combo-bar-nonnumeric.json +394 -30
  5. package/examples/private/not-loading.json +360 -0
  6. package/index.html +7 -14
  7. package/package.json +2 -2
  8. package/src/CdcChart.tsx +92 -1512
  9. package/src/CdcChartComponent.tsx +1105 -0
  10. package/src/_stories/Chart.Anchors.stories.tsx +1 -1
  11. package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
  12. package/src/_stories/Chart.DynamicSeries.stories.tsx +1 -1
  13. package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
  14. package/src/_stories/Chart.ScatterPlot.stories.tsx +19 -0
  15. package/src/_stories/Chart.tooltip.stories.tsx +1 -2
  16. package/src/_stories/ChartAnnotation.stories.tsx +1 -1
  17. package/src/_stories/ChartAxisLabels.stories.tsx +1 -1
  18. package/src/_stories/ChartAxisTitles.stories.tsx +1 -1
  19. package/src/_stories/ChartEditor.stories.tsx +1 -1
  20. package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
  21. package/src/_stories/ChartLine.Symbols.stories.tsx +18 -0
  22. package/src/_stories/ChartPrefixSuffix.stories.tsx +1 -1
  23. package/src/_stories/_mock/line_chart_symbols.json +437 -0
  24. package/src/_stories/_mock/scatterplot-image-download.json +1244 -0
  25. package/src/components/Annotations/components/AnnotationDraggable.tsx +3 -11
  26. package/src/components/Annotations/components/AnnotationDropdown.tsx +3 -3
  27. package/src/components/Axis/Categorical.Axis.tsx +3 -4
  28. package/src/components/BarChart/components/BarChart.Horizontal.tsx +14 -5
  29. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +10 -4
  30. package/src/components/BoxPlot/BoxPlot.tsx +34 -32
  31. package/src/components/BoxPlot/helpers/index.ts +108 -18
  32. package/src/components/DeviationBar.jsx +2 -6
  33. package/src/components/EditorPanel/EditorPanel.tsx +62 -6
  34. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +4 -0
  35. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +44 -7
  36. package/src/components/ForestPlot/ForestPlot.tsx +176 -26
  37. package/src/components/Legend/Legend.Component.tsx +29 -38
  38. package/src/components/Legend/Legend.Suppression.tsx +3 -5
  39. package/src/components/Legend/Legend.tsx +2 -2
  40. package/src/components/Legend/LegendLine.Shape.tsx +51 -0
  41. package/src/components/Legend/helpers/createFormatLabels.tsx +29 -26
  42. package/src/components/Legend/helpers/getLegendClasses.ts +20 -38
  43. package/src/components/Legend/helpers/index.ts +14 -7
  44. package/src/components/Legend/tests/getLegendClasses.test.ts +3 -20
  45. package/src/components/LineChart/components/LineChart.Circle.tsx +90 -88
  46. package/src/components/LineChart/index.tsx +4 -0
  47. package/src/components/LinearChart.tsx +54 -29
  48. package/src/components/PairedBarChart.jsx +2 -9
  49. package/src/components/ZoomBrush.tsx +5 -7
  50. package/src/data/initial-state.js +6 -3
  51. package/src/helpers/getBoxPlotConfig.ts +68 -0
  52. package/src/helpers/getColorScale.ts +28 -0
  53. package/src/helpers/getComboChartConfig.ts +42 -0
  54. package/src/helpers/getExcludedData.ts +37 -0
  55. package/src/helpers/getTopAxis.ts +7 -0
  56. package/src/hooks/useBarChart.ts +28 -9
  57. package/src/hooks/{useHighlightedBars.js → useHighlightedBars.ts} +2 -1
  58. package/src/hooks/useIntersectionObserver.ts +37 -0
  59. package/src/hooks/useMinMax.ts +4 -0
  60. package/src/hooks/useReduceData.ts +1 -1
  61. package/src/hooks/useTooltip.tsx +9 -1
  62. package/src/index.jsx +1 -0
  63. package/src/scss/DataTable.scss +0 -5
  64. package/src/scss/main.scss +30 -115
  65. package/src/types/ChartConfig.ts +6 -3
  66. package/src/types/ChartContext.ts +1 -3
  67. package/src/helpers/getQuartiles.ts +0 -27
  68. package/src/hooks/useColorScale.ts +0 -50
  69. package/src/hooks/useIntersectionObserver.jsx +0 -29
  70. package/src/hooks/useTopAxis.js +0 -6
@@ -16,31 +16,23 @@ import {
16
16
  handleTextY
17
17
  } from './helpers'
18
18
 
19
- import useColorScale from '../../../hooks/useColorScale'
20
-
21
19
  // visx
22
20
  import { HtmlLabel, CircleSubject, EditableAnnotation, Connector, Annotation as VisxAnnotation } from '@visx/annotation'
23
21
  import { Drag } from '@visx/drag'
24
22
  import { MarkerArrow } from '@visx/marker'
25
23
  import { LinePath } from '@visx/shape'
26
- import { fontSizes } from '@cdc/core/helpers/cove/fontSettings'
27
24
 
28
25
  // styles
29
26
  import './AnnotationDraggable.styles.css'
30
27
 
31
28
  const Annotations = ({ xScale, yScale, xScaleAnnotation, xMax, svgRef, onDragStateChange }) => {
32
29
  // prettier-ignore
33
- const {
34
- config,
35
- dimensions,
36
- isEditor,
37
- updateConfig
38
- } = useContext(ConfigContext)
30
+ const { config, dimensions, isEditor, updateConfig, colorScale } = useContext(ConfigContext)
39
31
 
40
32
  // destructure config items here...
41
33
  const { annotations } = config
42
34
  const [height] = dimensions
43
- const { colorScale } = useColorScale()
35
+
44
36
  const AnnotationComponent = isEditor ? EditableAnnotation : VisxAnnotation
45
37
 
46
38
  return (
@@ -134,7 +126,7 @@ const Annotations = ({ xScale, yScale, xScaleAnnotation, xMax, svgRef, onDragSta
134
126
  </p>
135
127
  </>
136
128
  )}
137
- <div style={{ fontSize: fontSizes[config.fontSize] }} dangerouslySetInnerHTML={sanitizedData()} />
129
+ <div dangerouslySetInnerHTML={sanitizedData()} />
138
130
  </div>
139
131
  </HtmlLabel>
140
132
  {annotation.connectionType === 'line' && (
@@ -3,13 +3,13 @@ import ConfigContext from '../../../ConfigContext'
3
3
  import './AnnotationDropdown.styles.css'
4
4
  import Icon from '@cdc/core/components/ui/Icon'
5
5
  import Annotation from '..'
6
- import { fontSizes } from '@cdc/core/helpers/cove/fontSettings'
6
+ import { appFontSize } from '@cdc/core/helpers/cove/fontSettings'
7
7
 
8
8
  const AnnotationDropdown = () => {
9
9
  const { currentViewport: viewport, config } = useContext(ConfigContext)
10
10
  const [expanded, setExpanded] = useState(false)
11
11
 
12
- const titleFontSize = ['sm', 'xs', 'xxs'].includes(viewport) ? '13px' : `${fontSizes[config?.fontSize]}px`
12
+ const titleFontSize = ['sm', 'xs', 'xxs'].includes(viewport) ? '13px' : `${appFontSize}px`
13
13
 
14
14
  const {
15
15
  config: { annotations }
@@ -21,7 +21,7 @@ const AnnotationDropdown = () => {
21
21
  }
22
22
 
23
23
  const handleAccordionClassName = () => {
24
- const classNames = ['data-table-heading', 'annotation__dropdown-list']
24
+ const classNames = ['data-table-heading', 'annotation__dropdown-list', 'p-3']
25
25
  if (!expanded) {
26
26
  classNames.push('collapsed')
27
27
  }
@@ -6,12 +6,11 @@ import { Text } from '@visx/text'
6
6
  import ConfigContext from '../../ConfigContext'
7
7
  import chroma from 'chroma-js'
8
8
  import createBarElement from '@cdc/core/components/createBarElement'
9
- import { useBarChart } from '../../hooks/useBarChart'
10
9
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
10
+ import { appFontSize } from '@cdc/core/helpers/cove/fontSettings'
11
11
 
12
12
  const CategoricalYAxis = ({ yMax, leftSize, max, xMax }) => {
13
13
  const { config } = useContext(ConfigContext)
14
- const { fontSize } = useBarChart()
15
14
 
16
15
  const { orientation } = config
17
16
 
@@ -95,9 +94,9 @@ const CategoricalYAxis = ({ yMax, leftSize, max, xMax }) => {
95
94
  barStacks.map(barStack =>
96
95
  barStack.bars.map(bar => {
97
96
  const isLastIndex = config.yAxis.categories.length - 1 === barStack.index
98
- const textSize = fontSize[config.fontSize] / 1.3
97
+ const textSize = appFontSize / 1.3
99
98
  const textColor = chroma(bar.color).luminance() < 0.4 ? '#fff' : '#000'
100
- const textWidth = getTextWidth(bar.key, `normal ${textSize}px sans-serif`)
99
+ const textWidth = getTextWidth(bar.key)
101
100
  const displayText = Number(textWidth) < bar.width && bar.height > textSize
102
101
  const tooltip = `<ul>
103
102
  <li class="tooltip-heading""> Label : ${bar.key} </li>
@@ -11,7 +11,7 @@ import { Text } from '@visx/text'
11
11
  import { BarGroup } from '@visx/shape'
12
12
 
13
13
  // CDC core components and helpers
14
- import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
14
+ import { getColorContrast, getContrastColor } from '@cdc/core/helpers/cove/accessibility'
15
15
  import createBarElement from '@cdc/core/components/createBarElement'
16
16
  import { getBarConfig, testZeroValue } from '../helpers'
17
17
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
@@ -45,7 +45,6 @@ export const BarChartHorizontal = () => {
45
45
  updateBars,
46
46
  assignColorsToValues,
47
47
  section,
48
- fontSize,
49
48
  isLabelBelowBar,
50
49
  displayNumbersOnBar,
51
50
  lollipopBarWidth,
@@ -136,7 +135,7 @@ export const BarChartHorizontal = () => {
136
135
  const barDefaultLabel = !config.yAxis.displayNumbersOnBar ? '' : yAxisValue
137
136
 
138
137
  // check if bar text/value string fits into each bars.
139
- const textWidth = getTextWidth(barDefaultLabel, `normal ${fontSize[config.fontSize]}px sans-serif`)
138
+ const textWidth = getTextWidth(barDefaultLabel)
140
139
  const textFits = Number(textWidth) < defaultBarWidth - 5
141
140
 
142
141
  // control text position
@@ -201,8 +200,12 @@ export const BarChartHorizontal = () => {
201
200
  // update label color
202
201
  if (barColor && labelColor && textFits) {
203
202
  labelColor = getContrastColor('#000', barColor)
203
+ let constrast = getColorContrast('#000', barColor)
204
+ const contrastLevel = 7
205
+ if (constrast < contrastLevel) {
206
+ labelColor = '#fff'
207
+ }
204
208
  }
205
-
206
209
  const background = () => {
207
210
  if (isRegularLollipopColor) return barColor
208
211
  if (isTwoToneLollipopColor) return chroma(barColor).brighten(1)
@@ -215,7 +218,13 @@ export const BarChartHorizontal = () => {
215
218
  const yPos = barHeight * bar.index + barHeight / 2
216
219
  const [upperPos, lowerPos] = ['upper', 'lower'].map(position => {
217
220
  if (!hasConfidenceInterval) return
218
- const d = datum.dynamicData ? datum.CI[bar.key][position] : datum[config.confidenceKeys[position]]
221
+ if (datum.dynamicData) {
222
+ const ci = datum.CI[bar.key]
223
+ if (!ci) return
224
+ const d = ci[position]
225
+ return xScale(d)
226
+ }
227
+ const d = datum[config.confidenceKeys[position]]
219
228
  return xScale(d)
220
229
  })
221
230
  // End Confidence Interval Variables
@@ -4,7 +4,7 @@ import { useBarChart } from '../../../hooks/useBarChart'
4
4
  import { BarStackHorizontal } from '@visx/shape'
5
5
  import { Group } from '@visx/group'
6
6
  import { Text } from '@visx/text'
7
- import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
7
+ import { getColorContrast, getContrastColor } from '@cdc/core/helpers/cove/accessibility'
8
8
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
9
9
 
10
10
  // types
@@ -30,7 +30,7 @@ const BarChartStackedHorizontal = () => {
30
30
  } = useContext<ChartContext>(ConfigContext)
31
31
 
32
32
  // prettier-ignore
33
- const { barBorderWidth, displayNumbersOnBar, fontSize, getAdditionalColumn, hoveredBar, isHorizontal, isLabelBelowBar, onMouseLeaveBar, onMouseOverBar, updateBars, barStackedSeriesKeys } = useBarChart()
33
+ const { barBorderWidth, displayNumbersOnBar, getAdditionalColumn, hoveredBar, isHorizontal, isLabelBelowBar, onMouseLeaveBar, onMouseOverBar, updateBars, barStackedSeriesKeys } = useBarChart()
34
34
 
35
35
  const { orientation, visualizationSubType } = config
36
36
  return (
@@ -59,7 +59,13 @@ const BarChartStackedHorizontal = () => {
59
59
  seriesHighlight.length === 0 ||
60
60
  seriesHighlight.indexOf(bar.key) !== -1
61
61
  config.barHeight = Number(config.barHeight)
62
- const labelColor = getContrastColor('#000', colorScale(config.runtime.seriesLabels[bar.key]))
62
+ let barColor = colorScale(config.runtime.seriesLabels[bar.key])
63
+ let labelColor = getContrastColor('#000', barColor)
64
+ let constrast = getColorContrast('#000', barColor)
65
+ const contrastLevel = 7
66
+ if (constrast < contrastLevel) {
67
+ labelColor = '#fff'
68
+ }
63
69
  // tooltips
64
70
  const xAxisValue = formatNumber(data[bar.index][bar.key], 'left')
65
71
  const yAxisValue =
@@ -69,7 +75,7 @@ const BarChartStackedHorizontal = () => {
69
75
  const yAxisTooltip = config.runtime.yAxis.label
70
76
  ? `${config.runtime.yAxis.label}: ${yAxisValue}`
71
77
  : yAxisValue
72
- const textWidth = getTextWidth(xAxisValue, `normal ${fontSize[config.fontSize]}px sans-serif`)
78
+ const textWidth = getTextWidth(xAxisValue)
73
79
  const additionalColTooltip = getAdditionalColumn(hoveredBar)
74
80
  const tooltipBody = `${config.runtime.seriesLabels[bar.key]}: ${xAxisValue}`
75
81
  const tooltip = `<ul>
@@ -4,7 +4,7 @@ import { Group } from '@visx/group'
4
4
  import ConfigContext from '../../ConfigContext'
5
5
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
6
6
  import { colorPalettesChart } from '@cdc/core/data/colorPalettes'
7
- import { handleTooltip, calculateBoxPlotStats, createPlots } from './helpers/index'
7
+ import { handleTooltip, createPlots } from './helpers/index'
8
8
  import _ from 'lodash'
9
9
 
10
10
  const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
@@ -13,13 +13,16 @@ const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
13
13
 
14
14
  const tooltip_id = `cdc-open-viz-tooltip-${config.runtime.uniqueId}`
15
15
  const boxWidth = xScale.bandwidth()
16
+
17
+ const bodyStyles = getComputedStyle(document.body)
18
+ const defaultColor = bodyStyles.getPropertyValue('--cool-gray-90').trim()
16
19
  const constrainedWidth = Math.min(40, boxWidth)
17
20
  const color_0 = _.get(colorPalettesChart, [config.palette, 0], '#000')
18
-
21
+ const plots = createPlots(data, config)
19
22
  return (
20
23
  <ErrorBoundary component='BoxPlot'>
21
24
  <Group left={Number(config.yAxis.size)} className='boxplot' key={`boxplot-group`}>
22
- {createPlots(data, config).map((d, i) => {
25
+ {plots.map((d, i) => {
23
26
  const offset = boxWidth - constrainedWidth
24
27
  const radius = 4
25
28
 
@@ -29,10 +32,6 @@ const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
29
32
  left={xScale(d.columnCategory) + (xScale.bandwidth() - seriesScale.bandwidth()) / 2}
30
33
  >
31
34
  {config.series.map((item, index) => {
32
- const valuesByKey = d.keyValues[item.dataKey]
33
- const { min, max, median, firstQuartile, thirdQuartile } = calculateBoxPlotStats(valuesByKey)
34
- let iqr = Number(thirdQuartile - firstQuartile).toFixed(config.dataFormat.roundTo)
35
-
36
35
  const isTransparent =
37
36
  config.legend.behavior === 'highlight' &&
38
37
  seriesHighlight.length > 0 &&
@@ -42,19 +41,19 @@ const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
42
41
  seriesHighlight.length === 0 ||
43
42
  seriesHighlight.indexOf(item.dataKey) !== -1
44
43
  const fillOpacity = isTransparent ? 0.3 : 0.5
45
-
46
44
  return (
47
45
  <Group key={`boxplotplot-${item.dataKey}-${index}`}>
48
46
  {boxplot.plotNonOutlierValues &&
49
- valuesByKey.map((value, index) => {
47
+ d.columnNonOutliers[item.dataKey].map((value, index) => {
50
48
  return (
51
49
  <circle
52
50
  display={displayPlot ? 'block' : 'none'}
53
51
  cx={seriesScale(item.dataKey) + seriesScale.bandwidth() / 2}
54
52
  cy={yScale(value)}
55
53
  r={radius}
56
- fill={'#ccc'}
57
- style={{ opacity: fillOpacity, fillOpacity: 1, stroke: 'black' }}
54
+ opacity={fillOpacity}
55
+ fill={defaultColor}
56
+ style={{ stroke: defaultColor }}
58
57
  key={`boxplot-${i}--circle-${index}`}
59
58
  />
60
59
  )
@@ -64,53 +63,56 @@ const CoveBoxPlot = ({ xScale, yScale, seriesScale }) => {
64
63
  display={displayPlot ? 'block' : 'none'}
65
64
  data-left={xScale(d.columnCategory) + config.yAxis.size + offset / 2 + 0.5}
66
65
  key={`box-plot-${i}-${item}`}
67
- min={Number(min)}
68
- max={Number(max)}
66
+ min={Number(d.min[item.dataKey])}
67
+ max={Number(d.max[item.dataKey])}
69
68
  left={seriesScale(item.dataKey)}
70
- firstQuartile={firstQuartile}
71
- thirdQuartile={thirdQuartile}
72
- median={median}
69
+ firstQuartile={d.q1[item.dataKey]}
70
+ thirdQuartile={d.q3[item.dataKey]}
71
+ median={d.median[item.dataKey]}
73
72
  boxWidth={seriesScale.bandwidth()}
74
73
  fill={colorScale(item.dataKey)}
75
- fillOpacity={fillOpacity}
76
- stroke={fillOpacity ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.1)'}
74
+ fillOpacity={1}
75
+ stroke={defaultColor}
77
76
  valueScale={yScale}
78
- outliers={boxplot.plotOutlierValues ? d.columnOutliers : []}
77
+ outliers={boxplot.plotOutlierValues ? _.map(d.columnOutliers[item.dataKey], item => item) : []}
79
78
  outlierProps={{
80
79
  style: {
81
- fill: `${color_0}`,
82
- opacity: fillOpacity
80
+ fill: defaultColor,
81
+ opacity: fillOpacity,
82
+ stroke: defaultColor
83
83
  }
84
84
  }}
85
85
  medianProps={{
86
86
  style: {
87
- stroke: 'black',
88
- opacity: fillOpacity
87
+ opacity: fillOpacity,
88
+ stroke: defaultColor
89
89
  }
90
90
  }}
91
91
  boxProps={{
92
92
  style: {
93
- stroke: 'black',
94
- strokeWidth: boxplot.borders === 'true' ? 1 : 0,
93
+ stroke: defaultColor,
94
+ strokeWidth: boxplot.borders === 'true' ? 1.5 : 0,
95
95
  opacity: fillOpacity
96
96
  }
97
97
  }}
98
98
  maxProps={{
99
99
  style: {
100
- stroke: 'black',
101
- opacity: fillOpacity
100
+ opacity: fillOpacity,
101
+ stroke: defaultColor
102
102
  }
103
103
  }}
104
104
  container
105
105
  containerProps={{
106
106
  'data-tooltip-html': handleTooltip(
107
107
  boxplot,
108
- d,
108
+ d.columnCategory,
109
109
  item.dataKey,
110
- firstQuartile,
111
- thirdQuartile,
112
- median,
113
- iqr
110
+ _.round(d.q1[item.dataKey], config.dataFormat.roundTo),
111
+ _.round(d.q3[item.dataKey], config.dataFormat.roundTo),
112
+ _.round(d.median[item.dataKey], config.dataFormat.roundTo),
113
+ _.round(d.iqr[item.dataKey], config.dataFormat.roundTo),
114
+ config.xAxis.label,
115
+ defaultColor
114
116
  ),
115
117
  'data-tooltip-id': tooltip_id,
116
118
  tabIndex: -1
@@ -1,32 +1,65 @@
1
- import { max, min, median, quantile } from 'd3-array'
2
1
  import _ from 'lodash'
2
+ import * as d3 from 'd3-array'
3
3
 
4
4
  interface Plot {
5
5
  columnCategory: string
6
- keyValues: { [key: string]: number[] }
6
+ columnOutliers: Record<string, number[]>
7
+ columnNonOutliers: Record<string, number[]>
8
+ keyValues: Record<string, number[]>
9
+ min: Record<string, number | null>
10
+ max: Record<string, number | null>
11
+ q1: Record<string, number>
12
+ q3: Record<string, number>
13
+ median: Record<string, number | null>
14
+ iqr: Record<string, number>
7
15
  }
8
- export const handleTooltip = (boxplot, d, key, q1, q3, median, iqr) => {
16
+ export const handleTooltip = (boxplot, columnCategory, key, q1, q3, median, iqr, label, color) => {
9
17
  return `
10
- <strong>${d.columnCategory}</strong></br>
11
- <strong>Key:${key}</strong></br>
12
- ${boxplot.labels.q1}: ${q1}<br/>
13
- ${boxplot.labels.q3}: ${q3}<br/>
14
- ${boxplot.labels.iqr}: ${iqr}<br/>
15
- ${boxplot.labels.median}: ${median}
16
- `
18
+ <div class="p-2 text-red" style="max-width: 300px; word-wrap: break-word; opacity:0.7; background: rgba(255, 255, 255, 0.9)">
19
+ <div class="fw-bold" style="color: ${color};">
20
+ ${label ? `${label} : ${columnCategory}` : columnCategory}
21
+ </div>
22
+ <div class="" style="background: ${color}; height: 2px;"></div>
23
+ <strong>Key:</strong> ${key}<br/>
24
+ <strong>${boxplot.labels.q1}:</strong> ${q1}<br/>
25
+ <strong>${boxplot.labels.q3}:</strong> ${q3}<br/>
26
+ <strong>${boxplot.labels.iqr}:</strong> ${iqr}<br/>
27
+ <strong>${boxplot.labels.median}:</strong> ${median}
28
+ </div>
29
+ `
17
30
  }
18
31
 
19
- export const calculateBoxPlotStats = values => {
20
- if (!values || !values.length) return {}
32
+ export const calculateBoxPlotStats = (values: number[]) => {
33
+ if (!values || values.length === 0) return {}
34
+
35
+ // Sort the values
21
36
  const sortedValues = _.sortBy(values)
37
+
38
+ // Quartiles
39
+ const firstQuartile = d3.quantile(sortedValues, 0.25) ?? 0
40
+ const thirdQuartile = d3.quantile(sortedValues, 0.75) ?? 0
41
+
42
+ // Interquartile Range (IQR)
43
+ const iqr = thirdQuartile - firstQuartile
44
+
45
+ // Outlier Bounds
46
+ const lowerBound = firstQuartile - 1.5 * iqr
47
+ const upperBound = thirdQuartile + 1.5 * iqr
48
+
49
+ // Non-Outlier Values
50
+ const nonOutliers = sortedValues.filter(value => value >= lowerBound && value <= upperBound)
51
+
52
+ // Calculate Box Plot Stats
22
53
  return {
23
- min: min(values),
24
- max: max(values),
25
- median: median(values),
26
- firstQuartile: quantile(sortedValues, 0.25),
27
- thirdQuartile: quantile(sortedValues, 0.75)
54
+ min: d3.min(nonOutliers), // Smallest non-outlier value
55
+ max: d3.max(nonOutliers), // Largest non-outlier value
56
+ median: d3.median(sortedValues), // Median of all values
57
+ firstQuartile,
58
+ thirdQuartile,
59
+ iqr
28
60
  }
29
61
  }
62
+
30
63
  const getValuesBySeriesKey = (group: string, config, data) => {
31
64
  const allSeriesKeys = config.series.map(item => item?.dataKey)
32
65
  const result = {}
@@ -38,17 +71,74 @@ const getValuesBySeriesKey = (group: string, config, data) => {
38
71
  return result
39
72
  }
40
73
 
74
+ // Helper to calculate outliers based on IQR
75
+ export const calculateOutliers = (values: number[], firstQuartile: number, thirdQuartile: number) => {
76
+ const iqr = thirdQuartile - firstQuartile
77
+ const lowerBound = firstQuartile - 1.5 * iqr
78
+ const upperBound = thirdQuartile + 1.5 * iqr
79
+ return values.filter(value => value < lowerBound || value > upperBound)
80
+ }
81
+
82
+ // Helper to calculate non-outliers based on IQR
83
+ export const calculateNonOutliers = (values: number[], firstQuartile: number, thirdQuartile: number): number[] => {
84
+ const iqr = thirdQuartile - firstQuartile
85
+ const lowerBound = firstQuartile - 1.5 * iqr
86
+ const upperBound = thirdQuartile + 1.5 * iqr
87
+
88
+ // Return values within the bounds
89
+ return values.filter(value => value >= lowerBound && value <= upperBound)
90
+ }
91
+
92
+ // Main function to create plots with additional outlier data
41
93
  export const createPlots = (data, config) => {
42
94
  const dataKeys = data.map(d => d[config.xAxis.dataKey])
43
95
  const plots: Plot[] = []
44
96
  const groups: string[] = _.uniq(dataKeys)
97
+
45
98
  if (groups && groups.length > 0) {
46
99
  groups.forEach(group => {
100
+ const keyValues = getValuesBySeriesKey(group, config, data)
101
+ const columnOutliers: Record<string, number[]> = {}
102
+ const columnNonOutliers: Record<string, number[]> = {}
103
+ const columnMedian = {}
104
+ const columnMin = {}
105
+ const columnMax = {}
106
+ const columnQ1 = {}
107
+ const columnQ3 = {}
108
+ const columnIqr = {}
109
+
110
+ // Calculate outliers and non-outliers for each series key
111
+ Object.keys(keyValues).forEach(key => {
112
+ const values = keyValues[key]
113
+
114
+ // Calculate box plot statistics
115
+ const { firstQuartile, thirdQuartile, min, max, median, iqr } = calculateBoxPlotStats(values)
116
+ // Calculate outliers and non-outliers
117
+ columnOutliers[key] = calculateOutliers(values, firstQuartile, thirdQuartile).map(Number)
118
+ columnNonOutliers[key] = calculateNonOutliers(values, firstQuartile, thirdQuartile).map(Number)
119
+ columnMedian[key] = median
120
+ columnMin[key] = min
121
+ columnMax[key] = max
122
+ columnQ1[key] = firstQuartile
123
+ columnQ3[key] = thirdQuartile
124
+ columnIqr[key] = iqr
125
+ })
126
+
127
+ // Add the plot object to the plots array
47
128
  plots.push({
48
129
  columnCategory: group,
49
- keyValues: getValuesBySeriesKey(group, config, data)
130
+ keyValues,
131
+ columnOutliers,
132
+ columnNonOutliers,
133
+ min: columnMin,
134
+ max: columnMax,
135
+ q1: columnQ1,
136
+ q3: columnQ3,
137
+ median: columnMedian,
138
+ iqr: columnIqr
50
139
  })
51
140
  })
52
141
  }
142
+
53
143
  return plots
54
144
  }
@@ -30,7 +30,6 @@ export default function DeviationBar({ height, xScale }) {
30
30
  : roundingStyle === 'finger'
31
31
  ? '15px'
32
32
  : '0px'
33
- const fontSize = { small: 16, medium: 18, large: 20 }
34
33
  const isRounded = config.barStyle === 'rounded'
35
34
  const target = Number(config.xAxis.target)
36
35
  const seriesKey = config.series[0].dataKey
@@ -65,7 +64,7 @@ export default function DeviationBar({ height, xScale }) {
65
64
  const firstBarValue = data[0][seriesKey]
66
65
  const barPosition = firstBarValue < target ? 'left' : 'right'
67
66
  const label = `${config.xAxis.targetLabel} ${formatNumber(config.xAxis.target || 0, 'left')}`
68
- const labelWidth = getTextWidth(label, `bold ${fontSize[config.fontSize]}px sans-serif`)
67
+ const labelWidth = getTextWidth(label)
69
68
  let labelY = config.isLollipopChart ? lollipopBarHeight / 2 : Number(config.barHeight) / 2
70
69
  let paddingX = 0
71
70
  let labelX = 0
@@ -165,10 +164,7 @@ export default function DeviationBar({ height, xScale }) {
165
164
  config.heights.horizontal = totalheight
166
165
 
167
166
  // text,labels postiions
168
- const textWidth = getTextWidth(
169
- formatNumber(barValue, 'left'),
170
- `normal ${fontSize[config.fontSize]}px sans-serif`
171
- )
167
+ const textWidth = getTextWidth(formatNumber(barValue, 'left'))
172
168
  const textFits = textWidth < barWidth - 6
173
169
  const textX = barBaseX
174
170
  const textY = barY + barHeight / 2
@@ -610,7 +610,7 @@ const EditorPanel = () => {
610
610
  lineOptions,
611
611
  rawData,
612
612
  highlight,
613
- highlightReset,
613
+ handleShowAll,
614
614
  dimensions
615
615
  } = useContext<ChartContext>(ConfigContext)
616
616
 
@@ -906,7 +906,7 @@ const EditorPanel = () => {
906
906
  return Object.keys(columns)
907
907
  }
908
908
 
909
- const getLegendStyleOptions = (option: 'style' | 'subStyle'): string[] => {
909
+ const getLegendStyleOptions = (option: 'style' | 'subStyle' | 'shapes'): string[] => {
910
910
  const options: string[] = []
911
911
 
912
912
  switch (option) {
@@ -963,7 +963,7 @@ const EditorPanel = () => {
963
963
 
964
964
  const convertStateToConfig = () => {
965
965
  let strippedState = JSON.parse(JSON.stringify(config))
966
- if (false === missingRequiredSections()) {
966
+ if (false === missingRequiredSections(config)) {
967
967
  delete strippedState.newViz
968
968
  }
969
969
  delete strippedState.runtime
@@ -1631,8 +1631,19 @@ const EditorPanel = () => {
1631
1631
  value={config.yAxis.label}
1632
1632
  section='yAxis'
1633
1633
  fieldName='label'
1634
- label='Label '
1634
+ label='Label'
1635
1635
  updateField={updateField}
1636
+ maxLength={35}
1637
+ tooltip={
1638
+ <Tooltip style={{ textTransform: 'none' }}>
1639
+ <Tooltip.Target>
1640
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
1641
+ </Tooltip.Target>
1642
+ <Tooltip.Content>
1643
+ <p>35 character limit</p>
1644
+ </Tooltip.Content>
1645
+ </Tooltip>
1646
+ }
1636
1647
  />
1637
1648
  {config.runtime.seriesKeys &&
1638
1649
  config.runtime.seriesKeys.length === 1 &&
@@ -2306,6 +2317,17 @@ const EditorPanel = () => {
2306
2317
  fieldName='rightLabel'
2307
2318
  label='Label'
2308
2319
  updateField={updateField}
2320
+ maxLength={35}
2321
+ tooltip={
2322
+ <Tooltip style={{ textTransform: 'none' }}>
2323
+ <Tooltip.Target>
2324
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
2325
+ </Tooltip.Target>
2326
+ <Tooltip.Content>
2327
+ <p>35 character limit</p>
2328
+ </Tooltip.Content>
2329
+ </Tooltip>
2330
+ }
2309
2331
  />
2310
2332
  <TextField
2311
2333
  value={config.yAxis.rightNumTicks}
@@ -2596,6 +2618,17 @@ const EditorPanel = () => {
2596
2618
  fieldName='label'
2597
2619
  label='Label'
2598
2620
  updateField={updateField}
2621
+ maxLength={35}
2622
+ tooltip={
2623
+ <Tooltip style={{ textTransform: 'none' }}>
2624
+ <Tooltip.Target>
2625
+ <Icon display='question' style={{ marginLeft: '0.5rem' }} />
2626
+ </Tooltip.Target>
2627
+ <Tooltip.Content>
2628
+ <p>35 character limit</p>
2629
+ </Tooltip.Content>
2630
+ </Tooltip>
2631
+ }
2599
2632
  />
2600
2633
 
2601
2634
  {config.xAxis.type === 'continuous' && (
@@ -3658,6 +3691,27 @@ const EditorPanel = () => {
3658
3691
  updateField={updateField}
3659
3692
  options={getLegendStyleOptions('style')}
3660
3693
  />
3694
+ <CheckBox
3695
+ tooltip={
3696
+ <Tooltip style={{ textTransform: 'none' }}>
3697
+ <Tooltip.Target>
3698
+ <Icon
3699
+ display='question'
3700
+ style={{ marginLeft: '0.5rem', display: 'inline-block', whiteSpace: 'nowrap' }}
3701
+ />
3702
+ </Tooltip.Target>
3703
+ <Tooltip.Content>
3704
+ <p>Choose option Shapes in Line Datapoint Symbols to display.</p>
3705
+ </Tooltip.Content>
3706
+ </Tooltip>
3707
+ }
3708
+ display={!config.legend.hide && config.legend.style === 'lines'}
3709
+ value={config.legend.hasShape}
3710
+ section='legend'
3711
+ fieldName='hasShape'
3712
+ label='Shapes'
3713
+ updateField={updateField}
3714
+ />
3661
3715
 
3662
3716
  <Select
3663
3717
  display={!config.legend.hide && config.legend.style === 'gradient'}
@@ -3797,7 +3851,7 @@ const EditorPanel = () => {
3797
3851
  updatedSeriesHighlight.splice(i, 1)
3798
3852
  updateField('legend', null, 'seriesHighlight', updatedSeriesHighlight)
3799
3853
  if (!updatedSeriesHighlight.length) {
3800
- highlightReset()
3854
+ handleShowAll()
3801
3855
  }
3802
3856
  }}
3803
3857
  >
@@ -3887,7 +3941,9 @@ const EditorPanel = () => {
3887
3941
  display={
3888
3942
  ['bottom', 'top'].includes(config.legend.position) &&
3889
3943
  !config.legend.hide &&
3890
- config.legend.style !== 'gradient'
3944
+ config.legend.style !== 'gradient' &&
3945
+ !config.legend.singleRow &&
3946
+ !config.legend.singleRow
3891
3947
  }
3892
3948
  value={config.legend.verticalSorted}
3893
3949
  section='legend'
@@ -209,6 +209,10 @@ const PanelGeneral: FC<PanelProps> = props => {
209
209
  <Icon display='question' style={{ marginLeft: '0.5rem' }} />
210
210
  </Tooltip.Target>
211
211
  <Tooltip.Content>
212
+ <p>
213
+ Recommended set to display for Section 508 compliance.
214
+ </p>
215
+ <hr/>
212
216
  <p>
213
217
  Selecting this option will <i> not </i> hide the display of "zero value", "suppressed data", or
214
218
  "missing data" indicators on the chart (if applicable).