@cdc/chart 4.24.12 → 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 (72) hide show
  1. package/dist/cdcchart.js +79611 -78971
  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/ehdi.json +29939 -0
  6. package/examples/private/not-loading.json +360 -0
  7. package/index.html +7 -14
  8. package/package.json +2 -2
  9. package/src/CdcChart.tsx +92 -1512
  10. package/src/CdcChartComponent.tsx +1105 -0
  11. package/src/_stories/Chart.Anchors.stories.tsx +1 -1
  12. package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
  13. package/src/_stories/Chart.DynamicSeries.stories.tsx +1 -1
  14. package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
  15. package/src/_stories/Chart.ScatterPlot.stories.tsx +19 -0
  16. package/src/_stories/Chart.tooltip.stories.tsx +1 -2
  17. package/src/_stories/ChartAnnotation.stories.tsx +1 -1
  18. package/src/_stories/ChartAxisLabels.stories.tsx +1 -1
  19. package/src/_stories/ChartAxisTitles.stories.tsx +1 -1
  20. package/src/_stories/ChartEditor.stories.tsx +1 -1
  21. package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
  22. package/src/_stories/ChartLine.Symbols.stories.tsx +18 -0
  23. package/src/_stories/ChartPrefixSuffix.stories.tsx +1 -1
  24. package/src/_stories/_mock/line_chart_symbols.json +437 -0
  25. package/src/_stories/_mock/scatterplot-image-download.json +1244 -0
  26. package/src/components/Annotations/components/AnnotationDraggable.tsx +3 -11
  27. package/src/components/Annotations/components/AnnotationDropdown.tsx +3 -3
  28. package/src/components/Axis/Categorical.Axis.tsx +3 -4
  29. package/src/components/BarChart/components/BarChart.Horizontal.tsx +14 -5
  30. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +10 -4
  31. package/src/components/BarChart/components/BarChart.Vertical.tsx +2 -2
  32. package/src/components/BoxPlot/BoxPlot.tsx +34 -32
  33. package/src/components/BoxPlot/helpers/index.ts +108 -18
  34. package/src/components/DeviationBar.jsx +2 -6
  35. package/src/components/EditorPanel/EditorPanel.tsx +62 -6
  36. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +4 -0
  37. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +44 -7
  38. package/src/components/ForestPlot/ForestPlot.tsx +176 -26
  39. package/src/components/Legend/Legend.Component.tsx +29 -38
  40. package/src/components/Legend/Legend.Suppression.tsx +3 -5
  41. package/src/components/Legend/Legend.tsx +2 -2
  42. package/src/components/Legend/LegendLine.Shape.tsx +51 -0
  43. package/src/components/Legend/helpers/createFormatLabels.tsx +29 -26
  44. package/src/components/Legend/helpers/getLegendClasses.ts +20 -38
  45. package/src/components/Legend/helpers/index.ts +14 -7
  46. package/src/components/Legend/tests/getLegendClasses.test.ts +3 -20
  47. package/src/components/LineChart/components/LineChart.Circle.tsx +90 -88
  48. package/src/components/LineChart/index.tsx +4 -0
  49. package/src/components/LinearChart.tsx +65 -31
  50. package/src/components/PairedBarChart.jsx +2 -9
  51. package/src/components/ZoomBrush.tsx +5 -7
  52. package/src/data/initial-state.js +6 -3
  53. package/src/helpers/getBoxPlotConfig.ts +68 -0
  54. package/src/helpers/getColorScale.ts +28 -0
  55. package/src/helpers/getComboChartConfig.ts +42 -0
  56. package/src/helpers/getExcludedData.ts +37 -0
  57. package/src/helpers/getTopAxis.ts +7 -0
  58. package/src/hooks/useBarChart.ts +28 -9
  59. package/src/hooks/{useHighlightedBars.js → useHighlightedBars.ts} +2 -1
  60. package/src/hooks/useIntersectionObserver.ts +37 -0
  61. package/src/hooks/useMinMax.ts +4 -0
  62. package/src/hooks/useReduceData.ts +1 -1
  63. package/src/hooks/useTooltip.tsx +9 -1
  64. package/src/index.jsx +1 -0
  65. package/src/scss/DataTable.scss +0 -5
  66. package/src/scss/main.scss +30 -115
  67. package/src/types/ChartConfig.ts +6 -3
  68. package/src/types/ChartContext.ts +1 -3
  69. package/src/helpers/getQuartiles.ts +0 -27
  70. package/src/hooks/useColorScale.ts +0 -50
  71. package/src/hooks/useIntersectionObserver.jsx +0 -29
  72. 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>
@@ -233,12 +233,12 @@ export const BarChartVertical = () => {
233
233
  const xPos = barX + (config.xAxis.type !== 'date-time' ? barWidth / 2 : 0)
234
234
 
235
235
  const upperPos = yScale(
236
- datum.dynamicData && datum.CI[bar.key]
236
+ datum.dynamicData && datum?.CI?.[bar.key]
237
237
  ? datum.CI[bar.key].upper
238
238
  : datum[config.confidenceKeys.upper]
239
239
  )
240
240
  const lowerPos = yScale(
241
- datum.dynamicData && datum.CI[bar.key]
241
+ datum.dynamicData && datum?.CI?.[bar.key]
242
242
  ? datum.CI[bar.key].lower
243
243
  : datum[config.confidenceKeys.lower]
244
244
  )
@@ -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