@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.
Files changed (135) hide show
  1. package/dist/{cdcchart-1a1724a1.es.js → cdcchart-dgT_1dIT.es.js} +136 -151
  2. package/dist/cdcchart.js +44003 -43518
  3. package/examples/feature/__data__/planet-example-data.json +1 -1
  4. package/examples/feature/boxplot/valid-boxplot.csv +38 -17
  5. package/examples/feature/pie/planet-pie-example-config.json +48 -2
  6. package/examples/private/DEV-11825.json +573 -0
  7. package/examples/private/DEV-12100.json +1303 -0
  8. package/examples/private/cat-y.json +1235 -0
  9. package/examples/private/data-points.json +228 -0
  10. package/examples/private/height.json +3915 -0
  11. package/examples/private/links.json +569 -0
  12. package/examples/private/na.json +913 -0
  13. package/examples/private/quadrant.txt +30 -0
  14. package/examples/private/test-data.csv +28 -0
  15. package/examples/private/test-forecast.json +5510 -0
  16. package/examples/private/warming-stripe-test.json +2578 -0
  17. package/examples/private/warming-stripes.json +4763 -0
  18. package/examples/tech-adoption-with-links.json +560 -0
  19. package/index.html +16 -140
  20. package/package.json +6 -5
  21. package/preview.html +1616 -0
  22. package/src/CdcChart.tsx +8 -11
  23. package/src/CdcChartComponent.tsx +329 -124
  24. package/src/_stories/Chart.Combo.stories.tsx +18 -0
  25. package/src/_stories/Chart.Forecast.stories.tsx +36 -0
  26. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +520 -0
  27. package/src/_stories/Chart.Patterns.stories.tsx +2 -1
  28. package/src/_stories/Chart.PreserveDecimals.stories.tsx +220 -0
  29. package/src/_stories/Chart.Regions.Categorical.stories.tsx +148 -0
  30. package/src/_stories/Chart.Regions.DateScale.stories.tsx +197 -0
  31. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +297 -0
  32. package/src/_stories/Chart.SmallMultiples.stories.tsx +47 -0
  33. package/src/_stories/Chart.stories.tsx +8 -0
  34. package/src/_stories/ChartAnnotation.stories.tsx +6 -3
  35. package/src/_stories/ChartBar.Editor.stories.tsx +3585 -0
  36. package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
  37. package/src/_stories/ChartBrush.stories.tsx +50 -0
  38. package/src/_stories/ChartEditor.Editor.stories.tsx +656 -0
  39. package/src/_stories/ChartEditor.stories.tsx +1 -2
  40. package/src/_stories/TechAdoptionWithLinks.stories.tsx +27 -0
  41. package/src/_stories/_mock/brush_enabled.json +326 -0
  42. package/src/_stories/_mock/brush_mock.json +2 -69
  43. package/src/_stories/_mock/combo.json +451 -0
  44. package/src/_stories/_mock/editor-test-configs.json +376 -0
  45. package/src/_stories/_mock/editor-test-datasets.json +477 -0
  46. package/src/_stories/_mock/editor-tests/bar-chart-editor-test.json +255 -0
  47. package/src/_stories/_mock/editor-tests/bar-chart-general-test.json +267 -0
  48. package/src/_stories/_mock/editor-tests/bar-chart-test.json +237 -0
  49. package/src/_stories/_mock/forecast_combo_with_gaps.json +913 -0
  50. package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
  51. package/src/_stories/_mock/pie_config.json +257 -62
  52. package/src/_stories/_mock/small_multiples/small_multiples_bars.json +1944 -0
  53. package/src/_stories/_mock/small_multiples/small_multiples_big_data_bars.json +1114 -0
  54. package/src/_stories/_mock/small_multiples/small_multiples_lines.json +2646 -0
  55. package/src/_stories/_mock/small_multiples/small_multiples_lines_colors.json +1305 -0
  56. package/src/_stories/_mock/small_multiples/small_multiples_stacked_bars.json +1936 -0
  57. package/src/components/Annotations/components/findNearestDatum.ts +6 -41
  58. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +10 -7
  59. package/src/components/AreaChart/index.tsx +1 -2
  60. package/src/components/Axis/Categorical.Axis.tsx +6 -7
  61. package/src/components/BarChart/components/BarChart.Horizontal.tsx +181 -27
  62. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
  63. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
  64. package/src/components/BarChart/components/BarChart.Vertical.tsx +8 -9
  65. package/src/components/BarChart/components/context.tsx +1 -0
  66. package/src/components/BarChart/helpers/useBarChart.ts +14 -2
  67. package/src/components/BoxPlot/helpers/index.ts +3 -3
  68. package/src/components/Brush/BrushSelector.tsx +1258 -0
  69. package/src/components/Brush/MiniChartPreview.tsx +283 -0
  70. package/src/components/DeviationBar.jsx +9 -7
  71. package/src/components/EditorPanel/EditorPanel.tsx +2720 -2586
  72. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +96 -111
  73. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
  74. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +76 -31
  75. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +104 -55
  76. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +54 -49
  77. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +427 -0
  78. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +96 -48
  79. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  80. package/src/components/EditorPanel/editor-panel.scss +0 -20
  81. package/src/components/EditorPanel/useEditorPermissions.ts +36 -31
  82. package/src/components/Forecasting/Forecasting.tsx +139 -21
  83. package/src/components/Legend/Legend.Component.tsx +16 -9
  84. package/src/components/Legend/Legend.tsx +3 -2
  85. package/src/components/Legend/helpers/createFormatLabels.tsx +325 -176
  86. package/src/components/Legend/helpers/getLegendClasses.ts +0 -1
  87. package/src/components/Legend/helpers/index.ts +10 -6
  88. package/src/components/LineChart/LineChartProps.ts +0 -3
  89. package/src/components/LineChart/helpers.ts +1 -1
  90. package/src/components/LineChart/index.tsx +36 -13
  91. package/src/components/LinearChart.tsx +559 -499
  92. package/src/components/PairedBarChart.jsx +20 -3
  93. package/src/components/Regions/components/Regions.tsx +366 -144
  94. package/src/components/Sankey/types/index.ts +1 -1
  95. package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
  96. package/src/components/SmallMultiples/SmallMultipleTile.tsx +202 -0
  97. package/src/components/SmallMultiples/SmallMultiples.css +32 -0
  98. package/src/components/SmallMultiples/SmallMultiples.tsx +271 -0
  99. package/src/components/SmallMultiples/index.ts +2 -0
  100. package/src/components/WarmingStripes/WarmingStripes.tsx +160 -0
  101. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
  102. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
  103. package/src/components/WarmingStripes/index.tsx +3 -0
  104. package/src/data/initial-state.js +16 -2
  105. package/src/helpers/buildForecastPaletteOptions.ts +0 -38
  106. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
  107. package/src/helpers/getColorScale.ts +10 -0
  108. package/src/{hooks/useMinMax.ts → helpers/getMinMax.ts} +26 -14
  109. package/src/helpers/getYAxisAutoPadding.ts +53 -0
  110. package/src/helpers/sizeHelpers.ts +0 -20
  111. package/src/helpers/smallMultiplesHelpers.ts +529 -0
  112. package/src/hooks/useChartHoverAnalytics.tsx +10 -9
  113. package/src/hooks/useProgrammaticTooltip.ts +96 -0
  114. package/src/hooks/useScales.ts +98 -34
  115. package/src/hooks/useSmallMultipleSynchronization.ts +59 -0
  116. package/src/hooks/useTooltip.tsx +91 -25
  117. package/src/scss/DataTable.scss +0 -4
  118. package/src/scss/main.scss +18 -83
  119. package/src/store/chart.actions.ts +2 -0
  120. package/src/store/chart.reducer.ts +4 -0
  121. package/src/test/CdcChart.test.jsx +1 -1
  122. package/src/types/ChartConfig.ts +27 -6
  123. package/src/types/ChartContext.ts +3 -0
  124. package/src/types/Label.ts +1 -0
  125. package/src/utils/analyticsTracking.ts +19 -0
  126. package/LICENSE +0 -201
  127. package/src/_stories/_mock/pie_data.json +0 -218
  128. package/src/components/AreaChart/components/AreaChart.jsx +0 -109
  129. package/src/components/Brush/BrushChart.tsx +0 -128
  130. package/src/components/Brush/BrushController.tsx +0 -71
  131. package/src/components/Brush/types.tsx +0 -8
  132. package/src/components/BrushChart.tsx +0 -223
  133. package/src/helpers/sort.ts +0 -7
  134. package/src/hooks/useActiveElement.js +0 -19
  135. package/src/hooks/useChartClasses.js +0 -41
