@cdc/chart 4.24.5 → 4.24.7

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 (68) hide show
  1. package/dist/cdcchart.js +39128 -35959
  2. package/examples/feature/annotations/index.json +542 -0
  3. package/examples/xaxis.json +493 -0
  4. package/index.html +5 -4
  5. package/package.json +5 -4
  6. package/src/CdcChart.tsx +104 -64
  7. package/src/_stories/Chart.stories.tsx +18 -171
  8. package/src/_stories/ChartAnnotation.stories.tsx +32 -0
  9. package/src/_stories/_mock/annotation_category_mock.json +473 -0
  10. package/src/_stories/_mock/annotation_date-linear_mock.json +530 -0
  11. package/src/_stories/_mock/annotation_date-time_mock.json +530 -0
  12. package/src/_stories/_mock/line_chart_two_points_new_chart.json +128 -0
  13. package/src/_stories/_mock/line_chart_two_points_regression_test.json +127 -0
  14. package/src/_stories/_mock/lollipop.json +171 -0
  15. package/src/components/Annotations/components/AnnotationDraggable.styles.css +31 -0
  16. package/src/components/Annotations/components/AnnotationDraggable.tsx +154 -0
  17. package/src/components/Annotations/components/AnnotationDropdown.styles.css +14 -0
  18. package/src/components/Annotations/components/AnnotationDropdown.tsx +72 -0
  19. package/src/components/Annotations/components/AnnotationList.styles.css +45 -0
  20. package/src/components/Annotations/components/AnnotationList.tsx +42 -0
  21. package/src/components/Annotations/components/findNearestDatum.ts +138 -0
  22. package/src/components/Annotations/components/helpers/index.tsx +46 -0
  23. package/src/components/Annotations/index.tsx +13 -0
  24. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -1
  25. package/src/components/AreaChart/components/AreaChart.jsx +1 -1
  26. package/src/components/BarChart/components/BarChart.Horizontal.tsx +44 -42
  27. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +1 -2
  28. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +11 -11
  29. package/src/components/BarChart/components/BarChart.Vertical.tsx +50 -22
  30. package/src/components/BarChart/helpers/index.ts +102 -0
  31. package/src/components/EditorPanel/EditorPanel.tsx +232 -98
  32. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +306 -0
  33. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +117 -6
  34. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +2 -3
  35. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +3 -2
  36. package/src/components/EditorPanel/components/Panels/index.tsx +3 -1
  37. package/src/components/EditorPanel/components/panels.scss +4 -0
  38. package/src/components/EditorPanel/editor-panel.scss +19 -0
  39. package/src/components/EditorPanel/useEditorPermissions.js +19 -2
  40. package/src/components/Legend/Legend.Component.tsx +7 -8
  41. package/src/components/Legend/helpers/createFormatLabels.tsx +1 -1
  42. package/src/components/Legend/helpers/index.ts +5 -0
  43. package/src/components/LineChart/LineChartProps.ts +3 -0
  44. package/src/components/LineChart/helpers.ts +21 -7
  45. package/src/components/LineChart/index.tsx +7 -7
  46. package/src/components/LinearChart.jsx +179 -136
  47. package/src/components/PairedBarChart.jsx +9 -9
  48. package/src/components/PieChart/PieChart.tsx +4 -4
  49. package/src/components/Sankey/index.tsx +73 -20
  50. package/src/components/ScatterPlot/ScatterPlot.jsx +22 -8
  51. package/src/components/ZoomBrush.tsx +90 -44
  52. package/src/data/initial-state.js +14 -6
  53. package/src/helpers/handleChartTabbing.ts +8 -0
  54. package/src/helpers/isConvertLineToBarGraph.ts +4 -0
  55. package/src/hooks/{useBarChart.js → useBarChart.ts} +2 -40
  56. package/src/hooks/useColorScale.ts +1 -1
  57. package/src/hooks/useMinMax.ts +8 -3
  58. package/src/hooks/useScales.ts +25 -3
  59. package/src/hooks/useTooltip.tsx +58 -11
  60. package/src/scss/main.scss +21 -14
  61. package/src/types/ChartConfig.ts +50 -3
  62. package/src/types/ChartContext.ts +9 -0
  63. package/tests-examples/helpers/testZeroValue.test.ts +30 -0
  64. package/LICENSE +0 -201
  65. package/src/helpers/filterData.ts +0 -18
  66. package/src/helpers/tests/computeMarginBottom.test.ts +0 -21
  67. /package/src/hooks/{useLegendClasses.js → useLegendClasses.ts} +0 -0
  68. /package/src/hooks/{useReduceData.js → useReduceData.ts} +0 -0
