@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.
- package/dist/cdcchart.js +79611 -78971
- package/examples/feature/boxplot/boxplot.json +2 -157
- package/examples/feature/boxplot/testing.csv +23 -38
- package/examples/feature/tests-non-numerics/example-combo-bar-nonnumeric.json +394 -30
- package/examples/private/ehdi.json +29939 -0
- package/examples/private/not-loading.json +360 -0
- package/index.html +7 -14
- package/package.json +2 -2
- package/src/CdcChart.tsx +92 -1512
- package/src/CdcChartComponent.tsx +1105 -0
- package/src/_stories/Chart.Anchors.stories.tsx +1 -1
- package/src/_stories/Chart.CustomColors.stories.tsx +1 -1
- package/src/_stories/Chart.DynamicSeries.stories.tsx +1 -1
- package/src/_stories/Chart.Legend.Gradient.stories.tsx +2 -2
- package/src/_stories/Chart.ScatterPlot.stories.tsx +19 -0
- package/src/_stories/Chart.tooltip.stories.tsx +1 -2
- package/src/_stories/ChartAnnotation.stories.tsx +1 -1
- package/src/_stories/ChartAxisLabels.stories.tsx +1 -1
- package/src/_stories/ChartAxisTitles.stories.tsx +1 -1
- package/src/_stories/ChartEditor.stories.tsx +1 -1
- package/src/_stories/ChartLine.Suppression.stories.tsx +1 -1
- package/src/_stories/ChartLine.Symbols.stories.tsx +18 -0
- package/src/_stories/ChartPrefixSuffix.stories.tsx +1 -1
- package/src/_stories/_mock/line_chart_symbols.json +437 -0
- package/src/_stories/_mock/scatterplot-image-download.json +1244 -0
- package/src/components/Annotations/components/AnnotationDraggable.tsx +3 -11
- package/src/components/Annotations/components/AnnotationDropdown.tsx +3 -3
- package/src/components/Axis/Categorical.Axis.tsx +3 -4
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +14 -5
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +10 -4
- package/src/components/BarChart/components/BarChart.Vertical.tsx +2 -2
- package/src/components/BoxPlot/BoxPlot.tsx +34 -32
- package/src/components/BoxPlot/helpers/index.ts +108 -18
- package/src/components/DeviationBar.jsx +2 -6
- package/src/components/EditorPanel/EditorPanel.tsx +62 -6
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +4 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +44 -7
- package/src/components/ForestPlot/ForestPlot.tsx +176 -26
- package/src/components/Legend/Legend.Component.tsx +29 -38
- package/src/components/Legend/Legend.Suppression.tsx +3 -5
- package/src/components/Legend/Legend.tsx +2 -2
- package/src/components/Legend/LegendLine.Shape.tsx +51 -0
- package/src/components/Legend/helpers/createFormatLabels.tsx +29 -26
- package/src/components/Legend/helpers/getLegendClasses.ts +20 -38
- package/src/components/Legend/helpers/index.ts +14 -7
- package/src/components/Legend/tests/getLegendClasses.test.ts +3 -20
- package/src/components/LineChart/components/LineChart.Circle.tsx +90 -88
- package/src/components/LineChart/index.tsx +4 -0
- package/src/components/LinearChart.tsx +65 -31
- package/src/components/PairedBarChart.jsx +2 -9
- package/src/components/ZoomBrush.tsx +5 -7
- package/src/data/initial-state.js +6 -3
- package/src/helpers/getBoxPlotConfig.ts +68 -0
- package/src/helpers/getColorScale.ts +28 -0
- package/src/helpers/getComboChartConfig.ts +42 -0
- package/src/helpers/getExcludedData.ts +37 -0
- package/src/helpers/getTopAxis.ts +7 -0
- package/src/hooks/useBarChart.ts +28 -9
- package/src/hooks/{useHighlightedBars.js → useHighlightedBars.ts} +2 -1
- package/src/hooks/useIntersectionObserver.ts +37 -0
- package/src/hooks/useMinMax.ts +4 -0
- package/src/hooks/useReduceData.ts +1 -1
- package/src/hooks/useTooltip.tsx +9 -1
- package/src/index.jsx +1 -0
- package/src/scss/DataTable.scss +0 -5
- package/src/scss/main.scss +30 -115
- package/src/types/ChartConfig.ts +6 -3
- package/src/types/ChartContext.ts +1 -3
- package/src/helpers/getQuartiles.ts +0 -27
- package/src/hooks/useColorScale.ts +0 -50
- package/src/hooks/useIntersectionObserver.jsx +0 -29
- 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
|
-
|
|
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
|
|
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 {
|
|
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' : `${
|
|
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 =
|
|
97
|
+
const textSize = appFontSize / 1.3
|
|
99
98
|
const textColor = chroma(bar.color).luminance() < 0.4 ? '#fff' : '#000'
|
|
100
|
-
const textWidth = getTextWidth(bar.key
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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={
|
|
71
|
-
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={
|
|
76
|
-
stroke={
|
|
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:
|
|
82
|
-
opacity: fillOpacity
|
|
80
|
+
fill: defaultColor,
|
|
81
|
+
opacity: fillOpacity,
|
|
82
|
+
stroke: defaultColor
|
|
83
83
|
}
|
|
84
84
|
}}
|
|
85
85
|
medianProps={{
|
|
86
86
|
style: {
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
opacity: fillOpacity,
|
|
88
|
+
stroke: defaultColor
|
|
89
89
|
}
|
|
90
90
|
}}
|
|
91
91
|
boxProps={{
|
|
92
92
|
style: {
|
|
93
|
-
stroke:
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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,
|
|
16
|
+
export const handleTooltip = (boxplot, columnCategory, key, q1, q3, median, iqr, label, color) => {
|
|
9
17
|
return `
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
${
|
|
15
|
-
|
|
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 ||
|
|
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(
|
|
24
|
-
max: max(
|
|
25
|
-
median: median(
|
|
26
|
-
firstQuartile
|
|
27
|
-
thirdQuartile
|
|
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
|
|
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
|
|
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
|