@cdc/chart 4.25.1 → 4.25.2-25
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 +40297 -40036
- package/examples/feature/tests-non-numerics/example-combo-bar-nonnumeric.json +396 -230
- package/examples/private/line-issue.json +497 -0
- package/index.html +6 -3
- package/package.json +2 -2
- package/src/CdcChartComponent.tsx +113 -105
- package/src/ConfigContext.tsx +6 -1
- package/src/_stories/Chart.DynamicSeries.stories.tsx +16 -1
- package/src/_stories/Chart.Filters.stories.tsx +19 -0
- package/src/components/Axis/Categorical.Axis.tsx +1 -1
- package/src/components/BarChart/components/BarChart.Vertical.tsx +3 -5
- package/src/components/BarChart/components/BarChart.jsx +24 -4
- package/src/components/BarChart/components/context.tsx +1 -0
- package/src/components/BrushChart.tsx +44 -24
- package/src/components/DeviationBar.jsx +2 -2
- package/src/components/EditorPanel/EditorPanel.tsx +2 -2
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +3 -1
- package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +6 -1
- package/src/components/Legend/helpers/index.ts +10 -4
- package/src/components/LineChart/components/LineChart.Circle.tsx +17 -9
- package/src/components/LineChart/index.tsx +2 -2
- package/src/components/LinearChart.tsx +12 -12
- package/src/components/ZoomBrush.tsx +1 -1
- package/src/helpers/getColorScale.ts +5 -9
- package/src/helpers/isConvertLineToBarGraph.ts +10 -3
- package/src/hooks/useBarChart.ts +12 -4
- package/src/hooks/useMinMax.ts +7 -8
- package/src/hooks/useScales.ts +10 -0
- package/src/hooks/useTooltip.tsx +12 -1
- package/src/scss/main.scss +1 -1
- package/src/store/chart.actions.ts +40 -0
- package/src/store/chart.reducer.ts +83 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Group } from '@visx/group'
|
|
2
2
|
import { useContext, useEffect, useRef, useState } from 'react'
|
|
3
|
-
import ConfigContext from '../ConfigContext'
|
|
3
|
+
import ConfigContext, { ChartDispatchContext } from '../ConfigContext'
|
|
4
4
|
import * as d3 from 'd3'
|
|
5
5
|
import { Text } from '@visx/text'
|
|
6
6
|
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
@@ -12,7 +12,8 @@ interface BrushChartProps {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
|
|
15
|
-
const { tableData, config,
|
|
15
|
+
const { tableData, config, dashboardConfig, formatDate, parseDate } = useContext(ConfigContext)
|
|
16
|
+
const dispatch = useContext(ChartDispatchContext)
|
|
16
17
|
const [brushState, setBrushState] = useState({ isBrushing: false, selection: [] })
|
|
17
18
|
const [brushKey, setBrushKey] = useState(0)
|
|
18
19
|
const sharedFilters = dashboardConfig?.dashboard?.sharedFilters ?? []
|
|
@@ -54,6 +55,9 @@ const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
|
|
|
54
55
|
|
|
55
56
|
const brushHandle = (g, selection, firstDate, lastDate) => {
|
|
56
57
|
const textWidth = getTextWidth(firstDate, `normal ${16 / 1.1}px sans-serif`)
|
|
58
|
+
const textPositionLeft = selection[0] < textWidth ? 0 : -textWidth
|
|
59
|
+
const textPositionRight = xMax - selection[1] < textWidth ? -textWidth : 0
|
|
60
|
+
|
|
57
61
|
return g
|
|
58
62
|
.selectAll('.handle--custom')
|
|
59
63
|
.data([{ side: 'left' }, { side: 'right' }])
|
|
@@ -61,7 +65,7 @@ const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
|
|
|
61
65
|
const handleGroup = enter.append('g').attr('class', 'handle--custom')
|
|
62
66
|
handleGroup
|
|
63
67
|
.append('text')
|
|
64
|
-
.attr('x', d => (d.side === 'left' ?
|
|
68
|
+
.attr('x', d => (d.side === 'left' ? textPositionLeft : textPositionRight))
|
|
65
69
|
.attr('y', 30)
|
|
66
70
|
.text(d => (d.side === 'left' ? firstDate : lastDate))
|
|
67
71
|
.attr('font-size', '13px')
|
|
@@ -102,22 +106,25 @@ const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
|
|
|
102
106
|
const sortedData = _.sortBy(newFilteredData, item => new Date(item[config.xAxis.dataKey]))
|
|
103
107
|
|
|
104
108
|
// If ascending is false, reverse the sorted array
|
|
105
|
-
const finalData = !sortByRecentDate ? sortedData : sortedData.reverse()
|
|
109
|
+
const finalData: object[] = !sortByRecentDate ? sortedData : sortedData.reverse()
|
|
106
110
|
|
|
107
111
|
// Retrieve the start and end dates based on the sorted data array
|
|
108
|
-
const startDate = _.get(_.first(finalData), config.xAxis.dataKey, '')
|
|
109
|
-
const endDate = _.get(_.last(finalData), config.xAxis.dataKey, '')
|
|
112
|
+
const startDate: string = _.get(_.first(finalData), config.xAxis.dataKey, '')
|
|
113
|
+
const endDate: string = _.get(_.last(finalData), config.xAxis.dataKey, '')
|
|
110
114
|
// add custom blue colored handlers to each corners of brush
|
|
111
115
|
svg.selectAll('.handle--custom').remove()
|
|
112
|
-
//
|
|
113
|
-
const
|
|
116
|
+
// Parse and format the dates, setting them to an empty string if undefined
|
|
117
|
+
const parseAndFormatDate = date => (date ? formatDate(parseDate(date)) : '')
|
|
118
|
+
const formattedStartDate = parseAndFormatDate(startDate)
|
|
119
|
+
const formattedEndDate = parseAndFormatDate(endDate)
|
|
114
120
|
svg.call(brushHandle, selection, formattedStartDate, formattedEndDate)
|
|
115
121
|
|
|
116
|
-
|
|
122
|
+
const payload = {
|
|
117
123
|
active: config.brush.active,
|
|
118
124
|
isBrushing: isUserBrushing,
|
|
119
125
|
data: finalData
|
|
120
|
-
}
|
|
126
|
+
}
|
|
127
|
+
dispatch({ type: 'SET_BRUSH_CONFIG', payload: payload })
|
|
121
128
|
setBrushState({
|
|
122
129
|
isBrushing: true,
|
|
123
130
|
selection
|
|
@@ -150,22 +157,35 @@ const BrushChart = ({ xMax, yMax }: BrushChartProps) => {
|
|
|
150
157
|
|
|
151
158
|
if ((isFiltersActive || isExclusionsActive || isDashboardFilters) && config.brush?.active) {
|
|
152
159
|
setBrushKey(prevKey => prevKey + 1)
|
|
153
|
-
setBrushConfig(prev => {
|
|
154
|
-
return {
|
|
155
|
-
...prev,
|
|
156
|
-
data: tableData
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
160
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
...prev,
|
|
164
|
-
data: []
|
|
165
|
-
}
|
|
166
|
-
})
|
|
161
|
+
dispatch({ type: 'SET_BRUSH_CONFIG', payload: { ...config.brush, data: tableData } })
|
|
162
|
+
|
|
163
|
+
return () => dispatch({ type: 'SET_BRUSH_CONFIG', payload: { ...config.brush, data: [] } })
|
|
167
164
|
}, [config.filters, config.exclusions, config.brush?.active, isDashboardFilters])
|
|
168
|
-
|
|
165
|
+
|
|
166
|
+
// this effect handles where brush chart is missing on production. it helpes re render
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
let timeoutId = null
|
|
169
|
+
|
|
170
|
+
const checkAndInitializeBrush = () => {
|
|
171
|
+
if (xMax > 0) {
|
|
172
|
+
initializeBrush()
|
|
173
|
+
} else {
|
|
174
|
+
// Clear the existing timeout and set a new one
|
|
175
|
+
clearTimeout(timeoutId)
|
|
176
|
+
timeoutId = setTimeout(checkAndInitializeBrush, 500)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
checkAndInitializeBrush()
|
|
181
|
+
|
|
182
|
+
// Cleanup function to clear timeout
|
|
183
|
+
return () => {
|
|
184
|
+
if (timeoutId) {
|
|
185
|
+
clearTimeout(timeoutId)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}, [xMax])
|
|
169
189
|
|
|
170
190
|
// reset brush on keychange
|
|
171
191
|
useEffect(() => {
|
|
@@ -64,7 +64,7 @@ export default function DeviationBar({ height, xScale }) {
|
|
|
64
64
|
const firstBarValue = data[0][seriesKey]
|
|
65
65
|
const barPosition = firstBarValue < target ? 'left' : 'right'
|
|
66
66
|
const label = `${config.xAxis.targetLabel} ${formatNumber(config.xAxis.target || 0, 'left')}`
|
|
67
|
-
const labelWidth = getTextWidth(label)
|
|
67
|
+
const labelWidth = getTextWidth(label, `bold ${20}px sans-serif`)
|
|
68
68
|
let labelY = config.isLollipopChart ? lollipopBarHeight / 2 : Number(config.barHeight) / 2
|
|
69
69
|
let paddingX = 0
|
|
70
70
|
let labelX = 0
|
|
@@ -164,7 +164,7 @@ export default function DeviationBar({ height, xScale }) {
|
|
|
164
164
|
config.heights.horizontal = totalheight
|
|
165
165
|
|
|
166
166
|
// text,labels postiions
|
|
167
|
-
const textWidth = getTextWidth(formatNumber(barValue, 'left'))
|
|
167
|
+
const textWidth = getTextWidth(formatNumber(barValue, 'left'), `normal ${16}px sans-serif`)
|
|
168
168
|
const textFits = textWidth < barWidth - 6
|
|
169
169
|
const textX = barBaseX
|
|
170
170
|
const textY = barY + barHeight / 2
|
|
@@ -596,7 +596,7 @@ const EditorPanel = () => {
|
|
|
596
596
|
updateConfig,
|
|
597
597
|
tableData,
|
|
598
598
|
transformedData: data,
|
|
599
|
-
|
|
599
|
+
isLoading,
|
|
600
600
|
colorScale,
|
|
601
601
|
colorPalettes,
|
|
602
602
|
twoColorPalette,
|
|
@@ -813,7 +813,7 @@ const EditorPanel = () => {
|
|
|
813
813
|
const [displayPanel, setDisplayPanel] = useState(true)
|
|
814
814
|
const [displayViewportOverrides, setDisplayViewportOverrides] = useState(false)
|
|
815
815
|
|
|
816
|
-
if (
|
|
816
|
+
if (isLoading) {
|
|
817
817
|
return null
|
|
818
818
|
}
|
|
819
819
|
|
|
@@ -500,6 +500,8 @@ const SeriesInputName = props => {
|
|
|
500
500
|
|
|
501
501
|
updateConfig(newConfig)
|
|
502
502
|
}
|
|
503
|
+
// if series name is emty show default data value.
|
|
504
|
+
const value = series.name !== undefined && series.name !== series.dataKey ? series.name : series.dataKey
|
|
503
505
|
|
|
504
506
|
return (
|
|
505
507
|
<>
|
|
@@ -507,7 +509,7 @@ const SeriesInputName = props => {
|
|
|
507
509
|
<input
|
|
508
510
|
type='text'
|
|
509
511
|
key={`series-name-${i}`}
|
|
510
|
-
value={
|
|
512
|
+
value={value}
|
|
511
513
|
onChange={event => {
|
|
512
514
|
changeSeriesName(i, event.target.value)
|
|
513
515
|
}}
|
|
@@ -30,7 +30,12 @@ export const updateFieldRankByValue = (
|
|
|
30
30
|
newConfig.rankByValue = newValue
|
|
31
31
|
|
|
32
32
|
if (config.rankByValue && !newValue) {
|
|
33
|
-
const
|
|
33
|
+
const CIkeys: string[] = Object.values(config.confidenceKeys) as string[]
|
|
34
|
+
const seriesKeys: string[] = config.series.map(s => s.dataKey)
|
|
35
|
+
const keysToClean: string[] = seriesKeys.concat(CIkeys)
|
|
36
|
+
const cleanData = config?.xAxis?.dataKey
|
|
37
|
+
? transform.cleanData(config.data, config.xAxis.dataKey, keysToClean)
|
|
38
|
+
: config.data
|
|
34
39
|
const newData = preTransformedData.sort((a, b) => {
|
|
35
40
|
const aIndex = indexOfObj(cleanData, a)
|
|
36
41
|
const bIndex = indexOfObj(cleanData, b)
|
|
@@ -11,14 +11,20 @@ export const getGradientConfig = (config, formatLabels, colorScale) => {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export const getMarginTop = (isLegendBottom, config) => {
|
|
14
|
+
// margin between charts xAxis legend not to overlap axis labels,ticks.
|
|
15
|
+
const DEFAULT_MARGIN_TOP = 27
|
|
16
|
+
if (isLegendBottom && config.legend.hide) {
|
|
17
|
+
return '0px'
|
|
18
|
+
}
|
|
14
19
|
if (!isLegendBottom) {
|
|
15
20
|
return '0px'
|
|
16
21
|
}
|
|
17
|
-
if (isLegendBottom && config.brush
|
|
18
|
-
const
|
|
19
|
-
return `${config.brush.height
|
|
22
|
+
if (isLegendBottom && config.brush.active && !config.legend.hide) {
|
|
23
|
+
const additiolMargin = 25
|
|
24
|
+
return `${DEFAULT_MARGIN_TOP + config.brush.height + additiolMargin}px`
|
|
25
|
+
} else {
|
|
26
|
+
return `${DEFAULT_MARGIN_TOP}px`
|
|
20
27
|
}
|
|
21
|
-
return '27px'
|
|
22
28
|
}
|
|
23
29
|
export const getMarginBottom = (isLegendBottom, config) => {
|
|
24
30
|
const isLegendTop = config.legend?.position === 'top' && !config.legend.hide
|
|
@@ -63,7 +63,7 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
63
63
|
seriesIndex
|
|
64
64
|
} = props
|
|
65
65
|
const { lineDatapointStyle, visual } = config
|
|
66
|
-
const filtered = config?.
|
|
66
|
+
const filtered = config?.series.filter(s => s.dataKey === seriesKey)?.[0]
|
|
67
67
|
const Shape =
|
|
68
68
|
Glyphs[
|
|
69
69
|
config.visual.lineDatapointSymbol === 'standard' && seriesIndex < visual.maximumShapeAmount ? seriesIndex : 0
|
|
@@ -80,8 +80,8 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
80
80
|
seriesKey: string
|
|
81
81
|
) => {
|
|
82
82
|
const seriesLabels = config.runtime.seriesLabels || []
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const seriesLabelsAll = config.runtime.seriesLabelsAll || []
|
|
84
|
+
let color = displayArea ? colorScale(seriesLabels[hoveredKey] || seriesLabelsAll[seriesIndex]) : ' transparent'
|
|
85
85
|
if (config.lineDatapointColor === 'Lighter than Line' && color !== 'transparent' && color) {
|
|
86
86
|
color = chroma(color).brighten(1)
|
|
87
87
|
}
|
|
@@ -130,15 +130,23 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
130
130
|
if (!hoveredXValue) return
|
|
131
131
|
|
|
132
132
|
let hoveredSeriesValue
|
|
133
|
-
let hoveredSeriesIndex
|
|
134
133
|
let hoveredSeriesData = tooltipData.data.filter(d => d[0] === seriesKey)
|
|
135
134
|
let hoveredSeriesKey = hoveredSeriesData?.[0]?.[0]
|
|
136
135
|
let hoveredSeriesAxis = hoveredSeriesData?.[0]?.[2]
|
|
136
|
+
const dynamicSeriesConfig = config.runtime.series.find(s => s.dynamicCategory)
|
|
137
|
+
const originalDataKey = dynamicSeriesConfig?.originalDataKey ?? seriesKey
|
|
138
|
+
|
|
137
139
|
if (!hoveredSeriesKey) return
|
|
138
|
-
hoveredSeriesIndex = tooltipData?.data.indexOf(hoveredSeriesKey)
|
|
139
140
|
hoveredSeriesValue = tableData?.find(d => {
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
const dynamicCategory = dynamicSeriesConfig?.dynamicCategory
|
|
142
|
+
const matchingXValue = d[config.xAxis.dataKey] === hoveredXValue
|
|
143
|
+
if (!matchingXValue) return false
|
|
144
|
+
if (dynamicCategory) {
|
|
145
|
+
const match = d[dynamicCategory] === hoveredSeriesKey
|
|
146
|
+
return match
|
|
147
|
+
}
|
|
148
|
+
return true
|
|
149
|
+
})?.[originalDataKey]
|
|
142
150
|
|
|
143
151
|
// hoveredSeriesValue = extractNumber(hoveredSeriesValue)
|
|
144
152
|
return tooltipData?.data.map((tooltipItem, index) => {
|
|
@@ -190,10 +198,10 @@ const LineChartCircle = (props: LineChartCircleProps) => {
|
|
|
190
198
|
return isFirstPoint || isLastPoint || isMiddlePoint
|
|
191
199
|
}
|
|
192
200
|
|
|
193
|
-
if (drawIsolatedPoints(dataIndex, seriesKey)
|
|
201
|
+
if (drawIsolatedPoints(dataIndex, seriesKey)) {
|
|
194
202
|
const positionTop = filtered?.axis === 'Right' ? yScaleRight(d[filtered?.dataKey]) : yScale(d[filtered?.dataKey])
|
|
195
203
|
const positionLeft = getXPos(d[config.xAxis?.dataKey])
|
|
196
|
-
const color = colorScale(config.runtime.
|
|
204
|
+
const color = colorScale(config.runtime.seriesLabelsAll[seriesIndex])
|
|
197
205
|
|
|
198
206
|
return (
|
|
199
207
|
<g transform={transformShape(positionTop, positionLeft)}>
|
|
@@ -89,14 +89,14 @@ const LineChart = (props: LineChartProps) => {
|
|
|
89
89
|
opacity={
|
|
90
90
|
legend.behavior === 'highlight' &&
|
|
91
91
|
seriesHighlight.length > 0 &&
|
|
92
|
-
seriesHighlight.indexOf(
|
|
92
|
+
seriesHighlight.indexOf(seriesKey) === -1
|
|
93
93
|
? 0.5
|
|
94
94
|
: 1
|
|
95
95
|
}
|
|
96
96
|
display={
|
|
97
97
|
legend.behavior === 'highlight' ||
|
|
98
98
|
(seriesHighlight.length === 0 && !legend.dynamicLegend) ||
|
|
99
|
-
seriesHighlight.indexOf(
|
|
99
|
+
seriesHighlight.indexOf(seriesKey) !== -1
|
|
100
100
|
? 'block'
|
|
101
101
|
: 'none'
|
|
102
102
|
}
|
|
@@ -26,7 +26,6 @@ import Regions from './Regions'
|
|
|
26
26
|
import CategoricalYAxis from './Axis/Categorical.Axis'
|
|
27
27
|
|
|
28
28
|
// Helpers
|
|
29
|
-
import { isConvertLineToBarGraph } from '../helpers/isConvertLineToBarGraph'
|
|
30
29
|
import { isLegendWrapViewport, isMobileHeightViewport } from '@cdc/core/helpers/viewports'
|
|
31
30
|
import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
|
|
32
31
|
import { calcInitialHeight } from '../helpers/sizeHelpers'
|
|
@@ -65,6 +64,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
65
64
|
brushConfig,
|
|
66
65
|
colorScale,
|
|
67
66
|
config,
|
|
67
|
+
convertLineToBarGraph,
|
|
68
68
|
currentViewport,
|
|
69
69
|
dimensions,
|
|
70
70
|
formatDate,
|
|
@@ -257,10 +257,6 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
257
257
|
const useDateSpanMonths = isDateTime && dateSpanMonths > xTickCount
|
|
258
258
|
|
|
259
259
|
// GETTERS & FUNCTIONS
|
|
260
|
-
const checkLineToBarGraph = () => {
|
|
261
|
-
return isConvertLineToBarGraph(config.visualizationType, data, config.allowLineToBarGraph)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
260
|
const handleLeftTickFormatting = (tick, index, ticks) => {
|
|
265
261
|
if (isLogarithmicAxis && tick === 0.1) {
|
|
266
262
|
//when logarithmic scale applied change value of first tick
|
|
@@ -385,7 +381,8 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
385
381
|
const topLabelOnGridline = topYLabelRef.current && yAxis.labelsAboveGridlines
|
|
386
382
|
|
|
387
383
|
// Heights to add
|
|
388
|
-
|
|
384
|
+
|
|
385
|
+
const brushHeight = brush?.active ? brush?.height + brush?.height : 0
|
|
389
386
|
const forestRowsHeight = isForestPlot ? config.data.length * forestPlot.rowHeight : 0
|
|
390
387
|
const topLabelOnGridlineHeight = topLabelOnGridline ? topYLabelRef.current.getBBox().height : 0
|
|
391
388
|
const additionalHeight = axisBottomHeight + brushHeight + forestRowsHeight + topLabelOnGridlineHeight
|
|
@@ -732,7 +729,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
732
729
|
showTooltip={showTooltip}
|
|
733
730
|
/>
|
|
734
731
|
)}
|
|
735
|
-
{(visualizationType === 'Bar' || visualizationType === 'Combo' ||
|
|
732
|
+
{(visualizationType === 'Bar' || visualizationType === 'Combo' || convertLineToBarGraph) && (
|
|
736
733
|
<BarChart
|
|
737
734
|
xScale={xScale}
|
|
738
735
|
yScale={yScale}
|
|
@@ -751,7 +748,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
751
748
|
chartRef={svgRef}
|
|
752
749
|
/>
|
|
753
750
|
)}
|
|
754
|
-
{((visualizationType === 'Line' && !
|
|
751
|
+
{((visualizationType === 'Line' && !convertLineToBarGraph) ||
|
|
755
752
|
visualizationType === 'Combo' ||
|
|
756
753
|
visualizationType === 'Bump Chart') && (
|
|
757
754
|
<LineChart
|
|
@@ -815,7 +812,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
815
812
|
{!['Paired Bar', 'Box Plot', 'Area Chart', 'Scatter Plot', 'Deviation Bar', 'Forecasting', 'Bar'].includes(
|
|
816
813
|
visualizationType
|
|
817
814
|
) &&
|
|
818
|
-
!
|
|
815
|
+
!convertLineToBarGraph && (
|
|
819
816
|
<>
|
|
820
817
|
<LineChart
|
|
821
818
|
xScale={xScale}
|
|
@@ -948,7 +945,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
948
945
|
{config.chartMessage.noData}
|
|
949
946
|
</Text>
|
|
950
947
|
)}
|
|
951
|
-
{(config.visualizationType === 'Bar' ||
|
|
948
|
+
{(config.visualizationType === 'Bar' || convertLineToBarGraph) &&
|
|
952
949
|
config.tooltips.singleSeries &&
|
|
953
950
|
config.visual.horizontalHoverLine && (
|
|
954
951
|
<Group
|
|
@@ -967,7 +964,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
967
964
|
/>
|
|
968
965
|
</Group>
|
|
969
966
|
)}
|
|
970
|
-
{(config.visualizationType === 'Bar' ||
|
|
967
|
+
{(config.visualizationType === 'Bar' || convertLineToBarGraph) &&
|
|
971
968
|
config.tooltips.singleSeries &&
|
|
972
969
|
config.visual.verticalHoverLine && (
|
|
973
970
|
<Group key='tooltipLine-vertical' className='vertical-tooltip-line'>
|
|
@@ -1536,6 +1533,7 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
1536
1533
|
Object.entries(tooltipData.data).length > 0 &&
|
|
1537
1534
|
tooltipOpen &&
|
|
1538
1535
|
showTooltip &&
|
|
1536
|
+
!tooltipData?.data?.some(subArray => subArray.some(item => item === undefined)) &&
|
|
1539
1537
|
tooltipData.dataYPosition &&
|
|
1540
1538
|
tooltipData.dataXPosition && (
|
|
1541
1539
|
<>
|
|
@@ -1551,7 +1549,9 @@ const LinearChart = forwardRef<SVGAElement, LinearChartProps>(({ parentHeight, p
|
|
|
1551
1549
|
>
|
|
1552
1550
|
<ul>
|
|
1553
1551
|
{typeof tooltipData === 'object' &&
|
|
1554
|
-
Object.entries(tooltipData.data)
|
|
1552
|
+
Object.entries(tooltipData.data)
|
|
1553
|
+
.filter(([_, values]) => Array.isArray(values) && !values.includes(undefined))
|
|
1554
|
+
.map((item, index) => <TooltipListItem item={item} key={index} />)}
|
|
1555
1555
|
</ul>
|
|
1556
1556
|
</TooltipWithBounds>
|
|
1557
1557
|
</>
|
|
@@ -210,7 +210,7 @@ const BrushHandle = props => {
|
|
|
210
210
|
const transform = isLeft ? 'scale(-1, 1)' : 'translate(0,0)'
|
|
211
211
|
const textAnchor = isLeft ? 'end' : 'start'
|
|
212
212
|
const tooltipText = isLeft ? ` Drag edges to focus on a specific segment ` : ''
|
|
213
|
-
const textWidth = getTextWidth(tooltipText,
|
|
213
|
+
const textWidth = getTextWidth(tooltipText, `${appFontSize / 1.1}px`)
|
|
214
214
|
|
|
215
215
|
return (
|
|
216
216
|
<>
|
|
@@ -9,7 +9,6 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
|
|
|
9
9
|
const allPalettes: Record<string, string[]> = { ...colorPalettes, ...twoColorPalette }
|
|
10
10
|
let palette = config.customColors || allPalettes[configPalette]
|
|
11
11
|
let numberOfKeys = config.runtime.seriesKeys.length
|
|
12
|
-
let newColorScale
|
|
13
12
|
|
|
14
13
|
while (numberOfKeys > palette.length) {
|
|
15
14
|
palette = palette.concat(palette)
|
|
@@ -17,12 +16,9 @@ export const getColorScale = (config: ChartConfig): ((value: string) => string)
|
|
|
17
16
|
|
|
18
17
|
palette = palette.slice(0, numberOfKeys)
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
return newColorScale
|
|
19
|
+
return scaleOrdinal({
|
|
20
|
+
domain: config.runtime.seriesLabelsAll,
|
|
21
|
+
range: palette,
|
|
22
|
+
unknown: null
|
|
23
|
+
})
|
|
28
24
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import _ from 'lodash'
|
|
2
|
+
|
|
3
|
+
export const isConvertLineToBarGraph = (configObj, filteredData) => {
|
|
4
|
+
const { allowLineToBarGraph, formattedData, series, visualizationType, xAxis } = configObj
|
|
5
|
+
if (!allowLineToBarGraph) return false
|
|
6
|
+
const lineWithLessThanThreePoints = visualizationType === 'Line' && filteredData?.length < 3
|
|
7
|
+
const isDynamicSeries = series?.some(series => series.dynamicCategory)
|
|
8
|
+
const isDynamicWithLessThanThreePoints =
|
|
9
|
+
isDynamicSeries && _.uniq(formattedData?.map(data => data[xAxis.dataKey])).length <= 2
|
|
10
|
+
return lineWithLessThanThreePoints || isDynamicWithLessThanThreePoints
|
|
4
11
|
}
|
package/src/hooks/useBarChart.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React, { useContext, useEffect, useState } from 'react'
|
|
2
|
-
import ConfigContext from '../ConfigContext'
|
|
2
|
+
import ConfigContext, { ChartDispatchContext } from '../ConfigContext'
|
|
3
3
|
import { formatNumber as formatColNumber } from '@cdc/core/helpers/cove/number'
|
|
4
4
|
import { appFontSize } from '@cdc/core/helpers/cove/fontSettings'
|
|
5
5
|
export const useBarChart = () => {
|
|
6
6
|
const { config, colorPalettes, tableData, updateConfig, parseDate, formatDate, setSeriesHighlight, seriesHighlight } =
|
|
7
7
|
useContext(ConfigContext)
|
|
8
|
+
const dispatch = useContext(ChartDispatchContext)
|
|
8
9
|
const { orientation } = config
|
|
9
10
|
const [hoveredBar, setHoveredBar] = useState(null)
|
|
10
11
|
|
|
@@ -192,9 +193,11 @@ export const useBarChart = () => {
|
|
|
192
193
|
const columns = config.columns
|
|
193
194
|
const columnsWithTooltips = []
|
|
194
195
|
let additionalTooltipItems = ''
|
|
196
|
+
const dynamicCategorySeries = config.runtime?.series?.find(series => series?.dynamicCategory)
|
|
195
197
|
const closestVal =
|
|
196
198
|
tableData.find(d => {
|
|
197
|
-
|
|
199
|
+
const dynamicCategoryMatch = dynamicCategorySeries ? d[dynamicCategorySeries.dynamicCategory] === series : true
|
|
200
|
+
return d[config.xAxis.dataKey] === xAxisDataValue && dynamicCategoryMatch
|
|
198
201
|
}) || {}
|
|
199
202
|
Object.keys(columns).forEach(colKeys => {
|
|
200
203
|
if (series && config.columns[colKeys].series && config.columns[colKeys].series !== series) return
|
|
@@ -223,11 +226,16 @@ export const useBarChart = () => {
|
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
const onMouseOverBar = (categoryValue, barKey) => {
|
|
226
|
-
if (config.legend.highlightOnHover && config.legend.behavior === 'highlight' && barKey)
|
|
229
|
+
if (config.legend.highlightOnHover && config.legend.behavior === 'highlight' && barKey) {
|
|
230
|
+
dispatch({ type: 'SET_SERIES_HIGHLIGHT', payload: [barKey] })
|
|
231
|
+
}
|
|
232
|
+
|
|
227
233
|
setHoveredBar(categoryValue)
|
|
228
234
|
}
|
|
229
235
|
const onMouseLeaveBar = () => {
|
|
230
|
-
if (config.legend.highlightOnHover && config.legend.behavior === 'highlight')
|
|
236
|
+
if (config.legend.highlightOnHover && config.legend.behavior === 'highlight') {
|
|
237
|
+
dispatch({ type: 'SET_SERIES_HIGHLIGHT', payload: [] })
|
|
238
|
+
}
|
|
231
239
|
}
|
|
232
240
|
|
|
233
241
|
return {
|
package/src/hooks/useMinMax.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ChartConfig } from '../types/ChartConfig'
|
|
2
2
|
import _ from 'lodash'
|
|
3
|
-
import
|
|
3
|
+
import ConfigContext from '../ConfigContext'
|
|
4
|
+
import { useContext } from 'react'
|
|
4
5
|
|
|
5
6
|
type UseMinMaxProps = {
|
|
6
7
|
/** config - standard chart config */
|
|
@@ -27,14 +28,12 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
|
|
|
27
28
|
let leftMax = 0
|
|
28
29
|
let rightMax = 0
|
|
29
30
|
|
|
31
|
+
const { convertLineToBarGraph } = useContext(ConfigContext)
|
|
32
|
+
|
|
30
33
|
if (!data) {
|
|
31
34
|
return { min, max }
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
const checkLineToBarGraph = () => {
|
|
35
|
-
return isConvertLineToBarGraph(config.visualizationType, data, config.allowLineToBarGraph)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
37
|
const { visualizationType, series } = config
|
|
39
38
|
const { max: enteredMaxValue, min: enteredMinValue } = config.runtime.yAxis
|
|
40
39
|
const paddingAddedToAxis = config.yAxis.enablePadding ? 1 + config.yAxis.scalePadding / 100 : 1
|
|
@@ -136,14 +135,14 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
|
|
|
136
135
|
|
|
137
136
|
// this should not apply to bar charts if there is negative CI data
|
|
138
137
|
if (
|
|
139
|
-
(visualizationType === 'Bar' ||
|
|
138
|
+
(visualizationType === 'Bar' || convertLineToBarGraph || (visualizationType === 'Combo' && !isAllLine)) &&
|
|
140
139
|
min > 0
|
|
141
140
|
) {
|
|
142
141
|
min = 0
|
|
143
142
|
}
|
|
144
143
|
if (
|
|
145
144
|
(config.visualizationType === 'Bar' ||
|
|
146
|
-
|
|
145
|
+
convertLineToBarGraph ||
|
|
147
146
|
(config.visualizationType === 'Combo' && !isAllLine)) &&
|
|
148
147
|
min < 0
|
|
149
148
|
) {
|
|
@@ -167,7 +166,7 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
|
|
|
167
166
|
min = Number(enteredMinValue) && isMinValid ? Number(enteredMinValue) : 0
|
|
168
167
|
}
|
|
169
168
|
|
|
170
|
-
if (config.visualizationType === 'Line' && !
|
|
169
|
+
if (config.visualizationType === 'Line' && !convertLineToBarGraph) {
|
|
171
170
|
const isMinValid = isLogarithmicAxis
|
|
172
171
|
? Number(enteredMinValue) >= 0 && Number(enteredMinValue) < minValue
|
|
173
172
|
: Number(enteredMinValue) < minValue
|
package/src/hooks/useScales.ts
CHANGED
|
@@ -136,6 +136,16 @@ const useScales = (properties: useScaleProps) => {
|
|
|
136
136
|
})
|
|
137
137
|
xScale.type = scaleTypes.LINEAR
|
|
138
138
|
}
|
|
139
|
+
if (xAxis.type === 'categorical') {
|
|
140
|
+
// Map items to rounded numbers if numeric, skip formatting non-numeric strings.
|
|
141
|
+
const xAxisDataMappedRoundedItems = xAxisDataMapped.map(item => {
|
|
142
|
+
const strItem = String(item)
|
|
143
|
+
const parsed = parseFloat(strItem)
|
|
144
|
+
return !isNaN(parsed) ? Math.round(parsed).toString() : strItem
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
xScale = composeScaleBand(xAxisDataMappedRoundedItems, [0, xMax], 1 - config.barThickness)
|
|
148
|
+
}
|
|
139
149
|
}
|
|
140
150
|
|
|
141
151
|
// handle Box plot
|
package/src/hooks/useTooltip.tsx
CHANGED
|
@@ -175,11 +175,16 @@ export const useTooltip = props => {
|
|
|
175
175
|
?.flatMap(seriesKey => {
|
|
176
176
|
const value = resolvedScaleValues[0]?.[seriesKey]
|
|
177
177
|
const formattedValue = getFormattedValue(seriesKey, value, config, getAxisPosition)
|
|
178
|
+
const seriesObjWithName = config.runtime.series.find(
|
|
179
|
+
series => series.dataKey === seriesKey && series.name !== undefined
|
|
180
|
+
)
|
|
178
181
|
if (
|
|
179
182
|
(value === null || value === undefined || value === '' || formattedValue === 'N/A') &&
|
|
180
183
|
config.general.hideNullValue
|
|
181
184
|
) {
|
|
182
185
|
return []
|
|
186
|
+
} else if (seriesObjWithName && seriesObjWithName.name === '') {
|
|
187
|
+
return [['', formattedValue, getAxisPosition(seriesKey)]]
|
|
183
188
|
} else {
|
|
184
189
|
return [[seriesKey, formattedValue, getAxisPosition(seriesKey)]]
|
|
185
190
|
}
|
|
@@ -567,8 +572,14 @@ export const useTooltip = props => {
|
|
|
567
572
|
if (index == 1 && config.dataFormat.onlyShowTopPrefixSuffix) {
|
|
568
573
|
newValue = `${config.dataFormat.prefix}${newValue}${config.dataFormat.suffix}`
|
|
569
574
|
}
|
|
575
|
+
const activeLabel = getSeriesNameFromLabel(key)
|
|
576
|
+
const displayText = activeLabel ? `${activeLabel}: ${newValue}` : newValue
|
|
570
577
|
|
|
571
|
-
return
|
|
578
|
+
return (
|
|
579
|
+
<li style={style} className='tooltip-body'>
|
|
580
|
+
{displayText}
|
|
581
|
+
</li>
|
|
582
|
+
)
|
|
572
583
|
}
|
|
573
584
|
|
|
574
585
|
return {
|
package/src/scss/main.scss
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DimensionsType } from '@cdc/core/types/Dimensions'
|
|
2
|
+
import { ChartConfig } from '../types/ChartConfig'
|
|
3
|
+
|
|
4
|
+
type Action<T, P = undefined, R = undefined> = {
|
|
5
|
+
type: T
|
|
6
|
+
payload?: P
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Action Types
|
|
10
|
+
type SET_CONFIG = Action<'SET_CONFIG', ChartConfig>
|
|
11
|
+
type SET_LOADING = Action<'SET_LOADING', boolean>
|
|
12
|
+
type UPDATE_CONFIG = Action<'UPDATE_CONFIG', ChartConfig>
|
|
13
|
+
type SET_COLOR_SCALE = Action<'SET_COLOR_SCALE', Function>
|
|
14
|
+
type SET_STATE_DATA = Action<'SET_STATE_DATA', object[]>
|
|
15
|
+
type SET_EXCLUDED_DATA = Action<'SET_EXCLUDED_DATA', object[]>
|
|
16
|
+
type SET_FILTERED_DATA = Action<'SET_FILTERED_DATA', object[]>
|
|
17
|
+
type SET_SERIES_HIGHLIGHT = Action<'SET_SERIES_HIGHLIGHT', object>
|
|
18
|
+
type SET_VIEWPORT = Action<'SET_VIEWPORT', string>
|
|
19
|
+
type SET_DIMENSIONS = Action<'SET_DIMENSIONS', DimensionsType>
|
|
20
|
+
type SET_CONTAINER = Action<'SET_CONTAINER', object>
|
|
21
|
+
type SET_LOADED_EVENT = Action<'SET_LOADED_EVENT', boolean>
|
|
22
|
+
type SET_DRAG_ANNOTATIONS = Action<'SET_DRAG_ANNOTATIONS', boolean>
|
|
23
|
+
type SET_BRUSH_CONFIG = Action<'SET_BRUSH_CONFIG', object>
|
|
24
|
+
type ChartActions =
|
|
25
|
+
| SET_CONFIG
|
|
26
|
+
| UPDATE_CONFIG
|
|
27
|
+
| SET_COLOR_SCALE
|
|
28
|
+
| SET_STATE_DATA
|
|
29
|
+
| SET_EXCLUDED_DATA
|
|
30
|
+
| SET_FILTERED_DATA
|
|
31
|
+
| SET_SERIES_HIGHLIGHT
|
|
32
|
+
| SET_VIEWPORT
|
|
33
|
+
| SET_DIMENSIONS
|
|
34
|
+
| SET_CONTAINER
|
|
35
|
+
| SET_LOADED_EVENT
|
|
36
|
+
| SET_DRAG_ANNOTATIONS
|
|
37
|
+
| SET_BRUSH_CONFIG
|
|
38
|
+
| SET_LOADING
|
|
39
|
+
|
|
40
|
+
export default ChartActions
|