@@ -13,13 +13,16 @@ import 'react-tooltip/dist/react-tooltip.css'
13
13
  import ConfigContext from '@cdc/chart/src/ConfigContext'
14
14
  import { ChartContext } from '../../types/ChartContext'
15
15
  import type { SankeyNode, SankeyProps } from './types'
16
+ import { SankeyChartConfig, AllChartsConfig } from '../../types/ChartConfig'
16
17
 
17
18
  const Sankey = ({ width, height, runtime }: SankeyProps) => {
18
- const DEBUG = true
19
19
  const { config } = useContext<ChartContext>(ConfigContext)
20
20
  const { sankey: sankeyConfig } = config
21
- // !info - changed config.sankey.data here to work with our current upload pattern saved on config.data
22
- const data = config?.data[0]
21
+
22
+ const isSankeyChartConfig = (config: AllChartsConfig | SankeyChartConfig): config is SankeyChartConfig => {
23
+ return config.visualizationType === 'Sankey'
24
+ }
25
+
23
26
  const [largestGroupWidth, setLargestGroupWidth] = useState(0)
24
27
  const groupRefs = useRef([])
25
28
 
@@ -60,6 +63,9 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
60
63
  setLargestGroupWidth(largest)
61
64
  }, [groupRefs, sankeyConfig, window.innerWidth])
62
65
 
66
+ if (!isSankeyChartConfig(config)) return
67
+ const data = config?.data[0]
68
+
63
69
  //Retrieve all the unique values for the Nodes
64
70
  const uniqueNodes = Array.from(new Set(data?.links?.flatMap(link => [link.source, link.target])))
65
71
 
@@ -109,6 +115,8 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
109
115
  }
110
116
 
111
117
  const activeConnection = (id: String) => {
118
+ if (!sankeyData?.nodes) return { sourceNodes: [], activeLinks: [] }
119
+
112
120
  const currentNode = sankeyData.nodes.find(node => node.id === id)
113
121
 
114
122
  const sourceNodes = []
@@ -137,12 +145,12 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
137
145
  return { sourceNodes, activeLinks }
138
146
  }
139
147
 
140
- const tooltipVal = `${(data?.tooltips.find(item => item.node === tooltipID) || {}).value}`
141
- const tooltipSummary = `${(data?.tooltips.find(item => item.node === tooltipID) || {}).summary}`
142
- const tooltipColumn1Label = (data?.tooltips.find(item => item.node === tooltipID) || {}).column1Label
143
- const tooltipColumn2Label = (data?.tooltips.find(item => item.node === tooltipID) || {}).column2Label
144
- const tooltipColumn1 = (data?.tooltips.find(item => item.node === tooltipID) || {}).column1
145
- const tooltipColumn2 = (data?.tooltips.find(item => item.node === tooltipID) || {}).column2
148
+ const tooltipVal = `${(data?.tooltips?.find(item => item.node === tooltipID) || {}).value}`
149
+ const tooltipSummary = `${(data?.tooltips?.find(item => item.node === tooltipID) || {}).summary}`
150
+ const tooltipColumn1Label = (data?.tooltips?.find(item => item.node === tooltipID) || {}).column1Label
151
+ const tooltipColumn2Label = (data?.tooltips?.find(item => item.node === tooltipID) || {}).column2Label
152
+ const tooltipColumn1 = (data?.tooltips?.find(item => item.node === tooltipID) || {}).column1
153
+ const tooltipColumn2 = (data?.tooltips?.find(item => item.node === tooltipID) || {}).column2
146
154
 
