@cdc/chart 4.23.1 → 4.23.3

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 (74) hide show
  1. package/dist/cdcchart.js +56289 -702
  2. package/examples/Barchart_with_negative.json +34 -0
  3. package/examples/area-chart.json +187 -0
  4. package/examples/big-small-test-bar.json +328 -0
  5. package/examples/big-small-test-line.json +328 -0
  6. package/examples/big-small-test-negative.json +328 -0
  7. package/examples/box-plot.json +1 -2
  8. package/examples/dynamic-legends.json +1 -1
  9. package/examples/example-bar-chart-nonnumeric.json +36 -0
  10. package/examples/example-bar-chart.json +36 -0
  11. package/examples/example-combo-bar-nonnumeric.json +105 -0
  12. package/examples/example-sparkline.json +76 -0
  13. package/examples/gallery/bar-chart-horizontal/horizontal-bar-chart.json +31 -172
  14. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-categorical.json +1 -1
  15. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-confidence.json +1 -0
  16. package/examples/gallery/bar-chart-vertical/vertical-bar-chart-with-confidence.json +96 -14
  17. package/examples/gallery/bar-chart-vertical/vertical-bar-chart.json +2 -2
  18. package/examples/gallery/line/line.json +1 -0
  19. package/examples/gallery/paired-bar/paired-bar-chart.json +65 -13
  20. package/examples/horizontal-chart-max-increase.json +38 -0
  21. package/examples/line-chart-max-increase.json +32 -0
  22. package/examples/line-chart-nonnumeric.json +32 -0
  23. package/examples/line-chart.json +21 -63
  24. package/examples/newdata.json +1 -1
  25. package/examples/planet-combo-example-config.json +143 -20
  26. package/examples/planet-deviation-config.json +168 -0
  27. package/examples/planet-deviation-data.json +38 -0
  28. package/examples/planet-example-config.json +139 -20
  29. package/examples/planet-example-data-max-increase.json +56 -0
  30. package/examples/planet-example-data-nonnumeric.json +56 -0
  31. package/examples/planet-example-data.json +9 -9
  32. package/examples/planet-pie-example-config-nonnumeric.json +30 -0
  33. package/examples/scatterplot-continuous.csv +17 -0
  34. package/examples/scatterplot.json +136 -0
  35. package/examples/sparkline-chart-nonnumeric.json +76 -0
  36. package/examples/stacked-vertical-bar-example-negative.json +154 -0
  37. package/examples/stacked-vertical-bar-example-nonnumerics.json +154 -0
  38. package/index.html +91 -0
  39. package/package.json +33 -24
  40. package/src/{CdcChart.tsx → CdcChart.jsx} +196 -124
  41. package/src/components/AreaChart.jsx +198 -0
  42. package/src/components/{BarChart.tsx → BarChart.jsx} +154 -122
  43. package/src/components/BoxPlot.jsx +101 -0
  44. package/src/components/{DataTable.tsx → DataTable.jsx} +109 -28
  45. package/src/components/DeviationBar.jsx +191 -0
  46. package/src/components/{EditorPanel.js → EditorPanel.jsx} +676 -157
  47. package/src/components/{Filters.js → Filters.jsx} +6 -11
  48. package/src/components/Legend.jsx +316 -0
  49. package/src/components/{LineChart.tsx → LineChart.jsx} +22 -26
  50. package/src/components/{LinearChart.tsx → LinearChart.jsx} +214 -91
  51. package/src/components/{PairedBarChart.tsx → PairedBarChart.jsx} +44 -78
  52. package/src/components/{PieChart.tsx → PieChart.jsx} +26 -44
  53. package/src/components/ScatterPlot.jsx +51 -0
  54. package/src/components/SparkLine.jsx +218 -0
  55. package/src/components/{useIntersectionObserver.tsx → useIntersectionObserver.jsx} +2 -2
  56. package/src/data/initial-state.js +51 -5
  57. package/src/hooks/useColorPalette.js +68 -0
  58. package/src/hooks/{useReduceData.ts → useReduceData.js} +26 -16
  59. package/src/hooks/useRightAxis.js +3 -1
  60. package/src/index.jsx +16 -0
  61. package/src/scss/DataTable.scss +22 -0
  62. package/src/scss/editor-panel.scss +5 -0
  63. package/src/scss/main.scss +30 -10
  64. package/src/test/CdcChart.test.jsx +6 -0
  65. package/vite.config.js +4 -0
  66. package/dist/495.js +0 -3
  67. package/dist/703.js +0 -1
  68. package/src/components/BoxPlot.js +0 -92
  69. package/src/components/Legend.js +0 -291
  70. package/src/components/SparkLine.js +0 -185
  71. package/src/hooks/useColorPalette.ts +0 -76
  72. package/src/index.html +0 -67
  73. package/src/index.tsx +0 -18
  74. /package/src/{context.tsx → ConfigContext.jsx} +0 -0
