@cdc/chart 4.25.10 → 4.25.11
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-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
- package/dist/cdcchart.js +36258 -34658
- package/examples/feature/__data__/planet-example-data.json +1 -1
- package/examples/feature/boxplot/valid-boxplot.csv +38 -17
- package/examples/private/DEV-11825.json +573 -0
- package/examples/private/na.json +913 -0
- package/examples/private/test-data.csv +28 -0
- package/index.html +2 -121
- package/package.json +4 -4
- package/src/CdcChart.tsx +8 -11
- package/src/CdcChartComponent.tsx +256 -87
- package/src/_stories/Chart.Combo.stories.tsx +18 -0
- package/src/_stories/Chart.Forecast.stories.tsx +36 -0
- package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
- package/src/_stories/Chart.Patterns.stories.tsx +2 -1
- package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
- package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
- package/src/_stories/ChartAnnotation.stories.tsx +6 -3
- package/src/_stories/ChartBar.Editor.stories.tsx +3580 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +658 -0
- package/src/_stories/ChartEditor.stories.tsx +1 -2
- package/src/_stories/_mock/combo.json +451 -0
- package/src/_stories/_mock/editor-test-configs.json +376 -0
- package/src/_stories/_mock/editor-test-datasets.json +477 -0
- package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
- package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
- package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
- package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
- package/src/_stories/_mock/pie_config.json +257 -62
- package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
- package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
- package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
- package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
- package/src/components/Annotations/components/findNearestDatum.ts +6 -41
- package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -6
- package/src/components/AreaChart/index.tsx +1 -2
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -4
- package/src/components/BarChart/components/BarChart.Vertical.tsx +3 -2
- package/src/components/BoxPlot/helpers/index.ts +3 -3
- package/src/components/Brush/BrushChart.tsx +1 -1
- package/src/components/EditorPanel/EditorPanel.tsx +199 -190
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +19 -1
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +102 -55
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +422 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +75 -21
- package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
- package/src/components/EditorPanel/editor-panel.scss +0 -20
- package/src/components/EditorPanel/useEditorPermissions.ts +7 -15
- package/src/components/Forecasting/Forecasting.tsx +139 -21
- package/src/components/Legend/Legend.Component.tsx +16 -9
- package/src/components/Legend/helpers/createFormatLabels.tsx +181 -181
- package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
- package/src/components/LineChart/LineChartProps.ts +0 -3
- package/src/components/LineChart/helpers.ts +1 -1
- package/src/components/LineChart/index.tsx +36 -13
- package/src/components/LinearChart.tsx +75 -80
- package/src/components/Regions/components/Regions.tsx +3 -24
- package/src/components/Sankey/types/index.ts +1 -1
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +198 -0
- package/src/components/SmallMultiples/SmallMultiples.css +32 -0
- package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
- package/src/components/SmallMultiples/index.ts +2 -0
- package/src/data/initial-state.js +13 -1
- package/src/helpers/buildForecastPaletteOptions.ts +0 -38
- package/src/helpers/getColorScale.ts +10 -0
- package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +14 -7
- package/src/helpers/getYAxisAutoPadding.ts +53 -0
- package/src/helpers/smallMultiplesHelpers.ts +529 -0
- package/src/hooks/useProgrammaticTooltip.ts +96 -0
- package/src/hooks/useScales.ts +88 -34
- package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
- package/src/hooks/useTooltip.tsx +60 -15
- package/src/scss/main.scss +1 -80
- package/src/store/chart.actions.ts +2 -0
- package/src/store/chart.reducer.ts +4 -0
- package/src/types/ChartConfig.ts +24 -6
- package/src/types/ChartContext.ts +3 -0
- package/src/_stories/_mock/pie_data.json +0 -218
- package/src/components/AreaChart/components/AreaChart.jsx +0 -109
- package/src/helpers/sort.ts +0 -7
- package/src/hooks/useActiveElement.js +0 -19
- package/src/hooks/useChartClasses.js +0 -41
package/src/hooks/useScales.ts
CHANGED
|
@@ -13,6 +13,9 @@ import ConfigContext from '../ConfigContext'
|
|
|
13
13
|
import { ChartConfig } from '../types/ChartConfig'
|
|
14
14
|
import { ChartContext } from '../types/ChartContext'
|
|
15
15
|
import _ from 'lodash'
|
|
16
|
+
import { getYAxisAutoPadding } from '../helpers/getYAxisAutoPadding'
|
|
17
|
+
import getMinMax from '../helpers/getMinMax'
|
|
18
|
+
import { countNumOfTicks } from '../helpers/countNumOfTicks'
|
|
16
19
|
|
|
17
20
|
const scaleTypes = {
|
|
18
21
|
TIME: 'time',
|
|
@@ -27,23 +30,84 @@ export const TOP_PADDING = 10
|
|
|
27
30
|
type useScaleProps = {
|
|
28
31
|
config: ChartConfig // standard chart config
|
|
29
32
|
data: Object[] // standard data array
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
tableData: Object[] // table data for getMinMax
|
|
34
|
+
minValue: number // raw minimum value from data
|
|
35
|
+
maxValue: number // raw maximum value from data
|
|
36
|
+
existPositiveValue: boolean // whether data contains positive values
|
|
37
|
+
isAllLine: boolean // whether all series are line type
|
|
32
38
|
xAxisDataMapped: Object[] // array of x axis date/category items
|
|
33
39
|
xMax: number // chart svg width
|
|
34
40
|
yMax: number // chart svg height
|
|
41
|
+
needsYAxisAutoPadding?: boolean // whether Y-axis needs auto padding for label overflow
|
|
42
|
+
currentViewport?: string // current viewport for tick calculation
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
const useScales = (properties: useScaleProps) => {
|
|
38
|
-
let {
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
let {
|
|
47
|
+
xAxisDataMapped,
|
|
48
|
+
xMax,
|
|
49
|
+
yMax,
|
|
50
|
+
config,
|
|
51
|
+
data,
|
|
52
|
+
tableData,
|
|
53
|
+
minValue,
|
|
54
|
+
maxValue,
|
|
55
|
+
existPositiveValue,
|
|
56
|
+
isAllLine,
|
|
57
|
+
needsYAxisAutoPadding,
|
|
58
|
+
currentViewport
|
|
59
|
+
} = properties
|
|
60
|
+
|
|
61
|
+
const context = useContext<ChartContext>(ConfigContext)
|
|
62
|
+
const { rawData, dimensions, convertLineToBarGraph = false } = context
|
|
41
63
|
|
|
42
64
|
const [screenWidth] = dimensions
|
|
65
|
+
const isHorizontal = config.orientation === 'horizontal'
|
|
66
|
+
const { visualizationType, xAxis, forestPlot, runtime } = config
|
|
67
|
+
const isForestPlot = visualizationType === 'Forest Plot'
|
|
68
|
+
|
|
69
|
+
const minMaxProps = {
|
|
70
|
+
config,
|
|
71
|
+
minValue,
|
|
72
|
+
maxValue,
|
|
73
|
+
existPositiveValue,
|
|
74
|
+
data,
|
|
75
|
+
isAllLine,
|
|
76
|
+
tableData,
|
|
77
|
+
convertLineToBarGraph
|
|
78
|
+
}
|
|
79
|
+
let { min, max, leftMax, rightMax } = getMinMax(minMaxProps)
|
|
80
|
+
|
|
81
|
+
const yTickCount = countNumOfTicks({
|
|
82
|
+
axis: 'yAxis',
|
|
83
|
+
max,
|
|
84
|
+
runtime,
|
|
85
|
+
currentViewport,
|
|
86
|
+
isHorizontal,
|
|
87
|
+
data,
|
|
88
|
+
config,
|
|
89
|
+
min
|
|
90
|
+
})
|
|
91
|
+
const handleNumTicks = isForestPlot ? config.data.length : yTickCount
|
|
92
|
+
|
|
93
|
+
// Apply auto-padding if needed
|
|
94
|
+
if (needsYAxisAutoPadding && !isHorizontal) {
|
|
95
|
+
for (let i = 0; i < 3; i++) {
|
|
96
|
+
const scale = composeYScale({ min, max, yMax, config, leftMax })
|
|
97
|
+
const padding = getYAxisAutoPadding(scale, handleNumTicks, maxValue, minValue, config)
|
|
98
|
+
if (i === 0 || padding > 0) {
|
|
99
|
+
const adjustedConfig = { ...config, yAxis: { ...config.yAxis, scalePadding: padding, enablePadding: true } }
|
|
100
|
+
const result = getMinMax({ ...minMaxProps, config: adjustedConfig })
|
|
101
|
+
min = result.min
|
|
102
|
+
max = result.max
|
|
103
|
+
leftMax = result.leftMax
|
|
104
|
+
rightMax = result.rightMax
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
43
109
|
const seriesDomain = config.runtime.barSeriesKeys || config.runtime.seriesKeys
|
|
44
110
|
const xAxisType = config.runtime.xAxis.type
|
|
45
|
-
const isHorizontal = config.orientation === 'horizontal'
|
|
46
|
-
const { visualizationType, xAxis, forestPlot } = config
|
|
47
111
|
const paddingRange = ['Area Chart', 'Forecasting'].includes(config.visualizationType) ? 1 : 1 - config.barThickness
|
|
48
112
|
// define scales
|
|
49
113
|
let xScale = null
|
|
@@ -59,7 +123,7 @@ const useScales = (properties: useScaleProps) => {
|
|
|
59
123
|
|
|
60
124
|
// handle Horizontal bars
|
|
61
125
|
if (isHorizontal) {
|
|
62
|
-
xScale = composeXScale({ min: min * 1.03,
|
|
126
|
+
xScale = composeXScale({ min: min * 1.03, max, xMax, config })
|
|
63
127
|
xScale.type = config.yAxis.type === 'logarithmic' ? scaleTypes.LOG : scaleTypes.LINEAR
|
|
64
128
|
yScale = getYScaleFunction(xAxisType, xAxisDataMapped)
|
|
65
129
|
yScale.rangeRound([0, yMax])
|
|
@@ -69,7 +133,7 @@ const useScales = (properties: useScaleProps) => {
|
|
|
69
133
|
// handle Vertical bars
|
|
70
134
|
if (!isHorizontal) {
|
|
71
135
|
xScale = composeScaleBand(xAxisDataMapped, [0, xMax], paddingRange)
|
|
72
|
-
yScale = composeYScale(
|
|
136
|
+
yScale = composeYScale({ min, max, yMax, config, leftMax })
|
|
73
137
|
seriesScale = composeScaleBand(seriesDomain, [0, xScale.bandwidth()], 0)
|
|
74
138
|
}
|
|
75
139
|
|
|
@@ -290,17 +354,29 @@ const useScales = (properties: useScaleProps) => {
|
|
|
290
354
|
}
|
|
291
355
|
}
|
|
292
356
|
}
|
|
293
|
-
return {
|
|
357
|
+
return {
|
|
358
|
+
xScale,
|
|
359
|
+
yScale,
|
|
360
|
+
seriesScale,
|
|
361
|
+
g1xScale,
|
|
362
|
+
g2xScale,
|
|
363
|
+
xScaleNoPadding,
|
|
364
|
+
xScaleAnnotation,
|
|
365
|
+
min,
|
|
366
|
+
max,
|
|
367
|
+
leftMax,
|
|
368
|
+
rightMax
|
|
369
|
+
}
|
|
294
370
|
}
|
|
295
371
|
|
|
296
372
|
export default useScales
|
|
297
373
|
|
|
298
|
-
|
|
374
|
+
const getFirstDayOfMonth = ms => {
|
|
299
375
|
const date = new Date(ms)
|
|
300
376
|
return new Date(date.getFullYear(), date.getMonth(), 1).getTime()
|
|
301
377
|
}
|
|
302
378
|
|
|
303
|
-
|
|
379
|
+
const dateFormatHasMonthButNoDays = dateFormat => {
|
|
304
380
|
return (
|
|
305
381
|
(dateFormat.includes('%b') ||
|
|
306
382
|
dateFormat.includes('%B') ||
|
|
@@ -352,28 +428,6 @@ export const getTickValues = (xAxisDataMapped, xScale, num, config) => {
|
|
|
352
428
|
}
|
|
353
429
|
}
|
|
354
430
|
|
|
355
|
-
// Ensure that the last tick is shown for charts with a "Date (Linear Scale)" scale
|
|
356
|
-
export const filterAndShiftLinearDateTicks = (config, axisProps, xAxisDataMapped, formatDate) => {
|
|
357
|
-
let ticks = axisProps.ticks
|
|
358
|
-
const filteredTickValues = getTicks(axisProps.scale, axisProps.numTicks)
|
|
359
|
-
if (filteredTickValues.length < xAxisDataMapped.length) {
|
|
360
|
-
let shift = 0
|
|
361
|
-
const lastIdx = xAxisDataMapped.indexOf(filteredTickValues[filteredTickValues.length - 1])
|
|
362
|
-
if (lastIdx < xAxisDataMapped.length - 1) {
|
|
363
|
-
shift = !config.xAxis.sortByRecentDate
|
|
364
|
-
? xAxisDataMapped.length - 1 - lastIdx
|
|
365
|
-
: xAxisDataMapped.indexOf(filteredTickValues[0]) * -1
|
|
366
|
-
}
|
|
367
|
-
ticks = filteredTickValues.map(value => {
|
|
368
|
-
return axisProps.ticks[axisProps.ticks.findIndex(tick => tick.value === value) + shift]
|
|
369
|
-
})
|
|
370
|
-
}
|
|
371
|
-
ticks.forEach((tick, i) => {
|
|
372
|
-
tick.formattedValue = formatDate(tick.value, i, ticks)
|
|
373
|
-
})
|
|
374
|
-
return ticks
|
|
375
|
-
}
|
|
376
|
-
|
|
377
431
|
/// helper functions
|
|
378
432
|
const composeXScale = ({ min, max, xMax, config }) => {
|
|
379
433
|
// Adjust min value if using logarithmic scale
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import ConfigContext from '../ConfigContext'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom hook to handle synchronized tooltips in small multiples.
|
|
6
|
+
* This hook provides mouse event handlers that coordinate tooltip display across multiple chart tiles.
|
|
7
|
+
*
|
|
8
|
+
* @param xMax - The maximum x coordinate of the chart area
|
|
9
|
+
* @param yMax - The maximum y coordinate of the chart area
|
|
10
|
+
* @param getXValueFromCoordinate - Function to convert pixel x-coordinate to data value
|
|
11
|
+
* @returns Object with onMouseMove and onMouseLeave handlers, or null if not in small multiples
|
|
12
|
+
*/
|
|
13
|
+
export const useSmallMultipleSynchronization = (
|
|
14
|
+
xMax: number,
|
|
15
|
+
yMax: number,
|
|
16
|
+
getXValueFromCoordinate: (x: number) => any
|
|
17
|
+
) => {
|
|
18
|
+
const { config, handleSmallMultipleHover } = useContext(ConfigContext)
|
|
19
|
+
|
|
20
|
+
// If not in small multiples mode, return null handlers
|
|
21
|
+
if (!handleSmallMultipleHover) {
|
|
22
|
+
return {
|
|
23
|
+
onMouseMove: null,
|
|
24
|
+
onMouseLeave: null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const yAxisSize = Number(config.yAxis.size || 0)
|
|
29
|
+
|
|
30
|
+
const onMouseMove = (event: any) => {
|
|
31
|
+
const svgRect = event.currentTarget.getBoundingClientRect()
|
|
32
|
+
const x = event.clientX - svgRect.left
|
|
33
|
+
const y = event.clientY - svgRect.top
|
|
34
|
+
|
|
35
|
+
// Only trigger synchronized tooltips when mouse is over the valid chart area
|
|
36
|
+
// (to the right of the Y-axis and within chart bounds)
|
|
37
|
+
const isOverChartArea = x >= yAxisSize && x <= yAxisSize + xMax && y >= 0 && y <= yMax
|
|
38
|
+
|
|
39
|
+
if (isOverChartArea) {
|
|
40
|
+
const xAxisValue = getXValueFromCoordinate(x - yAxisSize)
|
|
41
|
+
if (xAxisValue !== null && xAxisValue !== undefined) {
|
|
42
|
+
handleSmallMultipleHover(xAxisValue, y)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If we're not over a valid area or couldn't get a value, hide synchronized tooltips
|
|
48
|
+
handleSmallMultipleHover(null, null)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const onMouseLeave = () => {
|
|
52
|
+
handleSmallMultipleHover(null, null)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
onMouseMove,
|
|
57
|
+
onMouseLeave
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/hooks/useTooltip.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useContext, useRef } from 'react'
|
|
1
|
+
import { useContext, useRef, useLayoutEffect } from 'react'
|
|
2
2
|
// Local imports
|
|
3
3
|
import parse from 'html-react-parser'
|
|
4
4
|
import ConfigContext from '../ConfigContext'
|
|
@@ -28,6 +28,14 @@ export const useTooltip = props => {
|
|
|
28
28
|
const { xScale, yScale, seriesScale, showTooltip, hideTooltip, interactionLabel = '' } = props
|
|
29
29
|
const { xAxis, visualizationType, orientation, yAxis, runtime } = config
|
|
30
30
|
|
|
31
|
+
// Track the latest xScale in a ref to prevent stale closures
|
|
32
|
+
const xScaleRef = useRef(xScale)
|
|
33
|
+
|
|
34
|
+
// Update ref whenever xScale prop changes
|
|
35
|
+
useLayoutEffect(() => {
|
|
36
|
+
xScaleRef.current = xScale
|
|
37
|
+
}, [xScale])
|
|
38
|
+
|
|
31
39
|
const Y_AXIS_SIZE = Number(config.yAxis.size || 0)
|
|
32
40
|
|
|
33
41
|
// function handles only Single series hovered data tooltips
|
|
@@ -289,15 +297,15 @@ export const useTooltip = props => {
|
|
|
289
297
|
*/
|
|
290
298
|
const getXValueFromCoordinateDate = x => {
|
|
291
299
|
if (config.xAxis.type === 'categorical' || config.visualizationType === 'Combo') {
|
|
292
|
-
let eachBand =
|
|
300
|
+
let eachBand = xScaleRef.current.step()
|
|
293
301
|
let numerator = x
|
|
294
302
|
const index = Math.floor(Number(numerator) / eachBand)
|
|
295
|
-
return
|
|
303
|
+
return xScaleRef.current.domain()[index - 1] // fixes off by 1 error
|
|
296
304
|
}
|
|
297
305
|
|
|
298
306
|
if (isDateScale(config.xAxis) && config.visualizationType !== 'Combo') {
|
|
299
307
|
const bisectDate = bisector(d => parseDate(d[config.xAxis.dataKey])).left
|
|
300
|
-
const x0 =
|
|
308
|
+
const x0 = xScaleRef.current.invert(xScaleRef.current(x))
|
|
301
309
|
const index = bisectDate(config.data, x0, 1)
|
|
302
310
|
const val = parseDate(config.data[index - 1][config.xAxis.dataKey])
|
|
303
311
|
return val
|
|
@@ -309,12 +317,12 @@ export const useTooltip = props => {
|
|
|
309
317
|
* @function getXValueFromCoordinate
|
|
310
318
|
* @returns {String} - the closest x value to the cursor position
|
|
311
319
|
*/
|
|
312
|
-
const getXValueFromCoordinate =
|
|
320
|
+
const getXValueFromCoordinate = x => {
|
|
313
321
|
if (visualizationType === 'Pie') return
|
|
314
322
|
if (orientation === 'horizontal') return
|
|
315
323
|
|
|
316
324
|
// Check the type of x equal to point or if the type of xAxis is equal to continuous or date
|
|
317
|
-
if (
|
|
325
|
+
if (xScaleRef.current.type === 'point' || xAxis.type === 'continuous' || isDateScale(xAxis)) {
|
|
318
326
|
// Find the closest x value by calculating the minimum distance
|
|
319
327
|
let closestX = null
|
|
320
328
|
let minDistance = Number.MAX_VALUE
|
|
@@ -322,9 +330,11 @@ export const useTooltip = props => {
|
|
|
322
330
|
|
|
323
331
|
const barThicknessOffset = config.xAxis.type === 'date' ? xScale.bandwidth() / 2 : 0
|
|
324
332
|
data.forEach(d => {
|
|
325
|
-
const xPosition = isDateScale(xAxis)
|
|
333
|
+
const xPosition = isDateScale(xAxis)
|
|
334
|
+
? xScaleRef.current(parseDate(d[xAxis.dataKey]))
|
|
335
|
+
: xScaleRef.current(d[xAxis.dataKey])
|
|
326
336
|
let bwOffset = config.barHeight
|
|
327
|
-
const distance = Math.abs(Number(xPosition + barThicknessOffset - offset
|
|
337
|
+
const distance = Math.abs(Number(xPosition + barThicknessOffset - offset))
|
|
328
338
|
|
|
329
339
|
if (distance <= minDistance) {
|
|
330
340
|
minDistance = distance
|
|
@@ -334,16 +344,50 @@ export const useTooltip = props => {
|
|
|
334
344
|
return closestX
|
|
335
345
|
}
|
|
336
346
|
|
|
347
|
+
// For band scales, find which band the mouse x-coordinate falls within
|
|
337
348
|
if (config.xAxis.type === 'categorical' || visualizationType === 'Combo') {
|
|
338
|
-
|
|
339
|
-
|
|
349
|
+
const domain = xScaleRef.current.domain()
|
|
350
|
+
const bandwidth = xScaleRef.current.bandwidth()
|
|
340
351
|
|
|
341
|
-
let
|
|
342
|
-
|
|
343
|
-
|
|
352
|
+
let closestValue = null
|
|
353
|
+
let minDistance = Number.MAX_VALUE
|
|
354
|
+
|
|
355
|
+
domain.forEach(value => {
|
|
356
|
+
const bandStart = xScaleRef.current(value)
|
|
357
|
+
const bandCenter = bandStart + bandwidth / 2
|
|
358
|
+
const distance = Math.abs(x - bandCenter)
|
|
359
|
+
|
|
360
|
+
if (distance < minDistance) {
|
|
361
|
+
minDistance = distance
|
|
362
|
+
closestValue = value
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
return closestValue
|
|
344
367
|
}
|
|
345
368
|
}
|
|
346
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Helper for converting data value to pixel coordinate (inverse of getXValueFromCoordinate)
|
|
372
|
+
* @function getCoordinateFromXValue
|
|
373
|
+
* @param {any} xAxisValue - X-axis data value (date, number, or category)
|
|
374
|
+
* @returns {number} - pixel coordinate for the data value
|
|
375
|
+
*/
|
|
376
|
+
const getCoordinateFromXValue = xAxisValue => {
|
|
377
|
+
if (visualizationType === 'Pie') return 0
|
|
378
|
+
if (orientation === 'horizontal') return 0
|
|
379
|
+
|
|
380
|
+
// Convert data value to pixel coordinate using current xScale
|
|
381
|
+
let pixelX = isDateScale(xAxis) ? xScaleRef.current(parseDate(xAxisValue)) : xScaleRef.current(xAxisValue)
|
|
382
|
+
|
|
383
|
+
// For band scales (bar charts, categorical axes), add bandwidth offset to point to center of bar
|
|
384
|
+
if (xScaleRef.current.bandwidth) {
|
|
385
|
+
pixelX += xScaleRef.current.bandwidth() / 2
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return pixelX
|
|
389
|
+
}
|
|
390
|
+
|
|
347
391
|
const findClosest = (dataArray: [any, number][], mouseXorY) => {
|
|
348
392
|
let dataColumn: Object
|
|
349
393
|
dataArray.find(([d, xOrY]) => {
|
|
@@ -431,7 +475,7 @@ export const useTooltip = props => {
|
|
|
431
475
|
const eventSvgCoords = localPoint(e)
|
|
432
476
|
const { x } = eventSvgCoords
|
|
433
477
|
if (!x) throw new Error('COVE: no x value in handleTooltipClick.')
|
|
434
|
-
let closestXScaleValue = getXValueFromCoordinate(x
|
|
478
|
+
let closestXScaleValue = getXValueFromCoordinate(x)
|
|
435
479
|
let datum = config.data?.filter(item => item[config.xAxis.dataKey] === closestXScaleValue)
|
|
436
480
|
if (!closestXScaleValue) throw new Error('COVE: no closest x scale value in handleTooltipClick')
|
|
437
481
|
if (isDateScale(xAxis) && closestXScaleValue) {
|
|
@@ -502,7 +546,7 @@ export const useTooltip = props => {
|
|
|
502
546
|
const dataWithXScale = dataToSearch.map(
|
|
503
547
|
d => [d, seriesScale(d[dynamicSeries.dynamicCategory])] as [Object, number]
|
|
504
548
|
)
|
|
505
|
-
const xOffset = x - Y_AXIS_SIZE -
|
|
549
|
+
const xOffset = x - Y_AXIS_SIZE - xScaleRef.current(closestXScaleValue)
|
|
506
550
|
dataToSearch = [findClosest(dataWithXScale, xOffset)]
|
|
507
551
|
}
|
|
508
552
|
}
|
|
@@ -645,6 +689,7 @@ export const useTooltip = props => {
|
|
|
645
689
|
getIncludedTooltipSeries,
|
|
646
690
|
getXValueFromCoordinate,
|
|
647
691
|
getXValueFromCoordinateDate,
|
|
692
|
+
getCoordinateFromXValue,
|
|
648
693
|
handleTooltipClick,
|
|
649
694
|
handleTooltipMouseOff,
|
|
650
695
|
handleTooltipMouseOver,
|
package/src/scss/main.scss
CHANGED
|
@@ -1,52 +1,4 @@
|
|
|
1
|
-
@import '@cdc/core/styles/
|
|
2
|
-
|
|
3
|
-
@mixin breakpoint($class) {
|
|
4
|
-
@if $class == xs {
|
|
5
|
-
@media (max-width: 767px) {
|
|
6
|
-
@content;
|
|
7
|
-
}
|
|
8
|
-
} @else if $class == sm {
|
|
9
|
-
@media (min-width: 768px) {
|
|
10
|
-
@content;
|
|
11
|
-
}
|
|
12
|
-
} @else if $class == md {
|
|
13
|
-
@media (min-width: 960px) {
|
|
14
|
-
@content;
|
|
15
|
-
}
|
|
16
|
-
} @else if $class == lg {
|
|
17
|
-
@media (min-width: 1300px) {
|
|
18
|
-
@content;
|
|
19
|
-
}
|
|
20
|
-
} @else {
|
|
21
|
-
@warn "Breakpoint mixin supports: xs, sm, md, lg";
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@mixin breakpointClass($class) {
|
|
26
|
-
@if $class == xs {
|
|
27
|
-
&.xs,
|
|
28
|
-
&.xxs {
|
|
29
|
-
@content;
|
|
30
|
-
}
|
|
31
|
-
} @else if $class == sm {
|
|
32
|
-
&.sm,
|
|
33
|
-
&.md,
|
|
34
|
-
&.lg {
|
|
35
|
-
@content;
|
|
36
|
-
}
|
|
37
|
-
} @else if $class == md {
|
|
38
|
-
&.md,
|
|
39
|
-
&.lg {
|
|
40
|
-
@content;
|
|
41
|
-
}
|
|
42
|
-
} @else if $class == lg {
|
|
43
|
-
&.lg {
|
|
44
|
-
@content;
|
|
45
|
-
}
|
|
46
|
-
} @else {
|
|
47
|
-
@warn "Breakpoint Class mixin supports: xs, sm, md, lg";
|
|
48
|
-
}
|
|
49
|
-
}
|
|
1
|
+
@import '@cdc/core/styles/v2/utils/breakpoints';
|
|
50
2
|
|
|
51
3
|
.form-container {
|
|
52
4
|
overflow-y: auto;
|
|
@@ -91,37 +43,6 @@
|
|
|
91
43
|
|
|
92
44
|
border-radius: 3px;
|
|
93
45
|
|
|
94
|
-
.checkbox-group {
|
|
95
|
-
padding: 16px;
|
|
96
|
-
border: 1px solid #c4c4c4;
|
|
97
|
-
border-radius: 8px;
|
|
98
|
-
margin-top: 8px;
|
|
99
|
-
margin-bottom: 24px;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.loader {
|
|
103
|
-
width: 100%;
|
|
104
|
-
text-align: center;
|
|
105
|
-
display: inline-block;
|
|
106
|
-
animation: spin 1s linear infinite;
|
|
107
|
-
|
|
108
|
-
&::before {
|
|
109
|
-
content: '\21BB';
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.warning-icon {
|
|
114
|
-
position: relative;
|
|
115
|
-
top: 2px;
|
|
116
|
-
width: 15px;
|
|
117
|
-
height: 15px;
|
|
118
|
-
margin-left: 5px;
|
|
119
|
-
|
|
120
|
-
path {
|
|
121
|
-
fill: #d8000c;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
46
|
.chart-description {
|
|
126
47
|
margin-bottom: 20px;
|
|
127
48
|
}
|
|
@@ -12,6 +12,7 @@ type SET_EXCLUDED_DATA = Action<'SET_EXCLUDED_DATA', object[]>
|
|
|
12
12
|
type SET_FILTERED_DATA = Action<'SET_FILTERED_DATA', object[]>
|
|
13
13
|
type SET_SERIES_HIGHLIGHT = Action<'SET_SERIES_HIGHLIGHT', string[]>
|
|
14
14
|
type SET_VIEWPORT = Action<'SET_VIEWPORT', string>
|
|
15
|
+
type SET_VIZ_VIEWPORT = Action<'SET_VIZ_VIEWPORT', string>
|
|
15
16
|
type SET_DIMENSIONS = Action<'SET_DIMENSIONS', DimensionsType>
|
|
16
17
|
type SET_CONTAINER = Action<'SET_CONTAINER', object>
|
|
17
18
|
type SET_LOADED_EVENT = Action<'SET_LOADED_EVENT', boolean>
|
|
@@ -26,6 +27,7 @@ type ChartActions =
|
|
|
26
27
|
| SET_FILTERED_DATA
|
|
27
28
|
| SET_SERIES_HIGHLIGHT
|
|
28
29
|
| SET_VIEWPORT
|
|
30
|
+
| SET_VIZ_VIEWPORT
|
|
29
31
|
| SET_DIMENSIONS
|
|
30
32
|
| SET_CONTAINER
|
|
31
33
|
| SET_LOADED_EVENT
|
|
@@ -13,6 +13,7 @@ type ChartState = {
|
|
|
13
13
|
filteredData: object[]
|
|
14
14
|
seriesHighlight: string[]
|
|
15
15
|
currentViewport: ViewportSize
|
|
16
|
+
vizViewport: ViewportSize
|
|
16
17
|
dimensions: DimensionsType
|
|
17
18
|
container: HTMLElement | null
|
|
18
19
|
coveLoadedEventRan: boolean
|
|
@@ -32,6 +33,7 @@ export const getInitialState = (configObj: ChartConfig): ChartState => {
|
|
|
32
33
|
seriesHighlight:
|
|
33
34
|
configObj && configObj?.legend?.seriesHighlight?.length ? [...configObj?.legend?.seriesHighlight] : [],
|
|
34
35
|
currentViewport: 'lg',
|
|
36
|
+
vizViewport: 'lg',
|
|
35
37
|
dimensions: [0, 0],
|
|
36
38
|
container: null,
|
|
37
39
|
coveLoadedEventRan: false,
|
|
@@ -61,6 +63,8 @@ export const reducer = (state: ChartState, action: ChartActions): ChartState =>
|
|
|
61
63
|
return { ...state, seriesHighlight: action.payload }
|
|
62
64
|
case 'SET_VIEWPORT':
|
|
63
65
|
return { ...state, currentViewport: action.payload }
|
|
66
|
+
case 'SET_VIZ_VIEWPORT':
|
|
67
|
+
return { ...state, vizViewport: action.payload }
|
|
64
68
|
case 'SET_DIMENSIONS':
|
|
65
69
|
return { ...state, dimensions: action.payload }
|
|
66
70
|
case 'SET_CONTAINER':
|
package/src/types/ChartConfig.ts
CHANGED
|
@@ -10,12 +10,13 @@ import { BoxPlot } from '@cdc/core/types/BoxPlot'
|
|
|
10
10
|
import { General as CoreGeneral } from '@cdc/core/types/General'
|
|
11
11
|
|
|
12
12
|
// Extend the core General type to include palette information for charts
|
|
13
|
-
|
|
13
|
+
type General = CoreGeneral & {
|
|
14
14
|
palette?: {
|
|
15
15
|
name?: string
|
|
16
16
|
version?: string
|
|
17
17
|
isReversed?: boolean
|
|
18
18
|
customColors?: string[]
|
|
19
|
+
customColorsOrdered?: string[]
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
import { type Link } from './../components/Sankey/types'
|
|
@@ -30,7 +31,7 @@ import { Version } from '@cdc/core/types/Version'
|
|
|
30
31
|
import Footnotes from '@cdc/core/types/Footnotes'
|
|
31
32
|
|
|
32
33
|
export type ViewportSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg'
|
|
33
|
-
|
|
34
|
+
type ChartColumns = Record<string, Column>
|
|
34
35
|
export type ChartOrientation = 'vertical' | 'horizontal'
|
|
35
36
|
export type VisualizationType =
|
|
36
37
|
| 'Area Chart'
|
|
@@ -75,6 +76,7 @@ type DataFormat = {
|
|
|
75
76
|
bottomSuffix: string
|
|
76
77
|
commas: boolean
|
|
77
78
|
prefix: string
|
|
79
|
+
preserveOriginalDecimals?: boolean
|
|
78
80
|
rightCommas: boolean
|
|
79
81
|
rightPrefix: string
|
|
80
82
|
rightRoundTo: number
|
|
@@ -91,7 +93,7 @@ type Exclusions = {
|
|
|
91
93
|
dateEnd: string
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
type Legend = CoreLegend & {
|
|
95
97
|
seriesHighlight: string[]
|
|
96
98
|
unified: boolean
|
|
97
99
|
hideSuppressionLink: boolean
|
|
@@ -192,8 +194,22 @@ export type AllChartsConfig = {
|
|
|
192
194
|
runtimeDataUrl: string
|
|
193
195
|
series: Series
|
|
194
196
|
showLineSeriesLabels: boolean
|
|
197
|
+
showAreaUnderLine?: boolean
|
|
195
198
|
showSidebar: boolean
|
|
196
199
|
showTitle: boolean
|
|
200
|
+
smallMultiples?: {
|
|
201
|
+
mode?: 'by-column' | 'by-series'
|
|
202
|
+
tileColumn?: string
|
|
203
|
+
tilesPerRowDesktop?: number
|
|
204
|
+
tilesPerRowMobile?: number
|
|
205
|
+
tileOrderType?: 'asc' | 'desc' | 'custom'
|
|
206
|
+
tileOrder?: string[]
|
|
207
|
+
tileTitles?: { [key: string]: string }
|
|
208
|
+
independentYAxis?: boolean
|
|
209
|
+
colorMode?: 'same' | 'different'
|
|
210
|
+
synchronizedTooltips?: boolean
|
|
211
|
+
showAreaUnderLine?: boolean
|
|
212
|
+
}
|
|
197
213
|
sortData: 'ascending' | 'descending'
|
|
198
214
|
stackedAreaChartLineType: string
|
|
199
215
|
suppressedData?: { label: string; icon: string; value: string }[]
|
|
@@ -217,6 +233,8 @@ export type AllChartsConfig = {
|
|
|
217
233
|
visualizationSubType: string
|
|
218
234
|
xAxis: Axis
|
|
219
235
|
yAxis: Axis
|
|
236
|
+
hideXAxisLabel?: boolean
|
|
237
|
+
hideYAxisLabel?: boolean
|
|
220
238
|
xScale: Function
|
|
221
239
|
yScale: Function
|
|
222
240
|
regions: Region[]
|
|
@@ -248,7 +266,7 @@ export type AllChartsConfig = {
|
|
|
248
266
|
}
|
|
249
267
|
} & MarkupConfig
|
|
250
268
|
|
|
251
|
-
|
|
269
|
+
type ForestPlotConfig = {
|
|
252
270
|
visualizationType: 'Forest Plot'
|
|
253
271
|
forestPlot: ForestPlotConfigSettings
|
|
254
272
|
} & AllChartsConfig &
|
|
@@ -263,7 +281,7 @@ export type LineChartConfig = {
|
|
|
263
281
|
} & AllChartsConfig &
|
|
264
282
|
MarkupConfig
|
|
265
283
|
|
|
266
|
-
|
|
284
|
+
type SankeyLink = {
|
|
267
285
|
depth: number
|
|
268
286
|
height: number
|
|
269
287
|
id: string
|
|
@@ -284,7 +302,7 @@ type StoryNode = {
|
|
|
284
302
|
segmentTextBefore: string
|
|
285
303
|
}
|
|
286
304
|
|
|
287
|
-
|
|
305
|
+
type SankeyChartConfig = {
|
|
288
306
|
enableTooltips: boolean
|
|
289
307
|
data: [
|
|
290
308
|
{
|
|
@@ -16,7 +16,9 @@ type SharedChartContext = {
|
|
|
16
16
|
clean: Function
|
|
17
17
|
colorScale?: ColorScale
|
|
18
18
|
config: ChartConfig
|
|
19
|
+
convertLineToBarGraph?: boolean
|
|
19
20
|
currentViewport?: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
|
|
21
|
+
vizViewport?: 'lg' | 'md' | 'sm' | 'xs' | 'xxs'
|
|
20
22
|
dashboardConfig?: DashboardConfig
|
|
21
23
|
// process top level chart aria label for each chart type
|
|
22
24
|
handleChartAriaLabels: (config: any) => string
|
|
@@ -33,6 +35,7 @@ type SharedChartContext = {
|
|
|
33
35
|
parentRef?: React.RefObject<HTMLDivElement>
|
|
34
36
|
setLegendIsolateValues?: Function
|
|
35
37
|
svgRef?: React.RefObject<SVGSVGElement>
|
|
38
|
+
handleSmallMultipleHover?: (xAxisValue: any, yCoordinate: number) => void
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
// Line Chart Specific Context
|