147
155
  const ColumnList = ({ columnData }) => {
148
156
  return (
@@ -229,12 +237,27 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
229
237
  */
230
238
  fill={sankeyConfig.nodeFontColor}
231
239
  fontWeight='bold' // font weight
232
- style={{ pointerEvents: 'none' }}
233
240
  className='node-text'
241
+ style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
242
+ onClick={() => handleNodeClick(node.id)}
243
+ data-tooltip-html={data.tooltips && config.enableTooltips ? sankeyToolTip : null}
244
+ data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
234
245
  >
235
246
  {(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextBefore}
236
247
  </Text>
237
- <Text verticalAnchor='end' className={classStyle} x={node.x0! + textPositionHorizontal} y={(node.y1! + node.y0! + 25) / 2} fill={sankeyConfig.storyNodeFontColor || sankeyConfig.nodeFontColor} fontWeight='bold' textAnchor='start' style={{ pointerEvents: 'none' }}>
248
+ <Text
249
+ verticalAnchor='end'
250
+ className={classStyle}
251
+ x={node.x0! + textPositionHorizontal}
252
+ y={(node.y1! + node.y0! + 25) / 2}
253
+ fill={sankeyConfig.storyNodeFontColor || sankeyConfig.nodeFontColor}
254
+ fontWeight='bold'
255
+ textAnchor='start'
256
+ style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
257
+ onClick={() => handleNodeClick(node.id)}
258
+ data-tooltip-html={data.tooltips && config.enableTooltips ? sankeyToolTip : null}
259
+ data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
260
+ >
238
261
  {typeof node.value === 'number' ? node.value.toLocaleString() : node.value}
239
262
  </Text>
240
263
  <Text
@@ -244,20 +267,32 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
244
267
  fill={sankeyConfig.nodeFontColor}
245
268
  fontWeight='bold'
246
269
  textAnchor={sankeyData.nodes.length === i ? 'end' : 'start'}
247
- style={{ pointerEvents: 'none' }}
248
270
  className='node-text'
249
271
  verticalAnchor='end'
272
+ style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
273
+ onClick={() => handleNodeClick(node.id)}
274
+ data-tooltip-html={data.tooltips && config.enableTooltips ? sankeyToolTip : null}
275
+ data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
250
276
  >
251
277
  {(data?.storyNodeText?.find(storyNode => storyNode.StoryNode === node.id) || {}).segmentTextAfter}
252
278
  </Text>
253
279
  </>
254
280
  ) : (
255
281
  <>
256
- <text x={node.x0! + textPositionHorizontal} y={(node.y1! + node.y0!) / 2 + textPositionVertical} dominantBaseline='text-before-edge' fill={sankeyConfig.nodeFontColor} fontWeight='bold' textAnchor='start' style={{ pointerEvents: 'none' }}>
257
- <tspan id={node.id} className='node-id'>
258
- {node.id}
259
- </tspan>
260
- </text>
282
+ <Text
283
+ style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
284
+ onClick={() => handleNodeClick(node.id)}
285
+ data-tooltip-html={data.tooltips && config.enableTooltips ? sankeyToolTip : null}
286
+ data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
287
+ x={node.x0! + textPositionHorizontal}
288
+ y={(node.y1! + node.y0!) / 2 + textPositionVertical}
289
+ dominantBaseline='text-before-edge'
290
+ fill={sankeyConfig.nodeFontColor}
291
+ fontWeight='bold'
292
+ textAnchor='start'
293
+ >
294
+ {node.id}
295
+ </Text>
261
296
  <text
262
297
  x={node.x0! + textPositionHorizontal}
263
298
  /* adding 30 allows the node value to be on the next line underneath the node id */
@@ -267,7 +302,10 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
267
302
  //fontSize={16}
268
303
  fontWeight='bold'
269
304
  textAnchor='start'
270
- style={{ pointerEvents: 'none' }}
305
+ style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
306
+ onClick={() => handleNodeClick(node.id)}
307
+ data-tooltip-html={data.tooltips && config.enableTooltips ? sankeyToolTip : null}
308
+ data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
271
309
  >
272
310
  <tspan className={classStyle}>{sankeyConfig.nodeValueStyle.textBefore + (typeof node.value === 'number' ? node.value.toLocaleString() : node.value) + sankeyConfig.nodeValueStyle.textAfter}</tspan>
273
311
  </text>
@@ -291,7 +329,20 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
291
329
  opacityValue = sankeyConfig.opacity.LinkOpacityInactive
292
330
  }
293
331
 
294
- return <path key={i} d={path!} stroke={strokeColor} fill='none' strokeOpacity={opacityValue} strokeWidth={link.width! + 2} />
332
+ return (
333
+ <path
334
+ key={i}
335
+ d={path!}
336
+ stroke={strokeColor}
337
+ fill='none'
338
+ strokeOpacity={opacityValue}
339
+ strokeWidth={link.width! + 2}
340
+ style={{ pointerEvents: 'auto', cursor: 'pointer' }} // Enable pointer events
341
+ onClick={() => handleNodeClick(link.target.id || null)}
342
+ data-tooltip-html={data.tooltips && config.enableTooltips ? sankeyToolTip : null}
343
+ data-tooltip-id={`cdc-open-viz-tooltip-${runtime.uniqueId}-sankey`}
344
+ />
345
+ )
295
346
  })
296
347
 
297
348
  // max depth - calculates how many nodes deep the chart goes.
