@cdc/chart 4.25.10 → 4.26.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-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
- package/dist/cdcchart.js +44003 -43518
- package/examples/feature/__data__/planet-example-data.json +1 -1
- package/examples/feature/boxplot/valid-boxplot.csv +38 -17
- package/examples/feature/pie/planet-pie-example-config.json +48 -2
- package/examples/private/DEV-11825.json +573 -0
- package/examples/private/DEV-12100.json +1303 -0
- package/examples/private/cat-y.json +1235 -0
- package/examples/private/data-points.json +228 -0
- package/examples/private/height.json +3915 -0
- package/examples/private/links.json +569 -0
- package/examples/private/na.json +913 -0
- package/examples/private/quadrant.txt +30 -0
- package/examples/private/test-data.csv +28 -0
- package/examples/private/test-forecast.json +5510 -0
- package/examples/private/warming-stripe-test.json +2578 -0
- package/examples/private/warming-stripes.json +4763 -0
- package/examples/tech-adoption-with-links.json +560 -0
- package/index.html +16 -140
- package/package.json +6 -5
- package/preview.html +1616 -0
- package/src/CdcChart.tsx +8 -11
- package/src/CdcChartComponent.tsx +329 -124
- 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.Regions.Categorical.stories.tsx +148 -0
- package/src/_stories/Chart.Regions.DateScale.stories.tsx +197 -0
- package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +297 -0
- package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
- package/src/_stories/Chart.stories.tsx +8 -0
- package/src/_stories/ChartAnnotation.stories.tsx +6 -3
- package/src/_stories/ChartBar.Editor.stories.tsx +3585 -0
- package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
- package/src/_stories/ChartBrush.stories.tsx +50 -0
- package/src/_stories/ChartEditor.Editor.stories.tsx +656 -0
- package/src/_stories/ChartEditor.stories.tsx +1 -2
- package/src/_stories/TechAdoptionWithLinks.stories.tsx +27 -0
- package/src/_stories/_mock/brush_enabled.json +326 -0
- package/src/_stories/_mock/brush_mock.json +2 -69
- 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/horizontal-bars-dynamic-y-axis.json +413 -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 -7
- package/src/components/AreaChart/index.tsx +1 -2
- package/src/components/Axis/Categorical.Axis.tsx +6 -7
- package/src/components/BarChart/components/BarChart.Horizontal.tsx +181 -27
- package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
- package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
- package/src/components/BarChart/components/BarChart.Vertical.tsx +8 -9
- package/src/components/BarChart/components/context.tsx +1 -0
- package/src/components/BarChart/helpers/useBarChart.ts +14 -2
- package/src/components/BoxPlot/helpers/index.ts +3 -3
- package/src/components/Brush/BrushSelector.tsx +1258 -0
- package/src/components/Brush/MiniChartPreview.tsx +283 -0
- package/src/components/DeviationBar.jsx +9 -7
- package/src/components/EditorPanel/EditorPanel.tsx +2720 -2586
- package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
- package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
- package/src/components/EditorPanel/components/Panels/Panel.General.tsx +76 -31
- package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +104 -55
- package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
- package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +427 -0
- package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +96 -48
- 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 +36 -31
- package/src/components/Forecasting/Forecasting.tsx +139 -21
- package/src/components/Legend/Legend.Component.tsx +16 -9
- package/src/components/Legend/Legend.tsx +3 -2
- package/src/components/Legend/helpers/createFormatLabels.tsx +325 -176
- package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
- package/src/components/Legend/helpers/index.ts +10 -6
- 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 +559 -499
- package/src/components/PairedBarChart.jsx +20 -3
- package/src/components/Regions/components/Regions.tsx +366 -144
- package/src/components/Sankey/types/index.ts +1 -1
- package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
- package/src/components/SmallMultiples/SmallMultipleTile.tsx +202 -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/components/WarmingStripes/WarmingStripes.tsx +160 -0
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
- package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
- package/src/components/WarmingStripes/index.tsx +3 -0
- package/src/data/initial-state.js +16 -2
- package/src/helpers/buildForecastPaletteOptions.ts +0 -38
- package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
- package/src/helpers/getColorScale.ts +10 -0
- package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +26 -14
- package/src/helpers/getYAxisAutoPadding.ts +53 -0
- package/src/helpers/sizeHelpers.ts +0 -20
- package/src/helpers/smallMultiplesHelpers.ts +529 -0
- package/src/hooks/useChartHoverAnalytics.tsx +10 -9
- package/src/hooks/useProgrammaticTooltip.ts +96 -0
- package/src/hooks/useScales.ts +98 -34
- package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
- package/src/hooks/useTooltip.tsx +91 -25
- package/src/scss/DataTable.scss +0 -4
- package/src/scss/main.scss +18 -83
- package/src/store/chart.actions.ts +2 -0
- package/src/store/chart.reducer.ts +4 -0
- package/src/test/CdcChart.test.jsx +1 -1
- package/src/types/ChartConfig.ts +27 -6
- package/src/types/ChartContext.ts +3 -0
- package/src/types/Label.ts +1 -0
- package/src/utils/analyticsTracking.ts +19 -0
- package/LICENSE +0 -201
- package/src/_stories/_mock/pie_data.json +0 -218
- package/src/components/AreaChart/components/AreaChart.jsx +0 -109
- package/src/components/Brush/BrushChart.tsx +0 -128
- package/src/components/Brush/BrushController.tsx +0 -71
- package/src/components/Brush/types.tsx +0 -8
- package/src/components/BrushChart.tsx +0 -223
- 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,17 @@ const useScales = (properties: useScaleProps) => {
|
|
|
69
133
|
// handle Vertical bars
|
|
70
134
|
if (!isHorizontal) {
|
|
71
135
|
xScale = composeScaleBand(xAxisDataMapped, [0, xMax], paddingRange)
|
|
72
|
-
|
|
136
|
+
// For categorical y-axis, use [0, max] domain and [yMax, 0] range to match CategoricalYAxis
|
|
137
|
+
// This ensures line data aligns with categorical bars and bars go to 100% height
|
|
138
|
+
if (config.yAxis.type === 'categorical') {
|
|
139
|
+
yScale = scaleLinear({
|
|
140
|
+
domain: [0, max],
|
|
141
|
+
range: [yMax, 0],
|
|
142
|
+
clamp: true
|
|
143
|
+
})
|
|
144
|
+
} else {
|
|
145
|
+
yScale = composeYScale({ min, max, yMax, config, leftMax })
|
|
146
|
+
}
|
|
73
147
|
seriesScale = composeScaleBand(seriesDomain, [0, xScale.bandwidth()], 0)
|
|
74
148
|
}
|
|
75
149
|
|
|
@@ -290,17 +364,29 @@ const useScales = (properties: useScaleProps) => {
|
|
|
290
364
|
}
|
|
291
365
|
}
|
|
292
366
|
}
|
|
293
|
-
return {
|
|
367
|
+
return {
|
|
368
|
+
xScale,
|
|
369
|
+
yScale,
|
|
370
|
+
seriesScale,
|
|
371
|
+
g1xScale,
|
|
372
|
+
g2xScale,
|
|
373
|
+
xScaleNoPadding,
|
|
374
|
+
xScaleAnnotation,
|
|
375
|
+
min,
|
|
376
|
+
max,
|
|
377
|
+
leftMax,
|
|
378
|
+
rightMax
|
|
379
|
+
}
|
|
294
380
|
}
|
|
295
381
|
|
|
296
382
|
export default useScales
|
|
297
383
|
|
|
298
|
-
|
|
384
|
+
const getFirstDayOfMonth = ms => {
|
|
299
385
|
const date = new Date(ms)
|
|
300
386
|
return new Date(date.getFullYear(), date.getMonth(), 1).getTime()
|
|
301
387
|
}
|
|
302
388
|
|
|
303
|
-
|
|
389
|
+
const dateFormatHasMonthButNoDays = dateFormat => {
|
|
304
390
|
return (
|
|
305
391
|
(dateFormat.includes('%b') ||
|
|
306
392
|
dateFormat.includes('%B') ||
|
|
@@ -352,28 +438,6 @@ export const getTickValues = (xAxisDataMapped, xScale, num, config) => {
|
|
|
352
438
|
}
|
|
353
439
|
}
|
|
354
440
|
|
|
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
441
|
/// helper functions
|
|
378
442
|
const composeXScale = ({ min, max, xMax, config }) => {
|
|
379
443
|
// 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
|
|
@@ -97,7 +105,7 @@ export const useTooltip = props => {
|
|
|
97
105
|
addColCommas: column.commas
|
|
98
106
|
}
|
|
99
107
|
|
|
100
|
-
const pieColumnData = additionalChartData?.
|
|
108
|
+
const pieColumnData = additionalChartData?.data?.[column.name]
|
|
101
109
|
const columnData =
|
|
102
110
|
config.tooltips.singleSeries && visualizationType === 'Line'
|
|
103
111
|
? resolvedScaleValues.filter(
|
|
@@ -137,7 +145,7 @@ export const useTooltip = props => {
|
|
|
137
145
|
tooltipItems.push(
|
|
138
146
|
[config.xAxis.dataKey, pieData[config.xAxis.dataKey]],
|
|
139
147
|
[
|
|
140
|
-
config.runtime.yAxis.dataKey,
|
|
148
|
+
config.runtime.yAxis.label || config.runtime.yAxis.dataKey,
|
|
141
149
|
showPiePercent ? pctString(actualPieValue) : formatNumber(pieData[config.runtime.yAxis.dataKey])
|
|
142
150
|
],
|
|
143
151
|
showPiePercent ? [] : ['Percent', pctString(pctOf360)]
|
|
@@ -252,11 +260,30 @@ export const useTooltip = props => {
|
|
|
252
260
|
const dataXPosition = eventSvgCoords.x + 10
|
|
253
261
|
const dataYPosition = eventSvgCoords.y
|
|
254
262
|
|
|
263
|
+
// Helper to strip <a> tags and only show link text
|
|
264
|
+
function stripLinkTags(str) {
|
|
265
|
+
if (typeof str !== 'string') return str
|
|
266
|
+
// Remove HTML <a> tags, keep inner text
|
|
267
|
+
return str.replace(/<a [^>]*>(.*?)<\/a>/gi, '$1')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Strip link tags from all tooltip values
|
|
271
|
+
const cleanTooltipItems = [...tooltipItems, ...additionalTooltipItems].map(item => {
|
|
272
|
+
// item can be [key, value] or [key, value, axisPosition]
|
|
273
|
+
if (Array.isArray(item)) {
|
|
274
|
+
// Only strip from value (item[1])
|
|
275
|
+
const newItem = [...item]
|
|
276
|
+
newItem[1] = stripLinkTags(newItem[1])
|
|
277
|
+
return newItem
|
|
278
|
+
}
|
|
279
|
+
return item
|
|
280
|
+
})
|
|
281
|
+
|
|
255
282
|
const tooltipInformation = {
|
|
256
283
|
tooltipLeft: dataXPosition,
|
|
257
284
|
tooltipTop: dataYPosition,
|
|
258
285
|
tooltipData: {
|
|
259
|
-
data:
|
|
286
|
+
data: cleanTooltipItems,
|
|
260
287
|
dataXPosition,
|
|
261
288
|
dataYPosition
|
|
262
289
|
}
|
|
@@ -289,15 +316,15 @@ export const useTooltip = props => {
|
|
|
289
316
|
*/
|
|
290
317
|
const getXValueFromCoordinateDate = x => {
|
|
291
318
|
if (config.xAxis.type === 'categorical' || config.visualizationType === 'Combo') {
|
|
292
|
-
let eachBand =
|
|
319
|
+
let eachBand = xScaleRef.current.step()
|
|
293
320
|
let numerator = x
|
|
294
321
|
const index = Math.floor(Number(numerator) / eachBand)
|
|
295
|
-
return
|
|
322
|
+
return xScaleRef.current.domain()[index - 1] // fixes off by 1 error
|
|
296
323
|
}
|
|
297
324
|
|
|
298
325
|
if (isDateScale(config.xAxis) && config.visualizationType !== 'Combo') {
|
|
299
326
|
const bisectDate = bisector(d => parseDate(d[config.xAxis.dataKey])).left
|
|
300
|
-
const x0 =
|
|
327
|
+
const x0 = xScaleRef.current.invert(xScaleRef.current(x))
|
|
301
328
|
const index = bisectDate(config.data, x0, 1)
|
|
302
329
|
const val = parseDate(config.data[index - 1][config.xAxis.dataKey])
|
|
303
330
|
return val
|
|
@@ -309,12 +336,12 @@ export const useTooltip = props => {
|
|
|
309
336
|
* @function getXValueFromCoordinate
|
|
310
337
|
* @returns {String} - the closest x value to the cursor position
|
|
311
338
|
*/
|
|
312
|
-
const getXValueFromCoordinate =
|
|
339
|
+
const getXValueFromCoordinate = x => {
|
|
313
340
|
if (visualizationType === 'Pie') return
|
|
314
341
|
if (orientation === 'horizontal') return
|
|
315
342
|
|
|
316
343
|
// Check the type of x equal to point or if the type of xAxis is equal to continuous or date
|
|
317
|
-
if (
|
|
344
|
+
if (xScaleRef.current.type === 'point' || xAxis.type === 'continuous' || isDateScale(xAxis)) {
|
|
318
345
|
// Find the closest x value by calculating the minimum distance
|
|
319
346
|
let closestX = null
|
|
320
347
|
let minDistance = Number.MAX_VALUE
|
|
@@ -322,9 +349,11 @@ export const useTooltip = props => {
|
|
|
322
349
|
|
|
323
350
|
const barThicknessOffset = config.xAxis.type === 'date' ? xScale.bandwidth() / 2 : 0
|
|
324
351
|
data.forEach(d => {
|
|
325
|
-
const xPosition = isDateScale(xAxis)
|
|
352
|
+
const xPosition = isDateScale(xAxis)
|
|
353
|
+
? xScaleRef.current(parseDate(d[xAxis.dataKey]))
|
|
354
|
+
: xScaleRef.current(d[xAxis.dataKey])
|
|
326
355
|
let bwOffset = config.barHeight
|
|
327
|
-
const distance = Math.abs(Number(xPosition + barThicknessOffset - offset
|
|
356
|
+
const distance = Math.abs(Number(xPosition + barThicknessOffset - offset))
|
|
328
357
|
|
|
329
358
|
if (distance <= minDistance) {
|
|
330
359
|
minDistance = distance
|
|
@@ -334,16 +363,50 @@ export const useTooltip = props => {
|
|
|
334
363
|
return closestX
|
|
335
364
|
}
|
|
336
365
|
|
|
366
|
+
// For band scales, find which band the mouse x-coordinate falls within
|
|
337
367
|
if (config.xAxis.type === 'categorical' || visualizationType === 'Combo') {
|
|
338
|
-
|
|
339
|
-
|
|
368
|
+
const domain = xScaleRef.current.domain()
|
|
369
|
+
const bandwidth = xScaleRef.current.bandwidth()
|
|
340
370
|
|
|
341
|
-
let
|
|
342
|
-
|
|
343
|
-
|
|
371
|
+
let closestValue = null
|
|
372
|
+
let minDistance = Number.MAX_VALUE
|
|
373
|
+
|
|
374
|
+
domain.forEach(value => {
|
|
375
|
+
const bandStart = xScaleRef.current(value)
|
|
376
|
+
const bandCenter = bandStart + bandwidth / 2
|
|
377
|
+
const distance = Math.abs(x - bandCenter)
|
|
378
|
+
|
|
379
|
+
if (distance < minDistance) {
|
|
380
|
+
minDistance = distance
|
|
381
|
+
closestValue = value
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
return closestValue
|
|
344
386
|
}
|
|
345
387
|
}
|
|
346
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Helper for converting data value to pixel coordinate (inverse of getXValueFromCoordinate)
|
|
391
|
+
* @function getCoordinateFromXValue
|
|
392
|
+
* @param {any} xAxisValue - X-axis data value (date, number, or category)
|
|
393
|
+
* @returns {number} - pixel coordinate for the data value
|
|
394
|
+
*/
|
|
395
|
+
const getCoordinateFromXValue = xAxisValue => {
|
|
396
|
+
if (visualizationType === 'Pie') return 0
|
|
397
|
+
if (orientation === 'horizontal') return 0
|
|
398
|
+
|
|
399
|
+
// Convert data value to pixel coordinate using current xScale
|
|
400
|
+
let pixelX = isDateScale(xAxis) ? xScaleRef.current(parseDate(xAxisValue)) : xScaleRef.current(xAxisValue)
|
|
401
|
+
|
|
402
|
+
// For band scales (bar charts, categorical axes), add bandwidth offset to point to center of bar
|
|
403
|
+
if (xScaleRef.current.bandwidth) {
|
|
404
|
+
pixelX += xScaleRef.current.bandwidth() / 2
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return pixelX
|
|
408
|
+
}
|
|
409
|
+
|
|
347
410
|
const findClosest = (dataArray: [any, number][], mouseXorY) => {
|
|
348
411
|
let dataColumn: Object
|
|
349
412
|
dataArray.find(([d, xOrY]) => {
|
|
@@ -431,7 +494,7 @@ export const useTooltip = props => {
|
|
|
431
494
|
const eventSvgCoords = localPoint(e)
|
|
432
495
|
const { x } = eventSvgCoords
|
|
433
496
|
if (!x) throw new Error('COVE: no x value in handleTooltipClick.')
|
|
434
|
-
let closestXScaleValue = getXValueFromCoordinate(x
|
|
497
|
+
let closestXScaleValue = getXValueFromCoordinate(x)
|
|
435
498
|
let datum = config.data?.filter(item => item[config.xAxis.dataKey] === closestXScaleValue)
|
|
436
499
|
if (!closestXScaleValue) throw new Error('COVE: no closest x scale value in handleTooltipClick')
|
|
437
500
|
if (isDateScale(xAxis) && closestXScaleValue) {
|
|
@@ -479,14 +542,16 @@ export const useTooltip = props => {
|
|
|
479
542
|
includedSeries.push(...dynamicDataCategories)
|
|
480
543
|
|
|
481
544
|
if (config.visualizationType === 'Forecasting') {
|
|
482
|
-
config.runtime.series
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
545
|
+
config.runtime.series
|
|
546
|
+
.filter(s => s.type === 'Forecasting' && s.confidenceIntervals)
|
|
547
|
+
.forEach(s => {
|
|
548
|
+
s.confidenceIntervals.forEach(c => {
|
|
549
|
+
if (c.showInTooltip) {
|
|
550
|
+
includedSeries.push(c.high)
|
|
551
|
+
includedSeries.push(c.low)
|
|
552
|
+
}
|
|
553
|
+
})
|
|
488
554
|
})
|
|
489
|
-
})
|
|
490
555
|
}
|
|
491
556
|
|
|
492
557
|
const colNames = Object.values(config.columns).map(column => column.name)
|
|
@@ -502,7 +567,7 @@ export const useTooltip = props => {
|
|
|
502
567
|
const dataWithXScale = dataToSearch.map(
|
|
503
568
|
d => [d, seriesScale(d[dynamicSeries.dynamicCategory])] as [Object, number]
|
|
504
569
|
)
|
|
505
|
-
const xOffset = x - Y_AXIS_SIZE -
|
|
570
|
+
const xOffset = x - Y_AXIS_SIZE - xScaleRef.current(closestXScaleValue)
|
|
506
571
|
dataToSearch = [findClosest(dataWithXScale, xOffset)]
|
|
507
572
|
}
|
|
508
573
|
}
|
|
@@ -645,6 +710,7 @@ export const useTooltip = props => {
|
|
|
645
710
|
getIncludedTooltipSeries,
|
|
646
711
|
getXValueFromCoordinate,
|
|
647
712
|
getXValueFromCoordinateDate,
|
|
713
|
+
getCoordinateFromXValue,
|
|
648
714
|
handleTooltipClick,
|
|
649
715
|
handleTooltipMouseOff,
|
|
650
716
|
handleTooltipMouseOver,
|
package/src/scss/DataTable.scss
CHANGED
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
|
}
|
|
@@ -129,9 +50,6 @@
|
|
|
129
50
|
.subtext,
|
|
130
51
|
.subtext--responsive-ticks,
|
|
131
52
|
.section-subtext {
|
|
132
|
-
&--brush-active {
|
|
133
|
-
margin-top: 3rem !important;
|
|
134
|
-
}
|
|
135
53
|
}
|
|
136
54
|
|
|
137
55
|
.type-pie {
|
|
@@ -404,6 +322,23 @@
|
|
|
404
322
|
margin-bottom: 2.5em;
|
|
405
323
|
}
|
|
406
324
|
|
|
325
|
+
// Brush touch support
|
|
326
|
+
.brush-overlay {
|
|
327
|
+
touch-action: none;
|
|
328
|
+
-webkit-touch-callout: none;
|
|
329
|
+
-webkit-user-select: none;
|
|
330
|
+
user-select: none;
|
|
331
|
+
|
|
332
|
+
.visx-brush,
|
|
333
|
+
.visx-brush-overlay,
|
|
334
|
+
.visx-brush-selection,
|
|
335
|
+
.visx-brush-handle-left,
|
|
336
|
+
.visx-brush-handle-right {
|
|
337
|
+
touch-action: none;
|
|
338
|
+
cursor: pointer;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
407
342
|
svg.dragging-annotation * {
|
|
408
343
|
user-select: none;
|
|
409
344
|
}
|
|
@@ -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':
|