@cdc/chart 4.25.5-1 → 4.25.6-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 (43) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cdcchart.js +32053 -27935
  3. package/index.html +130 -130
  4. package/package.json +2 -2
  5. package/src/CdcChartComponent.tsx +66 -26
  6. package/src/_stories/Chart.stories.tsx +99 -93
  7. package/src/_stories/ChartPrefixSuffix.stories.tsx +29 -32
  8. package/src/_stories/_mock/pie_calculated_area.json +417 -0
  9. package/src/components/BarChart/components/BarChart.Horizontal.tsx +4 -13
  10. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +3 -14
  11. package/src/components/BarChart/components/BarChart.Vertical.tsx +2 -8
  12. package/src/components/Brush/BrushChart.tsx +73 -0
  13. package/src/components/Brush/BrushController..tsx +39 -0
  14. package/src/components/DeviationBar.jsx +0 -1
  15. package/src/components/EditorPanel/EditorPanel.tsx +246 -156
  16. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +2 -2
  17. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +3 -2
  18. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +2 -1
  19. package/src/components/EditorPanel/components/Panels/panelVisual.styles.css +8 -0
  20. package/src/components/EditorPanel/useEditorPermissions.ts +7 -4
  21. package/src/components/HoverLine/HoverLine.tsx +74 -0
  22. package/src/components/Legend/Legend.Suppression.tsx +47 -3
  23. package/src/components/Legend/helpers/index.ts +1 -1
  24. package/src/components/LineChart/helpers.ts +7 -7
  25. package/src/components/LineChart/index.tsx +3 -6
  26. package/src/components/LinearChart.tsx +108 -72
  27. package/src/components/PieChart/PieChart.tsx +58 -13
  28. package/src/data/initial-state.js +8 -5
  29. package/src/helpers/countNumOfTicks.ts +4 -19
  30. package/src/helpers/getNewRuntime.ts +35 -0
  31. package/src/helpers/getPiePercent.ts +22 -0
  32. package/src/helpers/getTransformedData.ts +22 -0
  33. package/src/helpers/tests/getNewRuntime.test.ts +82 -0
  34. package/src/helpers/tests/getPiePercent.test.ts +38 -0
  35. package/src/hooks/useRightAxis.ts +1 -1
  36. package/src/hooks/useScales.ts +8 -3
  37. package/src/hooks/useTooltip.tsx +24 -10
  38. package/src/scss/main.scss +8 -4
  39. package/src/store/chart.actions.ts +2 -6
  40. package/src/store/chart.reducer.ts +23 -23
  41. package/src/types/ChartConfig.ts +7 -4
  42. package/src/types/ChartContext.ts +0 -2
  43. package/src/components/ZoomBrush.tsx +0 -251
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getNewRuntime } from '../getNewRuntime'
3
+
4
+ describe('getNewRuntime', () => {
5
+ it('should return a runtime object with default values when no data is provided', () => {
6
+ const visualizationConfig = { runtime: {} }
7
+ const newFilteredData = null
8
+
9
+ const result = getNewRuntime(visualizationConfig, newFilteredData)
10
+
11
+ expect(result.series).toEqual([])
12
+ expect(result.seriesLabels).toEqual({})
13
+ expect(result.seriesLabelsAll).toEqual([])
14
+ expect(result.seriesKeys).toEqual([])
15
+ })
16
+
17
+ it('should populate runtime.series with valid series from newFilteredData', () => {
18
+ const visualizationConfig = {
19
+ runtime: {},
20
+ filters: [],
21
+ columns: {},
22
+ dynamicSeriesType: 'bar',
23
+ dynamicSeriesLineType: 'solid',
24
+ xAxis: { dataKey: 'x' }
25
+ }
26
+ const newFilteredData = [
27
+ { x: 1, y: 10, z: 20 },
28
+ { x: 2, y: 15, z: 25 }
29
+ ]
30
+
31
+ const result = getNewRuntime(visualizationConfig, newFilteredData)
32
+
33
+ expect(result.series).toEqual([
34
+ { dataKey: 'y', type: 'bar', lineType: 'solid', tooltip: true },
35
+ { dataKey: 'z', type: 'bar', lineType: 'solid', tooltip: true }
36
+ ])
37
+ expect(result.seriesKeys).toEqual(['y', 'z'])
38
+ expect(result.seriesLabels).toEqual({ y: 'y', z: 'z' })
39
+ expect(result.seriesLabelsAll).toEqual(['y', 'z'])
40
+ })
41
+
42
+ it('should exclude series keys that match filters or columns', () => {
43
+ const visualizationConfig = {
44
+ runtime: {},
45
+ filters: [{ columnName: 'y' }],
46
+ columns: { z: {} },
47
+ dynamicSeriesType: 'bar',
48
+ dynamicSeriesLineType: 'solid',
49
+ xAxis: { dataKey: 'x' }
50
+ }
51
+ const newFilteredData = [
52
+ { x: 1, y: 10, z: 20, w: 30 },
53
+ { x: 2, y: 15, z: 25, w: 35 }
54
+ ]
55
+
56
+ const result = getNewRuntime(visualizationConfig, newFilteredData)
57
+
58
+ expect(result.series).toEqual([{ dataKey: 'w', type: 'bar', lineType: 'solid', tooltip: true }])
59
+ expect(result.seriesKeys).toEqual(['w'])
60
+ expect(result.seriesLabels).toEqual({ w: 'w' })
61
+ expect(result.seriesLabelsAll).toEqual(['w'])
62
+ })
63
+
64
+ it('should handle empty newFilteredData gracefully', () => {
65
+ const visualizationConfig = {
66
+ runtime: {},
67
+ filters: [],
68
+ columns: {},
69
+ dynamicSeriesType: 'bar',
70
+ dynamicSeriesLineType: 'solid',
71
+ xAxis: { dataKey: 'x' }
72
+ }
73
+ const newFilteredData = []
74
+
75
+ const result = getNewRuntime(visualizationConfig, newFilteredData)
76
+
77
+ expect(result.series).toEqual([])
78
+ expect(result.seriesKeys).toEqual([])
79
+ expect(result.seriesLabels).toEqual({})
80
+ expect(result.seriesLabelsAll).toEqual([])
81
+ })
82
+ })
@@ -0,0 +1,38 @@
1
+ // getPiePercent.test.ts
2
+ import { getPiePercent } from '../getPiePercent'
3
+
4
+ describe('getPiePercent', () => {
5
+ it('cgets percentages for purely numeric strings', () => {
6
+ const data = [{ A: '1' }, { A: '3' }, { A: '6' }]
7
+ const result = getPiePercent(data, 'A')
8
+
9
+ // sum = 1 + 3 + 6 = 10
10
+ expect(result[0].A).toBeCloseTo((1 / 10) * 100) // 10%
11
+ expect(result[1].A).toBeCloseTo((3 / 10) * 100) // 30%
12
+ expect(result[2].A).toBeCloseTo((6 / 10) * 100) // 60%
13
+ })
14
+
15
+ it('shandle non numbers like "ABC', () => {
16
+ const data = [{ A: '1' }, { A: 'ABC' }, { A: '2' }]
17
+ const result = getPiePercent(data, 'A')
18
+
19
+ expect(result[0].A).toBeCloseTo((1 / 3) * 100)
20
+ expect(result[1].A).toBe('ABC')
21
+ expect(result[2].A).toBeCloseTo((2 / 3) * 100)
22
+ })
23
+
24
+ it('handles all-zero total by producing 0%', () => {
25
+ const data = [{ A: '0' }, { A: '0' }]
26
+ const result = getPiePercent(data, 'A')
27
+ expect(result[0].A).toBe(0)
28
+ expect(result[1].A).toBe(0)
29
+ })
30
+
31
+ it('leaves rows missing the key entirely unchanged', () => {
32
+ const data = [{ A: '2' }, { B: 'foo' }]
33
+ const result = getPiePercent(data, 'A')
34
+
35
+ expect(result[0].A).toBeCloseTo(100)
36
+ expect(result[1]).toEqual({ B: 'foo' })
37
+ })
38
+ })
@@ -2,7 +2,7 @@ import { scaleLinear } from '@visx/scale'
2
2
  import useReduceData from './useReduceData'