@@ -393,7 +444,9 @@ const Sankey = ({ width, height, runtime }: SankeyProps) => {
393
444
  textAnchor='start'
394
445
  style={{ pointerEvents: 'none' }}
395
446
  >
396
- <tspan className={classStyle}>{sankeyConfig.nodeValueStyle.textBefore + (typeof node.value === 'number' ? node.value.toLocaleString() : node.value) + sankeyConfig.nodeValueStyle.textAfter}</tspan>
447
+ <tspan onClick={() => handleNodeClick(node.id)} className={classStyle}>
448
+ {sankeyConfig.nodeValueStyle.textBefore + (typeof node.value === 'number' ? node.value.toLocaleString() : node.value) + sankeyConfig.nodeValueStyle.textAfter}
449
+ </tspan>
397
450
  </text>
398
451
  </>
399
452
  )}
@@ -1,18 +1,32 @@
1
1
  import React, { useContext } from 'react'
2
2
  import ConfigContext from '../../ConfigContext'
3
3
  import { Group } from '@visx/group'
4
+ import { formatNumber as formatColNumber } from '@cdc/core/helpers/cove/number'
4
5
 
5
- const ScatterPlot = ({ xScale, yScale, getXAxisData, getYAxisData }) => {
6
- const { colorScale, transformedData: data, config, formatNumber, seriesHighlight, colorPalettes } = useContext(ConfigContext)
6
+ const ScatterPlot = ({ xScale, yScale }) => {
7
+ const { transformedData: data, config, tableData, formatNumber, seriesHighlight, colorPalettes } = useContext(ConfigContext)
7
8
 
8
9
  // TODO: copied from line chart should probably be a constant somewhere.
9
- let circleRadii = 4.5
10
+ const circleRadii = 4.5
10
11
  const hasMultipleSeries = Object.keys(config.runtime.seriesLabels).length > 1
11
-
12
- const handleTooltip = (item, s) => `<div>
12
+ // tooltips for additional columns
13
+ const additionalColumns = Object.entries(config.columns)
14
+ .filter(([_, value]) => value.tooltips)
15
+ .map(([_, value]) => [
16
+ value.label || value.name,
17
+ value.name,
18
+ {
19
+ addColPrefix: value.prefix,
20
+ addColSuffix: value.suffix,
21
+ addColRoundTo: value.roundToPlace,
22
+ addColCommas: value.commas
23
+ }
24
+ ])
25
+ const handleTooltip = (item, s, dataIndex) => `<div>
13
26
  ${config.legend.showLegendValuesTooltip && config.runtime.seriesLabels && hasMultipleSeries ? `${config.runtime.seriesLabels[s] || ''}<br/>` : ''}
14
27
  ${config.xAxis.label}: ${formatNumber(item[config.xAxis.dataKey], 'bottom')} <br/>
15
- ${config.yAxis.label}: ${formatNumber(item[s], 'left')}
28
+ ${config.yAxis.label}: ${formatNumber(item[s], 'left')}<br/>
29
+ ${additionalColumns.map(([label, name, options]) => `${label} : ${formatColNumber(tableData[dataIndex][name], 'left', false, config, options)}<br/>`).join('')}
16
30
  </div>`
17
31
 
18
32
  return (
@@ -37,9 +51,9 @@ const ScatterPlot = ({ xScale, yScale, getXAxisData, getYAxisData }) => {
37
51
  cx={xScale(item[config.xAxis.dataKey])}
38
52
  cy={yScale(item[s])}
39
53
  fill={displayArea ? seriesColor : 'transparent'}
40
- fillOpacity={transparentArea ? .25 : 1}
54
+ fillOpacity={transparentArea ? 0.25 : 1}
41
55
  style={pointStyles}
42
- data-tooltip-html={handleTooltip(item, s)}
56
+ data-tooltip-html={handleTooltip(item, s, dataIndex)}
43
57
  data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`}
44
58
  tabIndex={-1}
45
59
  />
@@ -6,6 +6,7 @@ import { FC, useContext, useEffect, useRef, useState } from 'react'
6
6
  import ConfigContext from '../ConfigContext'
7
7
  import { ScaleLinear, ScaleBand } from 'd3-scale'
8
8
  import { isDateScale } from '@cdc/core/helpers/cove/date'
9
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
9
10
 
10
11
  interface Props {
11
12
  xScaleBrush: ScaleLinear<number, number>
@@ -14,8 +15,11 @@ interface Props {
14
15
  yMax: number
15
16
  }
16
17
  const ZoomBrush: FC<Props> = props => {
17
- const { tableData, config, parseDate, formatDate, setBrushConfig } = useContext(ConfigContext)
18
+ const { tableData, config, parseDate, formatDate, setBrushConfig, getTextWidth, dashboardConfig } = useContext(ConfigContext)
19
+ const sharedFilters = dashboardConfig?.dashboard?.sharedFilters ?? []
20
+ const isDashboardFilters = sharedFilters?.length > 0
18
21
  const { fontSize } = useBarChart()
22
+ const [showTooltip, setShowTooltip] = useState(false)
19
23
  const [brushKey, setBrushKey] = useState(0)
20
24
  const brushRef = useRef(null)
21
25
  const radius = 15
@@ -24,16 +28,16 @@ const ZoomBrush: FC<Props> = props => {
24
28
  startPosition: 0,
25
29
  endPosition: 0,
26
30
  startValue: '',
27
- endValue: ''
31
+ endValue: '',
32
+ xMax: props.xMax
28
33
  })
29
34
 
30
35
  const initialPosition = {
31
36
  start: { x: 0 },
32
37
  end: { x: props.xMax }
33
38
  }
34
-
35
39
  const style = {
36
- fill: '#AFA6A5 ',
40
+ fill: '#474747',
37
41
  stroke: 'blue',
38
42
  fillOpacity: 0.8,
39
43
  strokeOpacity: 0,
@@ -41,18 +45,19 @@ const ZoomBrush: FC<Props> = props => {
41
45
  }
42
46
 
43
47
  const onBrushChange = event => {
44
- if (!event || !event.xValues) return
45
- const { xValues } = event
48
+ setShowTooltip(false)
49
+ const filteredValues = event?.xValues?.filter(val => val !== undefined)
50
+ if (filteredValues?.length === 0) return
46
51
 
47
52
  const dataKey = config.xAxis?.dataKey
48
53
 
49
- const brushedData = tableData.filter(item => xValues.includes(item[dataKey]))
54
+ const brushedData = tableData.filter(item => filteredValues.includes(item[dataKey]))
50
55
 
51
- const endValue = xValues
56
+ const endValue = filteredValues
52
57
  .slice()
53
58
  .reverse()
54
59
  .find(item => item !== undefined)
55
- const startValue = xValues.find(item => item !== undefined)
60
+ const startValue = filteredValues.find(item => item !== undefined)
56
61
 
57
62
  const formatIfDate = value => (isDateScale(config.runtime.xAxis) ? formatDate(parseDate(value)) : value)
58
63
 
@@ -61,7 +66,8 @@ const ZoomBrush: FC<Props> = props => {
61
66
  startPosition: brushRef.current?.state.start.x,
62
67
  endPosition: brushRef.current?.state.end.x,
63
68
  endValue: formatIfDate(endValue),
64
- startValue: formatIfDate(startValue)
69
+ startValue: formatIfDate(startValue),
70
+ xMax: props.xMax
65
71
  }))
66
72
 
67
73
  setBrushConfig(prev => {
@@ -72,9 +78,9 @@ const ZoomBrush: FC<Props> = props => {
72
78
  }
73
79
  })
74
80
  }
75
-
81
+ // reset brush if brush is off.
76
82
  useEffect(() => {
77
- if (!config.brush.active) {
83
+ if (!config.brush?.active) {
78
84
  setBrushKey(prevKey => prevKey + 1)
79
85
  setBrushConfig({
80
86
  data: [],
@@ -82,10 +88,15 @@ const ZoomBrush: FC<Props> = props => {
82
88
  isBrushing: false
83
89
  })
84
90
  }
85
- }, [config.brush.active])
91
+ }, [config.brush?.active])
92
+
93
+ // reset brush if filters or exclusions are ON each time
86
94
 
87
95
  useEffect(() => {
88
- if (config.filters?.some(filter => filter.active)) {
96
+ const isFiltersActive = config.filters?.some(filter => filter.active)
97
+ const isExclusionsActive = config.exclusions?.active
98
+
99
+ if ((isFiltersActive || isExclusionsActive || isDashboardFilters) && config.brush?.active) {
89
100
  setBrushKey(prevKey => prevKey + 1)
90
101
  setBrushConfig(prev => {
91
102
  return {
@@ -101,12 +112,12 @@ const ZoomBrush: FC<Props> = props => {
101
112
  data: []
102
113
  }
103
114
  })
104
- }, [config.filters])
115
+ }, [config.filters, config.exclusions, config.brush?.active, isDashboardFilters])
105
116
 
106
117
  const calculateTop = (): number => {
107
118
  const tickRotation = Number(config.xAxis.tickRotation) > 0 ? Number(config.xAxis.tickRotation) : 0
108
119
  let top = 0
109
- const offSet = 20
120
+ const offSet = 30
110
121
  if (!config.xAxis.label) {
111
122
  if (!config.isResponsiveTicks && tickRotation) {
112
123
  top = Number(tickRotation + config.xAxis.tickWidthMax) / 1.6
@@ -123,7 +134,7 @@ const ZoomBrush: FC<Props> = props => {
123
134
  }
124
135
  if (config.xAxis.label) {
125
136
  if (!config.isResponsiveTicks && tickRotation) {
126
- top = Number(config.xAxis.tickWidthMax + tickRotation)
137
+ top = Number(config.xAxis.tickWidthMax + tickRotation) + offSet
127
138
  }
128
139
 
129
140
  if (!config.isResponsiveTicks && !tickRotation) {
@@ -131,7 +142,7 @@ const ZoomBrush: FC<Props> = props => {
131
142
  }
132
143
 
133
144
  if (config.isResponsiveTicks && !tickRotation) {
134
- top = Number(config.dynamicMarginTop ? config.dynamicMarginTop : config.xAxis.labelOffset) + offSet
145
+ top = Number(config.dynamicMarginTop ? config.dynamicMarginTop : config.xAxis.labelOffset) + offSet * 2
135
146
  }
136
147
  }
137
148
 
@@ -142,30 +153,56 @@ const ZoomBrush: FC<Props> = props => {
142
153
  }
143
154
 
144
155
  return (
145
- <Group display={config.brush.active ? 'block' : 'none'} top={Number(props.yMax) + calculateTop()} left={Number(config.runtime.yAxis.size)} pointerEvents='fill'>
146
- <rect fill='#F7F7F7 ' width={props.xMax} height={config.brush.height} rx={radius} />
147
- <Brush
148
- key={brushKey}
149
- renderBrushHandle={props => <BrushHandle textProps={textProps} fontSize={fontSize[config.fontSize]} {...props} isBrushing={brushRef.current?.state.isBrushing} />}
150
- innerRef={brushRef}
151
- useWindowMoveEvents={true}
152
- selectedBoxStyle={style}
153
- xScale={props.xScaleBrush}
154
- yScale={props.yScale}
155
- width={props.xMax}
156
- resizeTriggerAreas={['left', 'right']}
157
- height={config.brush.height}
158
- handleSize={8}
159
- brushDirection='horizontal'
160
- initialBrushPosition={initialPosition}
161
- onChange={onBrushChange}
162
- />
163
- </Group>
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
+ getTextWidth={getTextWidth}
179
+ pixelDistance={textProps.endPosition - textProps.startPosition}
180
+ textProps={textProps}
181
+ fontSize={fontSize[config.fontSize]}
182
+ {...props}
183
+ isBrushing={brushRef.current?.state.isBrushing}
184
+ />
185
+ )}
186
+ innerRef={brushRef}
187
+ useWindowMoveEvents={true}
188
+ selectedBoxStyle={style}
189
+ xScale={props.xScaleBrush}
190
+ yScale={props.yScale}
191
+ width={props.xMax}
192
+ resizeTriggerAreas={['left', 'right']}
193
+ height={config.brush.height}
194
+ handleSize={8}
195
+ brushDirection='horizontal'
196
+ initialBrushPosition={initialPosition}
197
+ onChange={onBrushChange}
198
+ />
199
+ </Group>
200
+ </ErrorBoundary>
164
201
  )
165
202
  }
166
203
 
167
204
  const BrushHandle = props => {
168
- const { x, isBrushActive, isBrushing, className, textProps } = props
205
+ const { x, isBrushActive, isBrushing, className, textProps, fontSize, showTooltip, left, getTextWidth } = props
169
206
  const pathWidth = 8
170
207
  if (!isBrushActive) {
171
208
  return null
@@ -174,14 +211,23 @@ const BrushHandle = props => {
174
211
  const isLeft = className.includes('left')
175
212
  const transform = isLeft ? 'scale(-1, 1)' : 'translate(0,0)'
176
213
  const textAnchor = isLeft ? 'end' : 'start'
214
+ const tooltipText = isLeft ? ` Drag edges to focus on a specific segment ` : ''
215
+ const textWidth = getTextWidth(tooltipText, `normal ${fontSize / 1.1}px sans-serif`)
177
216
 
178
217
  return (
179
- <Group left={x + pathWidth / 2} top={-2}>
180
- <Text pointerEvents='visiblePainted' dominantBaseline='hanging' x={0} verticalAnchor='start' textAnchor={textAnchor} fontSize={props.fontSize / 1.4} dy={10} y={15}>
181
- {isLeft ? textProps.startValue : textProps.endValue}
182
- </Text>
183
- <path cursor='ew-resize' d='M0.5,10A6,6 0 0 1 6.5,16V14A6,6 0 0 1 0.5,20ZM2.5,18V12M4.5,18V12' fill={!isBrushing ? '#000' : '#297EF1'} strokeWidth='1' transform={transform}></path>
184
- </Group>
218
+ <>
219
+ {showTooltip && (
220
+ <Text x={(Number(textProps.xMax) - textWidth) / 2} dy={-12} pointerEvents='visiblePainted' fontSize={fontSize / 1.1}>
221
+ {tooltipText}
222
+ </Text>
223
+ )}
224
+ <Group left={x + pathWidth / 2} top={-2}>
225
+ <Text pointerEvents='visiblePainted' dominantBaseline='hanging' x={isLeft ? 55 : -50} y={25} verticalAnchor='start' textAnchor={textAnchor} fontSize={fontSize / 1.4}>
226
+ {isLeft ? textProps.startValue : textProps.endValue}
227
+ </Text>
228
+ <path cursor='ew-resize' d='M0.5,10A6,6 0 0 1 6.5,16V14A6,6 0 0 1 0.5,20ZM2.5,18V12M4.5,18V12' fill={'#297EF1'} strokeWidth='1' transform={transform}></path>
229
+ </Group>
230
+ </>
185
231
  )
186
232
  }
187
233
 
@@ -1,4 +1,6 @@
1
1
  export default {
2
+ annotations: [],
3
+ allowLineToBarGraph: undefined,
2
4
  type: 'chart',
3
5
  debugSvg: false,
4
6
  chartMessage: {
@@ -22,7 +24,11 @@ export default {
22
24
  tipRounding: 'top',
23
25
  isResponsiveTicks: false,
24
26
  general: {
25
- showDownloadButton: false
27
+ annotationDropdownText: 'Annotations',
28
+ showDownloadButton: false,
29
+ showMissingDataLabel: true,
30
+ showSuppressedSymbol: true,
31
+ showZeroValueDataLabel: true
26
32
  },
27
33
  padding: {
28
34
  left: 5,
@@ -53,7 +59,9 @@ export default {
53
59
  axisPadding: 0,
54
60
  scalePadding: 10,
55
61
  tickRotation: 0,
56
- anchors: []
62
+ anchors: [],
63
+ shoMissingDataLabel: true,
64
+ showMissingDataLine: true
57
65
  },
58
66
  boxplot: {
59
67
  plots: [],
@@ -116,9 +124,7 @@ export default {
116
124
  labelOffset: 65,
117
125
  axisPadding: 200,
118
126
  target: 0,
119
- maxTickRotation: 0,
120
- showSuppressedSymbol: true,
121
- showSuppressedLine: true
127
+ maxTickRotation: 0
122
128
  },
123
129
  table: {
124
130
  label: 'Data Table',
@@ -131,7 +137,9 @@ export default {
131
137
  indexLabel: '',
132
138
  download: false,
133
139
  showVertical: true,
134
- dateDisplayFormat: ''
140
+ dateDisplayFormat: '',
141
+ showMissingDataLabel: true,
142
+ showSuppressedSymbol: true
135
143
  },
136
144
  orientation: 'vertical',
137
145
  color: 'pinkpurple',
@@ -0,0 +1,8 @@
1
+ import { ChartConfig } from './../types/ChartConfig'
2
+
3
+ export const handleChartTabbing = (config: ChartConfig, legendId: string) => {
4
+ if (!config) return `dataTableSection`
5
+ if (!config.legend?.hide) return legendId
6
+ if (config?.title) return `dataTableSection__${config.title.replace(/\s/g, '')}`
7
+ return `dataTableSection`
8
+ }
@@ -0,0 +1,4 @@
1
+ export const isConvertLineToBarGraph = (visualizationType, filteredData, allowLineToBarGraph) => {
2
+ const convertLineToBarGraph = visualizationType === 'Line' && filteredData?.length < 3 && allowLineToBarGraph ? true : false
3
+ return convertLineToBarGraph
4
+ }
@@ -29,7 +29,7 @@ export const useBarChart = () => {
29
29
  updateConfig({
30
30
  ...config,
31
31
  yAxis: {
32
- ...config,
32
+ ...config.yAxis,
33
33
  labelPlacement: 'Below Bar'
34
34
  }
35
35
  })
@@ -210,43 +210,6 @@ export const useBarChart = () => {
210
210
  if (config.legend.highlightOnHover && config.legend.behavior === 'highlight') setSeriesHighlight([])
211
211
  }
212
212
 
213
- const composeSuppressionBars = ({ bar }) => {
214
- const suppresedBarHeight = config.xAxis.showSuppressedLine ? 3 : 0
215
- const ASTERISK = 'Asterisk'
216
- const getIconPadding = symbol => (String(symbol).includes(ASTERISK) ? -5 : -suppresedBarHeight * 3)
217
- const getVerticalAnchor = symbol => {
218
- return String(symbol).includes(ASTERISK) ? 'middle' : 'end'
219
- }
220
- const getIconSize = (symbol, barWidth) => {
221
- switch (symbol) {
222
- case ASTERISK:
223
- return barWidth * 1.2
224
- case 'Double ' + ASTERISK:
225
- return barWidth
226
- default:
227
- return barWidth / 1.5
228
- }
229
- }
230
-
231
- function shouldSuppressBar() {
232
- const isSuppressed = config.preliminaryData.some(pd => {
233
- const selectedSuppressionColumn = !pd.column || pd.column === bar.key
234
- const isValueMatch = String(pd.value) === String(bar.value) && pd.value !== '' && pd.type === 'suppression'
235
-
236
- return isValueMatch && selectedSuppressionColumn
237
- })
238
-
239
- return isSuppressed && config.xAxis.showSuppressedSymbol
240
- }
241
-
242
- return {
243
- suppresedBarHeight,
244
- getIconSize,
245
- getIconPadding,
246
- getVerticalAnchor,
247
- isSuppressed: shouldSuppressBar()
248
- }
249
- }
250
213
  return {
251
214
  isHorizontal,
252
215
  barBorderWidth,
@@ -273,7 +236,6 @@ export const useBarChart = () => {
273
236
  hoveredBar,
274
237
  setHoveredBar,
275
238
  onMouseOverBar,
276
- onMouseLeaveBar,
277
- composeSuppressionBars
239
+ onMouseLeaveBar
278
240
  }
279
241
  }
@@ -30,7 +30,7 @@ const useColorScale = () => {
30
30
  })
31
31
  }
32
32
  if (visualizationType === 'Bar' && visualizationSubType === 'regular' && series?.length === 1 && legend?.colorCode) {
33
- const set = new Set(data.map(d => d[legend.colorCode]))
33
+ const set = new Set(data?.map(d => d[legend.colorCode]))
34
34
  colorScale = scaleOrdinal({
35
35
  domain: [...set],
36
36
  range: generatePalette([...set].length)
@@ -1,5 +1,6 @@
1
1
  import { ChartConfig } from '../types/ChartConfig'
2
2
  import _ from 'lodash'
3
+ import { isConvertLineToBarGraph } from '../helpers/isConvertLineToBarGraph'
3
4
 
4
5
  type UseMinMaxProps = {
5
6
  /** config - standard chart config */
@@ -30,6 +31,10 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
30
31
  return { min, max }
31
32
  }
32
33
 
34
+ const checkLineToBarGraph = () => {
35
+ return isConvertLineToBarGraph(config.visualizationType, data, config.allowLineToBarGraph)
36
+ }
37
+
33
38
  const { visualizationType, series } = config
34
39
  const { max: enteredMaxValue, min: enteredMinValue } = config.runtime.yAxis
35
40
  const minRequiredCIPadding = 1.15 // regardless of Editor if CI data, there must be 10% padding added
@@ -126,10 +131,10 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
126
131
  }
127
132
 
128
133
  // this should not apply to bar charts if there is negative CI data
129
- if ((visualizationType === 'Bar' || (visualizationType === 'Combo' && !isAllLine)) && min > 0) {
134
+ if ((visualizationType === 'Bar' || checkLineToBarGraph() || (visualizationType === 'Combo' && !isAllLine)) && min > 0) {
130
135
  min = 0
131
136
  }
132
- if ((config.visualizationType === 'Bar' || (config.visualizationType === 'Combo' && !isAllLine)) && min < 0) {
137
+ if ((config.visualizationType === 'Bar' || checkLineToBarGraph() || (config.visualizationType === 'Combo' && !isAllLine)) && min < 0) {
133
138
  min = min * 1.1
134
139
  }
135
140
 
@@ -148,7 +153,7 @@ const useMinMax = ({ config, minValue, maxValue, existPositiveValue, data, isAll
148
153
  min = enteredMinValue && isMinValid ? enteredMinValue : 0
149
154
  }
150
155
 
151
- if (config.visualizationType === 'Line') {
156
+ if (config.visualizationType === 'Line' && !checkLineToBarGraph()) {
152
157
  const isMinValid = config.useLogScale ? enteredMinValue >= 0 && enteredMinValue < minValue : enteredMinValue < minValue
153
158
  // update minValue for (0) Suppression points
154
159
  const suppressedMinValue = tableData?.some((dataItem, index) => {