@@ -1,26 +1,24 @@
1
- import React, { useContext, useEffect, useState, useMemo, memo, Fragment } from 'react'
1
+ import React, { useContext, useEffect, useState, useMemo } from 'react'
2
2
  import { useTable, useSortBy, useResizeColumns, useBlockLayout } from 'react-table'
3
3
  import Papa from 'papaparse'
4
4
  import { Base64 } from 'js-base64'
5
5
 
6
6
  import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
7
7
  import LegendCircle from '@cdc/core/components/LegendCircle'
8
+ import Icon from '@cdc/core/components/ui/Icon'
8
9
 
9
- import Context from '../context'
10
+ import ConfigContext from '../ConfigContext'
10
11
 
11
- import CoveMediaControls from '@cdc/core/helpers/CoveMediaControls'
12
+ import CoveMediaControls from '@cdc/core/components/CoveMediaControls'
12
13
 
13
14
  export default function DataTable() {
14
- const { rawData, transformedData: data, config, colorScale, parseDate, formatDate, formatNumber: numberFormatter, colorPalettes, imageId } = useContext<any>(Context)
15
-
16
- // Debugging.
17
- // if (config.visualizationType === 'Box Plot') return null
15
+ const { rawData, transformedData: data, config, colorScale, parseDate, formatDate, formatNumber: numberFormatter, colorPalettes } = useContext(ConfigContext)
18
16
 
19
17
  const section = config.orientation === 'horizontal' ? 'yAxis' : 'xAxis'
20
- const [tableExpanded, setTableExpanded] = useState<boolean>(config.table.expanded)
18
+ const [tableExpanded, setTableExpanded] = useState(config.table.expanded)
21
19
  const [accessibilityLabel, setAccessibilityLabel] = useState('')
22
20
 
23
- const DownloadButton = ({ data }: any, type) => {
21
+ const DownloadButton = ({ data }, type) => {
24
22
  const fileName = `${config.title.substring(0, 50)}.csv`
25
23
 
26
24
  const csvData = Papa.unparse(data)
@@ -55,17 +53,51 @@ export default function DataTable() {
55
53
  const newTableColumns =
56
54
  config.visualizationType === 'Pie'
57
55
  ? []
56
+ : config.visualizationType === 'Box Plot'
57
+ ? [
58
+ {
59
+ Header: 'Measures',
60
+ Cell: props => {
61
+ const resolveName = () => {
62
+ let {
63
+ boxplot: { labels }
64
+ } = config
65
+ const columnLookup = {
66
+ columnMean: labels.mean,
67
+ columnMax: labels.maximum,
68
+ columnMin: labels.minimum,
69
+ columnIqr: labels.iqr,
70
+ columnCategory: 'Category',
71
+ columnMedian: labels.median,
72
+ columnFirstQuartile: labels.q1,
73
+ columnThirdQuartile: labels.q3,
74
+ columnOutliers: labels.outliers,
75
+ values: labels.values,
76
+ columnTotal: labels.total,
77
+ columnSd: 'Standard Deviation',
78
+ nonOutlierValues: 'Non Outliers'
79
+ }
80
+
81
+ let resolvedName = columnLookup[props.row.original[0]]
82
+
83
+ return resolvedName
84
+ }
85
+
86
+ return resolveName()
87
+ }
88
+ }
89
+ ]
58
90
  : [
59
91
  {
60
92
  Header: '',
61
93
  Cell: ({ row }) => {
62
94
  const seriesLabel = config.runtime.seriesLabels ? config.runtime.seriesLabels[row.original] : row.original
63
95
  return (
64
- <Fragment>
96
+ <>
65
97
  {config.visualizationType !== 'Pie' && (
66
98
  <LegendCircle
67
99
  fill={
68
- // non dynamic leged
100
+ // non-dynamic leged
69
101
  !config.legend.dynamicLegend
70
102
  ? colorScale(seriesLabel)
71
103
  : // dynamic legend
@@ -77,30 +109,72 @@ export default function DataTable() {
77
109
  />
78
110
  )}
79
111
  <span>{seriesLabel}</span>
80
- </Fragment>
112
+ </>
81
113
  )
82
114
  },
83
115
  id: 'series-label'
84
116
  }
85
117
  ]
118
+ if (config.visualizationType !== 'Box Plot') {
119
+ data.forEach((d, index) => {
120
+ const resolveTableHeader = () => {
121
+ if (config.runtime[section].type === 'date') return formatDate(parseDate(d[config.runtime.originalXAxis.dataKey]))
122
+ if (config.runtime[section].type === 'continuous') return numberFormatter(d[config.runtime.originalXAxis.dataKey], 'bottom')
123
+ return d[config.runtime.originalXAxis.dataKey]
124
+ }
125
+ const newCol = {
126
+ Header: resolveTableHeader(),
127
+ Cell: ({ row }) => {
128
+ return <>{numberFormatter(d[row.original], 'left')}</>
129
+ },
130
+ id: `${d[config.runtime.originalXAxis.dataKey]}--${index}`,
131
+ canSort: true
132
+ }
86
133
 
87
- data.forEach((d, index) => {
88
- const newCol = {
89
- Header: config.runtime[section].type === 'date' ? formatDate(parseDate(d[config.runtime.originalXAxis.dataKey])) : d[config.runtime.originalXAxis.dataKey],
90
- Cell: ({ row }) => {
91
- return <>{numberFormatter(d[row.original])}</>
92
- },
93
- id: `${d[config.runtime.originalXAxis.dataKey]}--${index}`,
94
- canSort: true
95
- }
134
+ newTableColumns.push(newCol)
135
+ })
136
+ }
137
+
138
+ if (config.visualizationType === 'Box Plot') {
139
+ config.boxplot.tableData.map((plot, index) => {
140
+ const newCol = {
141
+ Header: plot.columnCategory,
142
+ Cell: props => {
143
+ let resolveCell = () => {
144
+ if (Number(props.row.id) === 0) return true
145
+ if (Number(props.row.id) === 1) return plot.columnMax
146
+ if (Number(props.row.id) === 2) return plot.columnThirdQuartile
147
+ if (Number(props.row.id) === 3) return plot.columnMedian
148
+ if (Number(props.row.id) === 4) return plot.columnFirstQuartile
149
+ if (Number(props.row.id) === 5) return plot.columnMin
150
+ if (Number(props.row.id) === 6) return plot.columnTotal
151
+ if (Number(props.row.id) === 7) return plot.columnSd
152
+ if (Number(props.row.id) === 8) return plot.columnMean
153
+ if (Number(props.row.id) === 9) return plot.columnOutliers.length > 0 ? plot.columnOutliers.toString() : '-'
154
+ if (Number(props.row.id) === 10) return plot.values.length > 0 ? plot.values.toString() : '-'
155
+ return <p>-</p>
156
+ }
157
+ return resolveCell()
158
+ },
159
+ id: `${index}`,
160
+ canSort: false
161
+ }
96
162
 
97
- newTableColumns.push(newCol)
98
- })
163
+ return newTableColumns.push(newCol)
164
+ })
165
+ }
99
166
 
100
167
  return newTableColumns
101
- }, [config, colorScale])
168
+ }, [config, colorScale]) // eslint-disable-line
102
169
 
103
- const tableData = useMemo(() => (config.visualizationType === 'Pie' ? [config.yAxis.dataKey] : config.runtime.seriesKeys), [config.runtime.seriesKeys])
170
+ // prettier-ignore
171
+ const tableData = useMemo(() => (
172
+ config.visualizationType === 'Pie'
173
+ ? [config.yAxis.dataKey]
174
+ : config.visualizationType === 'Box Plot'
175
+ ? Object.entries(config.boxplot.tableData[0])
176
+ : config.runtime.seriesKeys),
177
+ [config.runtime.seriesKeys]) // eslint-disable-line
104
178
 
105
179
  // Change accessibility label depending on expanded status
106
180
  useEffect(() => {
@@ -125,8 +199,13 @@ export default function DataTable() {
125
199
  }),
126
200
  []
127
201
  )
128
-
129
202
  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable({ columns: tableColumns, data: tableData, defaultColumn }, useSortBy, useBlockLayout, useResizeColumns)
203
+
204
+ // sort continuous x axis scaling for data tables, ie. xAxis should read 1,2,3,4,5
205
+ if (config.xAxis.type === 'continuous' && headerGroups) {
206
+ data.sort((a, b) => a[config.xAxis.dataKey] - b[config.xAxis.dataKey])
207
+ }
208
+
130
209
  return (
131
210
  <ErrorBoundary component='DataTable'>
132
211
  <CoveMediaControls.Section classes={['download-links']}>
@@ -148,6 +227,7 @@ export default function DataTable() {
148
227
  }
149
228
  }}
150
229
  >
230
+ <Icon display={tableExpanded ? 'minus' : 'plus'} base />
151
231
  {config.table.label}
152
232
  </div>
153
233
  <div className='table-container' hidden={!tableExpanded} style={{ maxHeight: config.table.limitHeight && `${config.table.height}px`, overflowY: 'scroll' }}>
@@ -182,7 +262,7 @@ export default function DataTable() {
182
262
  {rows.map((row, index) => {
183
263
  prepareRow(row)
184
264
  return (
185
- <tr {...row.getRowProps()} key={`tbody__tr-${index}`}>
265
+ <tr {...row.getRowProps()} key={`tbody__tr-${index}`} className={`row-${String(config.visualizationType).replace(' ', '-')}--${index}`}>
186
266
  {row.cells.map((cell, index) => {
187
267
  return (
188
268
  <td tabIndex='0' {...cell.getCellProps()} key={`tbody__tr__td-${index}`} role='gridcell'>
@@ -195,7 +275,7 @@ export default function DataTable() {
195
275
  })}
196
276
  </tbody>
197
277
  </table>
198
- {config.regions && config.regions.length > 0 ? (
278
+ {config.regions && config.regions.length > 0 && !config.visualizationType === 'Box Plot' ? (
199
279
  <table className='region-table data-table'>
200
280
  <caption className='visually-hidden'>Table of the highlighted regions in the visualization</caption>
201
281
  <thead>
@@ -207,6 +287,7 @@ export default function DataTable() {
207
287
  </thead>
208
288
  <tbody>
209
289
  {config.regions.map((region, index) => {
290
+ if (config.visualizationType === 'Box Plot') return false
210
291
  if (!Object.keys(region).includes('from') || !Object.keys(region).includes('to')) return null
211
292
 
212
293
  return (
@@ -0,0 +1,191 @@
1
+ import { Line } from '@visx/shape'
2
+ import { Group } from '@visx/group'
3
+ import { useContext, useEffect } from 'react'
4
+ import ConfigContext from '../ConfigContext'
5
+ import { Text } from '@visx/text'
6
+ import ErrorBoundary from '@cdc/core/components/ErrorBoundary'
7
+ import chroma from 'chroma-js'
8
+
9
+ // create function to position text based where bar is located left/or right
10
+ function getTextProps(isLollipopChart, textFits, lollipopShapeSize, fill) {
11
+ if (isLollipopChart) {
12
+ return {
13
+ right: {
14
+ textAnchor: 'start',
15
+ dx: lollipopShapeSize + 6,
16
+ fill: '#000000'
17
+ },
18
+ left: {
19
+ textAnchor: 'end',
20
+ dx: -lollipopShapeSize,
21
+ fill: '#000000'
22
+ }
23
+ }
24
+ } else {
25
+ return {
26
+ right: {
27
+ textAnchor: textFits ? 'end' : 'start',
28
+ dx: textFits ? -6 : 6,
29
+ fill: textFits ? fill : '#000000'
30
+ },
31
+ left: {
32
+ textAnchor: textFits ? 'start' : 'end',
33
+ dx: textFits ? 6 : -6,
34
+ fill: textFits ? fill : '#000000'
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ export function DeviationBar({ height, xScale }) {
41
+ const { transformedData: data, config, formatNumber, twoColorPalette, getTextWidth, updateConfig, parseDate, formatDate } = useContext(ConfigContext)
42
+
43
+ if (!config || config?.series?.length !== 1 || config.orientation !== 'horizontal') return
44
+
45
+ const { barStyle, tipRounding, roundingStyle, twoColor } = config
46
+
47
+ const radius = roundingStyle === 'standard' ? '8px' : roundingStyle === 'shallow' ? '5px' : roundingStyle === 'finger' ? '15px' : '0px'
48
+ const fontSize = { small: 16, medium: 18, large: 20 }
49
+ const isRounded = config.barStyle === 'rounded'
50
+ const target = Number(config.xAxis.target)
51
+ const seriesKey = config.series[0].dataKey
52
+ const maxVal = Number(xScale.domain()[1])
53
+ const hasNegativeValues = data.some(d => d[seriesKey] < 0)
54
+ const shouldShowTargetLine = hasNegativeValues || target > 0 || xScale.domain()[0] < 0
55
+ const borderWidth = config.barHasBorder === 'true' ? 1 : 0
56
+ const lollipopBarHeight = config.lollipopSize === 'large' ? 7 : config.lollipopSize === 'medium' ? 6 : 5
57
+ const lollipopShapeSize = config.lollipopSize === 'large' ? 14 : config.lollipopSize === 'medium' ? 12 : 10
58
+
59
+ const targetX = Math.max(xScale(0), Math.min(xScale(target), xScale(maxVal * 1.05)))
60
+
61
+ const applyRadius = barPosition => {
62
+ if (barPosition === undefined || barPosition === null || barStyle !== 'rounded') return
63
+ let style = {}
64
+ if (barPosition === 'left') {
65
+ style = { borderRadius: `${radius} 0 0 ${radius}` }
66
+ }
67
+
68
+ if (barPosition === 'right') {
69
+ style = { borderRadius: `0 ${radius} ${radius} 0` }
70
+ }
71
+ if (tipRounding === 'full') {
72
+ style = { borderRadius: radius }
73
+ }
74
+
75
+ return style
76
+ }
77
+
78
+ const targetLabel = {
79
+ calculate: function () {
80
+ const firstBarValue = data[0][seriesKey]
81
+ const barPosition = firstBarValue < target ? 'left' : 'right'
82
+ const label = `${config.xAxis.targetLabel} ${formatNumber(config.xAxis.target || 0, 'left')}`
83
+ const labelWidth = getTextWidth(label, `bold ${fontSize[config.fontSize]}px sans-serif`)
84
+ let labelY = config.isLollipopChart ? lollipopBarHeight / 2 : Number(config.barHeight) / 2
85
+ let paddingX = 0
86
+ let labelX = 0
87
+ let showLabel = false
88
+
89
+ if (barPosition === 'right') {
90
+ paddingX = -10
91
+ showLabel = labelWidth - paddingX < targetX
92
+ labelX = targetX - labelWidth
93
+ }
94
+
95
+ if (barPosition === 'left') {
96
+ paddingX = 10
97
+ showLabel = xScale(maxVal) - targetX > labelWidth + paddingX
98
+ labelX = targetX
99
+ }
100
+
101
+ this.text = label
102
+ this.y = labelY
103
+ this.x = labelX
104
+ this.padding = paddingX
105
+ this.showLabel = config.xAxis.showTargetLabel ? showLabel : false
106
+ }
107
+ }
108
+ targetLabel.calculate()
109
+
110
+ useEffect(() => {
111
+ if (config.barStyle === 'lollipop' && !config.isLollipopChart) {
112
+ updateConfig({ ...config, isLollipopChart: true })
113
+ }
114
+ if (isRounded || config.barStyle === 'flat') {
115
+ updateConfig({ ...config, isLollipopChart: false })
116
+ }
117
+ }, [config.barStyle])
118
+
119
+ return (
120
+ <ErrorBoundary component='Deviation Bar'>
121
+ <Group left={Number(config.xAxis.size)}>
122
+ {data.map((d, index) => {
123
+ const barValue = Number(d[seriesKey])
124
+ const barHeight = config.isLollipopChart ? lollipopBarHeight : Number(config.barHeight)
125
+ const barSpace = Number(config.barSpace) || 15
126
+ const barWidth = Math.abs(xScale(barValue) - targetX)
127
+ const barBaseX = xScale(barValue)
128
+ const barX = barValue > target ? targetX : barBaseX
129
+ const barPosition = barValue < target ? 'left' : 'right'
130
+
131
+ // update bar Y to give dynamic Y when user applyes BarSpace
132
+ let barY = 0
133
+ barY = index !== 0 ? (barSpace + barHeight + borderWidth) * index : barY
134
+ const totalheight = (barSpace + barHeight + borderWidth) * data.length
135
+ config.heights.horizontal = totalheight
136
+
137
+ // text,labels postiions
138
+ const textWidth = getTextWidth(formatNumber(barValue, 'left'), `normal ${fontSize[config.fontSize]}px sans-serif`)
139
+ const textFits = textWidth < barWidth - 6
140
+ const textX = barBaseX
141
+ const textY = barY + barHeight / 2
142
+
143
+ // lollipop shapes
144
+ const circleX = barBaseX
145
+ const circleY = barY + barHeight / 2
146
+ const squareX = barBaseX
147
+ const squareY = barY - barHeight / 2
148
+ const borderRadius = applyRadius(barPosition)
149
+ // colors
150
+ const [leftColor, rightColor] = twoColorPalette[twoColor.palette]
151
+ const barColor = { left: leftColor, right: rightColor }
152
+ const isBarColorDark = chroma.contrast('#000000', barColor[barPosition]) < 4.9
153
+ const fill = isBarColorDark ? '#FFFFFF' : '#000000'
154
+
155
+ let textProps = getTextProps(config.isLollipopChart, textFits, lollipopShapeSize, fill)
156
+
157
+ // tooltips
158
+ const xAxisValue = formatNumber(barValue, 'left')
159
+ const yAxisValue = config.runtime.yAxis.type === 'date' ? formatDate(parseDate(data[index][config.runtime.originalXAxis.dataKey])) : data[index][config.runtime.originalXAxis.dataKey]
160
+ let yAxisTooltip = config.runtime.yAxis.label ? `${config.runtime.yAxis.label}: ${yAxisValue}` : yAxisValue
161
+ let xAxisTooltip = config.runtime.xAxis.label ? `${config.runtime.xAxis.label}: ${xAxisValue}` : xAxisValue
162
+ const tooltip = `<div>
163
+ ${yAxisTooltip}<br />
164
+ ${xAxisTooltip}
165
+ </div>`
166
+
167
+ return (
168
+ <Group key={`deviation-bar-${config.orientation}-${seriesKey}-${index}`}>
169
+ <foreignObject x={barX} y={barY} width={barWidth} height={barHeight} style={{ border: `${borderWidth}px solid #333`, backgroundColor: barColor[barPosition], ...borderRadius }} data-tooltip-html={tooltip} data-tooltip-id={`cdc-open-viz-tooltip-${config.runtime.uniqueId}`} />
170
+ {config.yAxis.displayNumbersOnBar && (
171
+ <Text verticalAnchor='middle' x={textX} y={textY} {...textProps[barPosition]}>
172
+ {formatNumber(d[seriesKey], 'left')}
173
+ </Text>
174
+ )}
175
+
176
+ {config.isLollipopChart && config.lollipopShape === 'circle' && <circle cx={circleX} cy={circleY} r={lollipopShapeSize / 2} fill={barColor[barPosition]} style={{ filter: 'unset', opacity: 1 }} />}
177
+ {config.isLollipopChart && config.lollipopShape === 'square' && <rect x={squareX} y={squareY} width={lollipopShapeSize} height={lollipopShapeSize} fill={barColor[barPosition]} style={{ opacity: 1, filter: 'unset' }}></rect>}
178
+ </Group>
179
+ )
180
+ })}
181
+ {targetLabel.showLabel && (
182
+ <Text fontWeight='bold' dx={targetLabel.padding} verticalAnchor='middle' x={targetLabel.x} y={targetLabel.y}>
183
+ {targetLabel.text}
184
+ </Text>
185
+ )}
186
+
187
+ {shouldShowTargetLine && <Line from={{ x: targetX, y: 0 }} to={{ x: targetX, y: height }} stroke='#333' strokeWidth={2} />}
188
+ </Group>
189
+ </ErrorBoundary>
190
+ )
191
+ }