@@ -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
- max: number // maximum value from useMinMax hook
31
- min: number // minimum value from useMinMax hook
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 { xAxisDataMapped, xMax, yMax, min, max, config, data } = properties
39
-
40
- const { rawData, dimensions } = useContext<ChartContext>(ConfigContext)
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, ...properties })
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
- yScale = composeYScale(properties)
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 { xScale, yScale, seriesScale, g1xScale, g2xScale, xScaleNoPadding, xScaleAnnotation }
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
- export const getFirstDayOfMonth = ms => {
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
- export const dateFormatHasMonthButNoDays = dateFormat => {
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
+ }
@@ -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?.arc?.data[column.name]
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: [...tooltipItems, ...additionalTooltipItems],
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 = xScale.step()
319
+ let eachBand = xScaleRef.current.step()
293
320
  let numerator = x
294
321
  const index = Math.floor(Number(numerator) / eachBand)
295
- return xScale.domain()[index - 1] // fixes off by 1 error
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 = xScale.invert(xScale(x))
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 = (x, isClick = false) => {
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 (xScale.type === 'point' || xAxis.type === 'continuous' || isDateScale(xAxis)) {
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) ? xScale(parseDate(d[xAxis.dataKey])) : xScale(d[xAxis.dataKey])
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 + (isClick ? bwOffset * 2 : 0)))
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
- let range = xScale.range()[1] - xScale.range()[0]
339
- let eachBand = range / (xScale.domain().length + 1)
368
+ const domain = xScaleRef.current.domain()
369
+ const bandwidth = xScaleRef.current.bandwidth()
340
370
 
341
- let numerator = x
342
- const index = Math.floor((Number(numerator) - eachBand / 2) / eachBand)
343
- return xScale.domain()[index] // fixes off by 1 error
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, true)
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.map(s => {
483
- s.confidenceIntervals.map(c => {
484
- if (c.showInTooltip) {
485
- includedSeries.push(c.high)
486
- includedSeries.push(c.low)
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 - xScale(closestXScaleValue)
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,
@@ -1,8 +1,4 @@
1
1
  .data-table-container {
2
- &.brush-active {
3
- margin: 80px 0 0;
4
- }
5
-
6
2
  width: 100%;
7
3
  }
8
4
 
@@ -1,52 +1,4 @@
1
- @import '@cdc/core/styles/accessibility';
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':
@@ -7,5 +7,5 @@ describe('Chart', () => {
7
7
  const pkgDir = path.join(__dirname, '..')
8
8
  const result = testStandaloneBuild(pkgDir)
9
9
  expect(result).toBe(true)
10
- })
10
+ }, 300000)
11
11
  })