@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.
- package/dist/cdcchart.js +79411 -78816
- 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/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/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 +54 -29
- 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>
|
|
@@ -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
|
|
@@ -610,7 +610,7 @@ const EditorPanel = () => {
|
|
|
610
610
|
lineOptions,
|
|
611
611
|
rawData,
|
|
612
612
|
highlight,
|
|
613
|
-
|
|
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
|
-
|
|
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).
|