3
3
  import { TOP_PADDING } from './useScales'
4
4
 
5
- export default function useRightAxis({ config, yMax = 0, data = [], updateConfig }) {
5
+ export default function useRightAxis({ config, yMax = 0, data = [] }) {
6
6
  const hasRightAxis = config.visualizationType === 'Combo' && config.orientation === 'vertical'
7
7
  const rightSeriesKeys =
8
8
  config.series && config.series.filter(series => series.axis === 'Right').map(key => key.dataKey)
@@ -79,13 +79,18 @@ const useScales = (properties: useScaleProps) => {
79
79
  xScale = composeScaleBand(xAxisDataMappedSorted, [0, xMax], 1 - config.barThickness)
80
80
  }
81
81
 
82
+ // handle Linear scaled viz
83
+ if (config.xAxis.type === 'date' && !isHorizontal) {
84
+ const sorted = sortXAxisData(xAxisDataMapped, config.xAxis.sortByRecentDate)
85
+
86
+ xScale = composeScaleBand(sorted, [0, xMax], 1 - config.barThickness)
87
+ xScale.type = scaleTypes.BAND
88
+ }
89
+
82
90
  if (xAxis.type === 'date-time' || xAxis.type === 'continuous') {
83
91
  let xAxisMin = Math.min(...xAxisDataMapped.map(Number))
84
92
  let xAxisMax = Math.max(...xAxisDataMapped.map(Number))
85
93
  let paddingRatio = config.xAxis.padding ? config.xAxis.padding * 0.01 : 0
86
- if (config.brush.active) {
87
- paddingRatio = config.barThickness * 0.2
88
- }
89
94
 
90
95
  xAxisMin -= paddingRatio * (xAxisMax - xAxisMin)
91
96
  xAxisMax += visualizationType === 'Line' ? 0 : paddingRatio * (xAxisMax - xAxisMin)
@@ -1,5 +1,6 @@
1
1
  import { useContext } from 'react'
2
2
  // Local imports
3
+ import parse from 'html-react-parser'
3
4
  import ConfigContext from '../ConfigContext'
4
5
  import { type ChartContext } from '../types/ChartContext'
5
6
  import { formatNumber as formatColNumber } from '@cdc/core/helpers/cove/number'
@@ -120,16 +121,25 @@ export const useTooltip = props => {
120
121
  const pieData = additionalChartData?.data ?? {}
121
122
  const startAngle = additionalChartData?.startAngle ?? 0
122
123
  const endAngle = additionalChartData?.endAngle ?? 0
124
+ const actualPieValue = Number(additionalChartData.data[config?.yAxis?.dataKey])
123
125
 
124
126
  const degrees = ((endAngle - startAngle) * 180) / Math.PI
125
127
  const pctOf360 = (degrees / 360) * 100
126
- const pctString = pctOf360.toFixed(roundTo) + '%'
128
+ const pctString = value => value.toFixed(roundTo) + '%'
129
+ const showPiePercent = config.dataFormat.showPiePercent || false
127
130
 
128
- tooltipItems.push(
129
- [config.xAxis.dataKey, pieData[config.xAxis.dataKey]],
130
- [config.runtime.yAxis.dataKey, formatNumber(pieData[config.runtime.yAxis.dataKey])],
131
- ['Percent', pctString]
132
- )
131
+ if (showPiePercent && pieData[config.xAxis.dataKey] === 'Calculated Area') {
132
+ tooltipItems.push(['', 'Calculated Area'])
133
+ } else {
134
+ tooltipItems.push(
135
+ [config.xAxis.dataKey, pieData[config.xAxis.dataKey]],
136
+ [
137
+ config.runtime.yAxis.dataKey,
138
+ showPiePercent ? pctString(actualPieValue) : formatNumber(pieData[config.runtime.yAxis.dataKey])
139
+ ],
140
+ showPiePercent ? [] : ['Percent', pctString(pctOf360)]
141
+ )
142
+ }
133
143
  }
134
144
 
135
145
  if (visualizationType === 'Forest Plot') {
@@ -188,6 +198,8 @@ export const useTooltip = props => {
188
198
  }
189
199
  })
190
200
  } else {
201
+ const dynamicSeries = config.series.find(s => s.dynamicCategory)
202
+
191
203
  // Show Only the Hovered Series in Tooltip
192
204
  const dataColumn = resolvedScaleValues[0]
193
205
  const [seriesKey, value] = findDataKeyByThreshold(y, dataColumn)
@@ -198,7 +210,7 @@ export const useTooltip = props => {
198
210
  tooltipItems.push([config.xAxis.dataKey, closestXScaleValue || xVal])
199
211
  const formattedValue = getFormattedValue(seriesKey, value, config, getAxisPosition)
200
212
  tooltipItems.push([seriesKey, formattedValue])
201
- } else {
213
+ } else if (dynamicSeries) {
202
214
  Object.keys(dataColumn).forEach(key => {
203
215
  tooltipItems.push([key, dataColumn[key]])
204
216
  })
@@ -546,7 +558,9 @@ export const useTooltip = props => {
546
558
  config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ` : ''
547
559
  )} ${config.xAxis.type === 'date' ? formattedDate : value}`}</li>
548
560
  )
549
-
561
+ if (visualizationType === 'Pie' && config.dataFormat.showPiePercent && value === 'Calculated Area') {
562
+ return <li className='tooltip-heading'>{`${capitalize('Calculated Area')} `}</li>
563
+ }
550
564
  if (key === config.xAxis.dataKey)
551
565
  return (
552
566
  <li className='tooltip-heading'>{`${capitalize(
@@ -571,14 +585,14 @@ export const useTooltip = props => {
571
585
  let newValue = label || value
572
586
  const style = displayGray ? { color: '#8b8b8a' } : {}
573
587
 
574
- if (index == 1 && config.dataFormat.onlyShowTopPrefixSuffix) {
588
+ if (index == 1 && config.yAxis?.inlineLabel) {
575
589
  newValue = `${config.dataFormat.prefix}${newValue}${config.dataFormat.suffix}`
576
590
  }
577
591
  const activeLabel = getSeriesNameFromLabel(key)
578
592
  const displayText = activeLabel ? `${activeLabel}: ${newValue}` : newValue
579
593
 
580
594
  return (
581
- <li style={style} className='tooltip-body'>
595
+ <li style={style} className='tooltip-body mb-1'>
582
596
  {displayText}
583
597
  </li>
584
598
  )
@@ -513,10 +513,6 @@
513
513
  }
514
514
  }
515
515
 
516
- [tabindex]:focus-visible {
517
- outline: 2px solid rgb(0, 95, 204) !important;
518
- }
519
-
520
516
  // ANIMATIONS
521
517
  // Pie Chart Animations
522
518
  .animated-pie {
@@ -743,3 +739,11 @@
743
739
  .cdc-open-viz-module .debug {
744
740
  border: 2px solid red;
745
741
  }
742
+
743
+ // Only frontend styles are applied in WCMS/TP
744
+ // This helps match those styles when viewing in the editor
745
+ .modal.cdc-cove-editor *:focus-visible,
746
+ .cdc-open-viz-module *:focus-visible {
747
+ outline: dashed 2px rgba(255, 102, 1, 0.9) !important;
748
+ outline-offset: 3px !important;
749
+ }
@@ -1,10 +1,6 @@
1
1
  import { DimensionsType } from '@cdc/core/types/Dimensions'
2
2
  import { ChartConfig } from '../types/ChartConfig'
3
-
4
- type Action<T, P = undefined, R = undefined> = {
5
- type: T
6
- payload?: P
7
- }
3
+ import { Action } from '@cdc/core/types/Action'
8
4
 
9
5
  // Action Types
10
6
  type SET_CONFIG = Action<'SET_CONFIG', ChartConfig>
@@ -34,7 +30,7 @@ type ChartActions =
34
30
  | SET_CONTAINER
35
31
  | SET_LOADED_EVENT
36
32
  | SET_DRAG_ANNOTATIONS
37
- | SET_BRUSH_CONFIG
38
33
  | SET_LOADING
34
+ | SET_BRUSH_CONFIG
39
35
 
40
36
  export default ChartActions
@@ -4,7 +4,28 @@ import { ChartConfig, type ViewportSize } from '../types/ChartConfig'
4
4
  import { DimensionsType } from '@cdc/core/types/Dimensions'
5
5
  import _ from 'lodash'
6
6
 
7
- export const getInitialState = (configObj: ChartConfig) => {
7
+ type ChartState = {
8
+ isLoading: boolean
9
+ config: ChartConfig
10
+ stateData: object[]
11
+ colorScale: Function
12
+ excludedData: object[]
13
+ filteredData: object[]
14
+ seriesHighlight: string[]
15
+ currentViewport: ViewportSize
16
+ dimensions: DimensionsType
17
+ container: HTMLElement | null
18
+ coveLoadedEventRan: boolean
19
+ isDraggingAnnotation: boolean
20
+ imageId: string
21
+ brushConfig: {
22
+ data: object[]
23
+ isActive: boolean
24
+ isBrushing: boolean
25
+ }
26
+ }
27
+
28
+ export const getInitialState = (configObj: ChartConfig): ChartState => {
8
29
  return {
9
30
  isLoading: true,
10
31
  config: defaults,
@@ -28,28 +49,7 @@ export const getInitialState = (configObj: ChartConfig) => {
28
49
  }
29
50
  }
30
51
 
31
- type State = {
32
- isLoading: boolean
33
- config: ChartConfig
34
- stateData: object[]
35
- colorScale: Function
36
- excludedData: object[]
37
- filteredData: object[]
38
- seriesHighlight: string[]
39
- currentViewport: ViewportSize
40
- dimensions: DimensionsType
41
- container: HTMLElement | null
42
- coveLoadedEventRan: boolean
43
- isDraggingAnnotation: boolean
44
- imageId: string
45
- brushConfig: {
46
- data: object[]
47
- isActive: boolean
48
- isBrushing: boolean
49
- }
50
- }
51
-
52
- export const reducer = (state: State, action: ChartActions) => {
52
+ export const reducer = (state: ChartState, action: ChartActions): ChartState => {
53
53
  switch (action.type) {
54
54
  case 'SET_LOADING':
55
55
  return { ...state, isLoading: action.payload }
@@ -16,6 +16,7 @@ import { Region } from '@cdc/core/types/Region'
16
16
  import { VizFilter } from '@cdc/core/types/VizFilter'
17
17
  import { type Annotation } from '@cdc/core/types/Annotation'
18
18
  import { Version } from '@cdc/core/types/Version'
19
+ import Footnotes from '@cdc/core/types/Footnotes'
19
20
 
20
21
  export type ViewportSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg'
21
22
  export type ChartColumns = Record<string, Column>
@@ -43,7 +44,7 @@ export interface PreliminaryDataItem {
43
44
  iconCode: string
44
45
  label: string
45
46
  lineCode: string
46
- seriesKey: string
47
+ seriesKeys: string[]
47
48
  style: string
48
49
  symbol: string
49
50
  type: 'effect' | 'suppression'
@@ -69,7 +70,7 @@ type DataFormat = {
69
70
  rightSuffix: string
70
71
  roundTo: number
71
72
  suffix: string
72
- onlyShowTopPrefixSuffix?: boolean
73
+ showPiePercent: boolean
73
74
  }
74
75
 
75
76
  type Exclusions = {
@@ -121,7 +122,8 @@ export type AllChartsConfig = {
121
122
  boxplot: BoxPlot
122
123
  brush: {
123
124
  active: boolean
124
- height: number
125
+ data: object[]
126
+ isBrushing: boolean
125
127
  }
126
128
  chartMessage: { noData?: string }
127
129
  color: string
@@ -140,7 +142,8 @@ export type AllChartsConfig = {
140
142
  exclusions: Exclusions
141
143
  filters: VizFilter[]
142
144
  filterBehavior: FilterBehavior
143
- footnotes: string
145
+ legacyFootnotes: string // this footnote functionality should be moved to the Footnotes component
146
+ footnotes: Footnotes
144
147
  forestPlot: ForestPlotConfigSettings
145
148
  formattedData: Object[] & { urlFiltered: boolean }
146
149
  heights: {
@@ -12,7 +12,6 @@ export type TransformedData = {
12
12
 
13
13
  type SharedChartContext = {
14
14
  animatedChart?: boolean
15
- brushConfig: { data: []; isBrushing: boolean; isActive: boolean }
16
15
  capitalize: (value: string) => string
17
16
  clean: Function
18
17
  colorScale?: ColorScale
@@ -31,7 +30,6 @@ type SharedChartContext = {
31
30
  legendIsolateValues?: string[]
32
31
  legendRef?: React.RefObject<HTMLDivElement>
33
32
  parentRef?: React.RefObject<HTMLDivElement>
34
- setBrushConfig: Function
35
33
  setLegendIsolateValues?: Function
36
34
  svgRef?: React.RefObject<SVGSVGElement>
37
35
  }
@@ -1,251 +0,0 @@
1
- import { Brush } from '@visx/brush'
2
- import { Group } from '@visx/group'
3
- import { Text } from '@visx/text'
4
- import { FC, useContext, useEffect, useRef, useState } from 'react'
5
- import ConfigContext from '../ConfigContext'
6
- import { ScaleLinear, ScaleBand } from 'd3-scale'
7
- import { isDateScale } from '@cdc/core/helpers/cove/date'
8
- import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
9
- import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
10
- import { APP_FONT_SIZE } from '@cdc/core/helpers/constants'
11
-
12
- interface Props {
13
- xScaleBrush: ScaleLinear<number, number>
14
- yScale: ScaleBand<string>
15
- xMax: number
16
- yMax: number
17
- }
18
- const ZoomBrush: FC<Props> = props => {
19
- const { tableData, config, parseDate, formatDate, setBrushConfig, dashboardConfig } = useContext(ConfigContext)
20
- const sharedFilters = dashboardConfig?.dashboard?.sharedFilters ?? []
21
- const isDashboardFilters = sharedFilters?.length > 0
22
- const [showTooltip, setShowTooltip] = useState(false)
23
- const [brushKey, setBrushKey] = useState(0)
24
- const brushRef = useRef(null)
25
- const radius = 15
26
-
27
- const [textProps, setTextProps] = useState({
28
- startPosition: 0,
29
- endPosition: 0,
30
- startValue: '',
31
- endValue: '',
32
- xMax: props.xMax
33
- })
34
-
35
- const initialPosition = {
36
- start: { x: 0 },
37
- end: { x: props.xMax }
38
- }
39
- const style = {
40
- fill: '#474747',
41
- stroke: 'blue',
42
- fillOpacity: 0.8,
43
- strokeOpacity: 0,
44
- rx: radius
45
- }
46
-
47
- const onBrushChange = event => {
48
- setShowTooltip(false)
49
- const filteredValues = event?.xValues?.filter(val => val !== undefined)
50
- if (filteredValues?.length === 0) return
51
-
52
- const dataKey = config.xAxis?.dataKey
53
-
54
- const brushedData = tableData.filter(item => filteredValues.includes(item[dataKey]))
55
-
56
- const endValue = filteredValues
57
- .slice()
58
- .reverse()
59
- .find(item => item !== undefined)
60
- const startValue = filteredValues.find(item => item !== undefined)
61
-
62
- const formatIfDate = value => (isDateScale(config.runtime.xAxis) ? formatDate(parseDate(value)) : value)
63
-
64
- setTextProps(prev => ({
65
- ...prev,
66
- startPosition: brushRef.current?.state.start.x,
67
- endPosition: brushRef.current?.state.end.x,
68
- endValue: formatIfDate(endValue),
69
- startValue: formatIfDate(startValue),
70
- xMax: props.xMax
71
- }))
72
-
73
- setBrushConfig(prev => {
74
- return {
75
- ...prev,
76
- isBrushing: brushRef.current?.state.isBrushing,
77
- data: brushedData
78
- }
79
- })
80
- }
81
- // reset brush if brush is off.
82
- useEffect(() => {
83
- if (!config.brush?.active) {
84
- setBrushKey(prevKey => prevKey + 1)
85
- setBrushConfig({
86
- data: [],
87
- isActive: false,
88
- isBrushing: false
89
- })
90
- }
91
- }, [config.brush?.active])
92
-
93
- // reset brush if filters or exclusions are ON each time
94
-
95
- useEffect(() => {
96
- const isFiltersActive = config.filters?.some(filter => filter.active)
97
- const isExclusionsActive = config.exclusions?.active
98
-
99
- if ((isFiltersActive || isExclusionsActive || isDashboardFilters) && config.brush?.active) {
100
- setBrushKey(prevKey => prevKey + 1)
101
- setBrushConfig(prev => {
102
- return {
103
- ...prev,
104
- data: tableData
105
- }
106
- })
107
- }
108
- return () =>
109
- setBrushConfig(prev => {
110
- return {
111
- ...prev,
112
- data: []
113
- }
114
- })
115
- }, [config.filters, config.exclusions, config.brush?.active, isDashboardFilters])
116
-
117
- const calculateTop = (): number => {
118
- const tickRotation = Number(config.xAxis.tickRotation) > 0 ? Number(config.xAxis.tickRotation) : 0
119
- let top = 0
120
- const offSet = 30
121
- if (!config.xAxis.label) {
122
- if (!config.isResponsiveTicks && tickRotation) {
123
- top = Number(tickRotation + config.xAxis.tickWidthMax) / 1.6
124
- }
125
- if (!config.isResponsiveTicks && !tickRotation) {
126
- top = Number(config.xAxis.labelOffset) - offSet
127
- }
128
- if (config.isResponsiveTicks && config.dynamicMarginTop) {
129
- top = Number(config.xAxis.labelOffset + config.xAxis.tickWidthMax / 1.6)
130
- }
131
- if (config.isResponsiveTicks && !config.dynamicMarginTop) {
132
- top = Number(config.xAxis.labelOffset - offSet)
133
- }
134
- }
135
- if (config.xAxis.label) {
136
- if (!config.isResponsiveTicks && tickRotation) {
137
- top = Number(config.xAxis.tickWidthMax + tickRotation) + offSet
138
- }
139
-
140
- if (!config.isResponsiveTicks && !tickRotation) {
141
- top = config.xAxis.labelOffset + offSet
142
- }
143
-
144
- if (config.isResponsiveTicks && !tickRotation) {
145
- top = Number(config.dynamicMarginTop ? config.dynamicMarginTop : config.xAxis.labelOffset) + offSet * 2
146
- }
147
- }
148
-
149
- return top
150
- }
151
- if (!['Line', 'Bar', 'Area Chart', 'Combo'].includes(config.visualizationType)) {
152
- return
153
- }
154
-
155
- return (
156
- <ErrorBoundary component='Brush Chart'>
157
- <Group
158
- onMouseMove={() => {
159
- // show tooltip only once before brush started
160
- if (textProps.startPosition === 0 && (textProps.endPosition === 0 || textProps.endPosition === props.xMax)) {
161
- setShowTooltip(true)
162
- }
163
- }}
164
- onMouseLeave={() => setShowTooltip(false)}
165
- display={config.brush?.active ? 'block' : 'none'}
166
- top={Number(props.yMax) + calculateTop()}
167
- left={Number(config.runtime.yAxis.size)}
168
- pointerEvents='fill'
169
- >
170
- <rect fill='#949494' width={props.xMax} height={config.brush.height} rx={radius} />
171
- <Brush
172
- key={brushKey}
173
- disableDraggingOverlay={true}
174
- renderBrushHandle={props => (
175
- <BrushHandle
176
- left={Number(config.runtime.yAxis.size)}
177
- showTooltip={showTooltip}
178
- pixelDistance={textProps.endPosition - textProps.startPosition}
179
- textProps={textProps}
180
- {...props}
181
- isBrushing={brushRef.current?.state.isBrushing}
182
- />
183
- )}
184
- innerRef={brushRef}
185
- useWindowMoveEvents={true}
186
- selectedBoxStyle={style}
187
- xScale={props.xScaleBrush}
188
- yScale={props.yScale}
189
- width={props.xMax}
190
- resizeTriggerAreas={['left', 'right']}
191
- height={config.brush.height}
192
- handleSize={8}
193
- brushDirection='horizontal'
194
- initialBrushPosition={initialPosition}
195
- onChange={onBrushChange}
196
- />
197
- </Group>
198
- </ErrorBoundary>
199
- )
200
- }
201
-
202
- const BrushHandle = props => {
203
- const { x, isBrushActive, isBrushing, className, textProps, showTooltip, left } = props
204
- const pathWidth = 8
205
- if (!isBrushActive) {
206
- return null
207
- }
208
- // Flip the SVG path horizontally for the left handle
209
- const isLeft = className.includes('left')
210
- const transform = isLeft ? 'scale(-1, 1)' : 'translate(0,0)'
211
- const textAnchor = isLeft ? 'end' : 'start'
212
- const tooltipText = isLeft ? ` Drag edges to focus on a specific segment ` : ''
213
- const textWidth = getTextWidth(tooltipText, `${APP_FONT_SIZE / 1.1}px`)
214
-
215
- return (
216
- <>
217
- {showTooltip && (
218
- <Text
219
- x={(Number(textProps.xMax) - textWidth) / 2}
220
- dy={-12}
221
- pointerEvents='visiblePainted'
222
- fontSize={APP_FONT_SIZE / 1.1}
223
- >
224
- {tooltipText}
225
- </Text>
226
- )}
227
- <Group left={x + pathWidth / 2} top={-2}>
228
- <Text
229
- pointerEvents='visiblePainted'
230
- dominantBaseline='hanging'
231
- x={isLeft ? 55 : -50}
232
- y={25}
233
- verticalAnchor='start'
234
- textAnchor={textAnchor}
235
- fontSize={APP_FONT_SIZE / 1.4}
236
- >
237
- {isLeft ? textProps.startValue : textProps.endValue}
238
- </Text>
239
- <path
240
- cursor='ew-resize'
241
- d='M0.5,10A6,6 0 0 1 6.5,16V14A6,6 0 0 1 0.5,20ZM2.5,18V12M4.5,18V12'
242
- fill={'#297EF1'}
243
- strokeWidth='1'
244
- transform={transform}
245
- ></path>
246
- </Group>
247
- </>
248
- )
249
- }
250
-
251
- export default ZoomBrush