@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
@@ -8,12 +8,21 @@ import ConfigContext from '../ConfigContext'
8
8
  import { getContrastColor } from '@cdc/core/helpers/cove/accessibility'
9
9
  import { APP_FONT_COLOR } from '@cdc/core/helpers/constants'
10
10
  import { getTextWidth } from '@cdc/core/helpers/getTextWidth'
11
+ import { isMobileFontViewport } from '@cdc/core/helpers/viewports'
11
12
 
12
13
  const PairedBarChart = ({ width, height, originalWidth }) => {
13
- const { config, colorScale, transformedData: data, formatNumber, seriesHighlight } = useContext(ConfigContext)
14
+ const {
15
+ config,
16
+ colorScale,
17
+ transformedData: data,
18
+ formatNumber,
19
+ seriesHighlight,
20
+ vizViewport
21
+ } = useContext(ConfigContext)
14
22
 
15
23
  if (!config || config?.series?.length < 2) return
16
24
 
25
+ const labelFontSize = isMobileFontViewport(vizViewport) ? 13 : 16
17
26
  const borderWidth = config.barHasBorder === 'true' ? 1 : 0
18
27
  const halfWidth = width / 2
19
28
  const offset = 1.02 // Offset of the left bar from the Axis
@@ -109,7 +118,10 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
109
118
  const totalheight = (Number(config.barSpace) + barHeight + borderWidth) * data.length
110
119
  config.heights.horizontal = totalheight
111
120
  // check if text fits inside of the bar including suffix/prefix,comma,fontSize ..etc
112
- const textWidth = getTextWidth(formatNumber(d[groupOne.dataKey], 'left'))
121
+ const textWidth = getTextWidth(
122
+ formatNumber(d[groupOne.dataKey], 'left'),
123
+ `normal ${labelFontSize}px sans-serif`
124
+ )
113
125
  const textFits = textWidth < barWidth - 5 // minus padding dx(5)
114
126
 
115
127
  return (
@@ -141,6 +153,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
141
153
  x={halfWidth - barWidth}
142
154
  y={y + config.barHeight / 2}
143
155
  fill={textFits ? groupOne.labelColor : '#000'}
156
+ fontSize={labelFontSize}
144
157
  >
145
158
  {formatNumber(d[groupOne.dataKey], 'left')}
146
159
  </Text>
@@ -168,7 +181,10 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
168
181
  const totalheight = (Number(config.barSpace) + barHeight + borderWidth) * data.length
169
182
  config.heights.horizontal = totalheight
170
183
  // check if text fits inside of the bar including suffix/prefix,comma,fontSize ..etc
171
- const textWidth = getTextWidth(formatNumber(d[groupTwo.dataKey], 'left'))
184
+ const textWidth = getTextWidth(
185
+ formatNumber(d[groupTwo.dataKey], 'left'),
186
+ `normal ${labelFontSize}px sans-serif`
187
+ )
172
188
  const isTextFits = textWidth < barWidth - 5 // minus padding dx(5)
173
189
 
174
190
  return (
@@ -207,6 +223,7 @@ const PairedBarChart = ({ width, height, originalWidth }) => {
207
223
  x={halfWidth + barWidth}
208
224
  y={y + config.barHeight / 2}
209
225
  fill={isTextFits ? groupTwo.labelColor : '#000'}
226
+ fontSize={labelFontSize}
210
227
  >
211
228
  {formatNumber(d[groupTwo.dataKey], 'left')}
212
229
  </Text>
@@ -1,200 +1,422 @@
1
- import React, { MouseEventHandler, useContext } from 'react'
1
+ import React, { useContext } from 'react'
2
2
  import ConfigContext from '../../../ConfigContext'
3
3
  import { ChartContext } from '../../../types/ChartContext'
4
4
  import { Text } from '@visx/text'
5
5
  import { Group } from '@visx/group'
6
6
  import { formatDate, isDateScale } from '@cdc/core/helpers/cove/date.js'
7
7
 
8
+ // Constants for visualization types
9
+ const VIZ_TYPES = {
10
+ BAR: 'Bar',
11
+ LINE: 'Line',
12
+ AREA: 'Area Chart',
13
+ COMBO: 'Combo'
14
+ } as const
15
+
16
+ type Region = {
17
+ from: string
18
+ to: string
19
+ fromType?: 'Fixed' | 'Previous Days'
20
+ toType?: 'Fixed' | 'Last Date'
21
+ label: string
22
+ background: string
23
+ color: string
24
+ }
25
+
26
+ type XScale = {
27
+ (value: unknown): number
28
+ domain: () => unknown[]
29
+ bandwidth?: () => number
30
+ }
31
+
8
32
  type RegionsProps = {
9
- xScale: Function
33
+ xScale: XScale
10
34
  yMax: number
11
35
  barWidth?: number
12
36
  totalBarsInGroup?: number
13
- handleTooltipMouseOff: MouseEventHandler<SVGElement>
14
- handleTooltipMouseOver: MouseEventHandler<SVGElement>
15
- handleTooltipClick: MouseEventHandler<SVGElement>
16
- tooltipData: unknown
17
- showTooltip: Function
18
- hideTooltip: Function
37
+ xMax?: number
38
+ }
39
+
40
+ type HighlightedAreaProps = {
41
+ x: number
42
+ width: number
43
+ yMax: number
44
+ background: string
45
+ }
46
+
47
+ const HighlightedArea: React.FC<HighlightedAreaProps> = ({ x, width, yMax, background }) => (
48
+ <rect x={x} y={0} width={width} height={yMax} fill={background} opacity={0.3} />
49
+ )
50
+
51
+ /** Find the closest date in domain to a target date */
52
+ const findClosestDate = <T,>(targetTime: number, domain: T[], getTime: (d: T) => number): T => {
53
+ let closest = domain[0]
54
+ let minDiff = Math.abs(targetTime - getTime(closest))
55
+
56
+ for (let i = 1; i < domain.length; i++) {
57
+ const diff = Math.abs(targetTime - getTime(domain[i]))
58
+ if (diff < minDiff) {
59
+ minDiff = diff
60
+ closest = domain[i]
61
+ }
62
+ }
63
+ return closest
19
64
  }
20
65
 
66
+ /** Check if visualization type is line-like (Line or Area Chart) */
67
+ const isLineLike = (type: string): boolean => type === VIZ_TYPES.LINE || type === VIZ_TYPES.AREA
68
+
69
+ /** Check if visualization type is bar-like (Bar or Combo) */
70
+ const isBarLike = (type: string): boolean => type === VIZ_TYPES.BAR || type === VIZ_TYPES.COMBO
71
+
21
72
  // TODO: should regions be removed on categorical axis?
22
- const Regions: React.FC<RegionsProps> = ({
23
- xScale,
24
- barWidth = 0,
25
- totalBarsInGroup = 1,
26
- yMax,
27
- handleTooltipMouseOff,
28
- handleTooltipMouseOver,
29
- handleTooltipClick,
30
- tooltipData,
31
- showTooltip,
32
- hideTooltip
33
- }) => {
73
+ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax, xMax }) => {
34
74
  const { parseDate, config } = useContext<ChartContext>(ConfigContext)
35
75
 
36
- const { runtime, regions, visualizationType, orientation, xAxis } = config
37
- const domain = xScale.domain()
76
+ const { regions, visualizationType, orientation, xAxis } = config
77
+
78
+ const getBarOffset = (): number => (barWidth * totalBarsInGroup) / 2
79
+
80
+ // ============================================
81
+ // HELPER FUNCTIONS FOR PREVIOUS DAYS
82
+ // ============================================
83
+
84
+ const calculatePreviousDaysFrom = (region: Region, axisType: string): number => {
85
+ const previousDays = Number(region.from) || 0
86
+ const domain = xScale.domain()
87
+
88
+ // Determine the "to" reference date
89
+ const toRefDate =
90
+ region.toType === 'Last Date'
91
+ ? new Date(domain[domain.length - 1] as string | number).getTime()
92
+ : new Date(region.to)
93
+
94
+ const toFormatted = formatDate(config.xAxis.dateParseFormat, toRefDate)
95
+ const toDate = new Date(toFormatted)
96
+ const fromDate = new Date(toDate)
97
+ fromDate.setDate(fromDate.getDate() - previousDays)
38
98
 
39
- const getFromValue = region => {
40
- let from
99
+ let closestValue: unknown
41
100
 
42
- // Fixed Date
43
- if (!region?.fromType || region.fromType === 'Fixed') {
101
+ if (axisType === 'date') {
102
+ const fromTime = new Date(formatDate(xAxis.dateParseFormat, fromDate)).getTime()
103
+ closestValue = findClosestDate(fromTime, domain as number[], d => d)
104
+ } else if (axisType === 'categorical') {
105
+ const fromTime = fromDate.getTime()
106
+ closestValue = findClosestDate(fromTime, domain as string[], d => new Date(d).getTime())
107
+ } else if (axisType === 'date-time') {
108
+ closestValue = fromDate.getTime()
109
+ }
110
+
111
+ return xScale(closestValue)
112
+ }
113
+
114
+ // ============================================
115
+ // LINE/AREA CHART LOGIC
116
+ // ============================================
117
+
118
+ const getLineFromValue_Categorical = (region: Region): number => {
119
+ let from: number
120
+ if (region.fromType === 'Previous Days') {
121
+ from = calculatePreviousDaysFrom(region, 'categorical')
122
+ } else {
123
+ from = xScale(region.from)
124
+ }
125
+ // Add left padding (yAxis.size) + half bandwidth to center on the category
126
+ let scalePadding = Number(config.yAxis.size)
127
+ if (xScale.bandwidth) {
128
+ scalePadding += xScale.bandwidth() / 2
129
+ }
130
+ return from + scalePadding
131
+ }
132
+
133
+ const getLineToValue_Categorical = (region: Region): number => {
134
+ if (region.toType === 'Last Date') {
135
+ return calculateLineLastDatePosition_Categorical()
136
+ }
137
+ let to = xScale(region.to)
138
+ // Add left padding (yAxis.size) + half bandwidth
139
+ let scalePadding = Number(config.yAxis.size)
140
+ if (xScale.bandwidth) {
141
+ scalePadding += xScale.bandwidth() / 2
142
+ }
143
+ return to + scalePadding
144
+ }
145
+
146
+ const getLineFromValue_Date = (region: Region): number => {
147
+ let from: number
148
+ if (region.fromType === 'Previous Days') {
149
+ from = calculatePreviousDaysFrom(region, 'date')
150
+ } else {
151
+ // For date scale (band), we need to find the value in the domain
152
+ // Parse the region date to match the format in the domain
44
153
  const date = new Date(region.from)
45
154
  const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime()
46
- from = xScale(parsedDate)
47
155
 
48
- if (visualizationType === 'Bar' && xAxis.type === 'date-time') {
49
- from = from - (barWidth * totalBarsInGroup) / 2
50
- }
156
+ // For band scales, find the closest date in the domain
157
+ const domain = xScale.domain() as number[]
158
+ const closestDate = findClosestDate(parsedDate, domain, d => d)
159
+ from = xScale(closestDate)
160
+ }
161
+ // Add left padding (yAxis.size) + half bandwidth
162
+ let scalePadding = Number(config.yAxis.size)
163
+ if (xScale.bandwidth) {
164
+ scalePadding += xScale.bandwidth() / 2
51
165
  }
166
+ return from + scalePadding
167
+ }
52
168
 
53
- // Previous Date
169
+ const getLineToValue_Date = (region: Region): number => {
170
+ if (region.toType === 'Last Date') {
171
+ return calculateLineLastDatePosition_Date()
172
+ }
173
+ // For date scale (band), we need to find the value in the domain
174
+ // Parse the region date to match the format in the domain
175
+ const parsedDate = parseDate(region.to).getTime()
176
+
177
+ // For band scales, find the closest date in the domain
178
+ const domain = xScale.domain() as number[]
179
+ const closestDate = findClosestDate(parsedDate, domain, d => d)
180
+ let to = xScale(closestDate)
181
+
182
+ // Add left padding (yAxis.size) + half bandwidth
183
+ let scalePadding = Number(config.yAxis.size)
184
+ if (xScale.bandwidth) {
185
+ scalePadding += xScale.bandwidth() / 2
186
+ }
187
+ return to + scalePadding
188
+ }
189
+
190
+ const getLineFromValue_DateTime = (region: Region): number => {
54
191
  if (region.fromType === 'Previous Days') {
55
- const previousDays = Number(region.from) || 0
56
- const categoricalDomain = domain.map(d => formatDate(config.xAxis.dateParseFormat, new Date(d)))
57
- const d = region.toType === 'Last Date' ? new Date(domain[domain.length - 1]).getTime() : new Date(region.to) // on categorical charts force leading zero 03/15/2016 vs 3/15/2016 for valid date format
58
- const to =
59
- config.xAxis.type === 'categorical'
60
- ? formatDate(config.xAxis.dateParseFormat, d)
61
- : formatDate(config.xAxis.dateParseFormat, d)
62
- const toDate = new Date(to)
63
- from = new Date(toDate.setDate(toDate.getDate() - Number(previousDays)))
64
-
65
- if (xAxis.type === 'date') {
66
- from = new Date(formatDate(xAxis.dateParseFormat, from)).getTime()
67
-
68
- let closestDate = domain[0]
69
- let minDiff = Math.abs(from - closestDate)
70
-
71
- for (let i = 1; i < domain.length; i++) {
72
- const diff = Math.abs(from - domain[i])
73
- if (diff < minDiff) {
74
- minDiff = diff
75
- closestDate = domain[i]
76
- }
77
- }
78
- from = closestDate
79
- }
192
+ const from = calculatePreviousDaysFrom(region, 'date-time')
193
+ return from + Number(config.yAxis.size)
194
+ }
195
+ const date = new Date(region.from)
196
+ const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime()
197
+ let from = xScale(parsedDate)
198
+ // For date-time, xScale returns correct position (no bandwidth), just add left padding
199
+ return from + Number(config.yAxis.size)
200
+ }
80
201
 
81
- // Here the domain is in the xScale.dateParseFormat
82
- if (xAxis.type === 'categorical') {
83
- let closestDate = domain[0]
84
- let minDiff = Math.abs(new Date(from).getTime() - new Date(closestDate).getTime())
85
-
86
- for (let i = 1; i < domain.length; i++) {
87
- const diff = Math.abs(new Date(from).getTime() - new Date(domain[i]).getTime())
88
- if (diff < minDiff) {
89
- minDiff = diff
90
- closestDate = domain[i]
91
- }
92
- }
93
- from = closestDate
94
- }
202
+ const getLineToValue_DateTime = (region: Region): number => {
203
+ if (region.toType === 'Last Date') {
204
+ return calculateLineLastDatePosition_DateTime()
205
+ }
206
+ let to = xScale(parseDate(region.to).getTime())
207
+ return to + Number(config.yAxis.size)
208
+ }
209
+
210
+ const calculateLineLastDatePosition_Categorical = (): number => {
211
+ const chartStart = Number(config.yAxis.size || 0)
212
+ // Extend to the right edge of the chart
213
+ return chartStart + (xMax || 0)
214
+ }
215
+
216
+ const calculateLineLastDatePosition_Date = (): number => {
217
+ const chartStart = Number(config.yAxis.size || 0)
218
+ // For date scale line charts with Last Date, extend to the right edge of the chart
219
+ return chartStart + (xMax || 0)
220
+ }
221
+
222
+ const calculateLineLastDatePosition_DateTime = (): number => {
223
+ const domain = xScale.domain()
224
+ const lastDate = domain[domain.length - 1]
225
+ const lastDatePosition = xScale(lastDate)
226
+ // Match the non-Last Date logic: just add yAxis.size
227
+ return Number(lastDatePosition + Number(config.yAxis.size))
228
+ }
229
+
230
+ // ============================================
231
+ // BAR CHART LOGIC
232
+ // ============================================
95
233
 
96
- from = xScale(from)
234
+ const getBarFromValue_Categorical = (region: Region): number => {
235
+ if (region.fromType === 'Previous Days') {
236
+ return calculatePreviousDaysFrom(region, 'categorical')
97
237
  }
238
+ return xScale(region.from)
239
+ }
98
240
 
99
- if (xAxis.type === 'categorical' && region.fromType !== 'Previous Days') {
100
- from = xScale(region.from)
241
+ const getBarToValue_Categorical = (region: Region): number => {
242
+ if (region.toType === 'Last Date') {
243
+ return calculateBarLastDatePosition_Categorical()
101
244
  }
245
+ let to = xScale(region.to)
246
+ return to + barWidth * totalBarsInGroup
247
+ }
102
248
 
103
- if (visualizationType === 'Line' || visualizationType === 'Area Chart') {
104
- let scalePadding = Number(config.yAxis.size)
105
- if (xScale.bandwidth) {
106
- scalePadding += xScale.bandwidth() / 2
107
- }
108
- from = from + scalePadding
249
+ const getBarFromValue_Date = (region: Region): number => {
250
+ if (region.fromType === 'Previous Days') {
251
+ return calculatePreviousDaysFrom(region, 'date')
109
252
  }
253
+ // For date scale (band), we need to find the value in the domain
254
+ const date = new Date(region.from)
255
+ const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime()
256
+
257
+ // For band scales, find the closest date in the domain
258
+ const domain = xScale.domain() as number[]
259
+ const closestDate = findClosestDate(parsedDate, domain, d => d)
260
+ return xScale(closestDate)
261
+ }
110
262
 
111
- if (visualizationType === 'Bar' && config.xAxis.type === 'date-time' && region.fromType === 'Previous Days') {
112
- from = from - (barWidth * totalBarsInGroup) / 2
263
+ const getBarToValue_Date = (region: Region): number => {
264
+ if (region.toType === 'Last Date') {
265
+ return calculateBarLastDatePosition_Date()
113
266
  }
267
+ // For date scale (band), we need to find the value in the domain
268
+ const parsedDate = parseDate(region.to).getTime()
269
+
270
+ // For band scales, find the closest date in the domain
271
+ const domain = xScale.domain() as number[]
272
+ const closestDate = findClosestDate(parsedDate, domain, d => d)
273
+ let to = xScale(closestDate)
114
274
 
115
- return from
275
+ return to + barWidth * totalBarsInGroup
116
276
  }
117
277
 
118
- const getToValue = region => {
119
- let to
278
+ const getBarFromValue_DateTime = (region: Region): number => {
279
+ let from: number
280
+ if (region.fromType === 'Previous Days') {
281
+ from = calculatePreviousDaysFrom(region, 'date-time')
282
+ } else {
283
+ const date = new Date(region.from)
284
+ const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime()
285
+ from = xScale(parsedDate)
286
+ }
287
+ return from - getBarOffset()
288
+ }
120
289
 
121
- // when xScale is categorical leading zeros are removed, ie. 03/15/2016 is 3/15/2016
122
- if (xAxis.type === 'categorical') {
123
- to = xScale(region.to)
290
+ const getBarToValue_DateTime = (region: Region): number => {
291
+ if (region.toType === 'Last Date') {
292
+ return calculateBarLastDatePosition_DateTime()
124
293
  }
294
+ let to = xScale(parseDate(region.to).getTime())
295
+ return to - getBarOffset()
296
+ }
125
297
 
126
- if (isDateScale(xAxis)) {
127
- if (!region?.toType || region.toType === 'Fixed') {
128
- to = xScale(parseDate(region.to).getTime())
129
- }
298
+ const calculateBarLastDatePosition_Categorical = (): number => {
299
+ const domain = xScale.domain()
300
+ const lastDate = domain[domain.length - 1]
301
+ const lastDatePosition = xScale(lastDate)
302
+ const bandwidth = xScale.bandwidth ? xScale.bandwidth() : 0
303
+ // For categorical bars, extend to the end of the last bar
304
+ // Don't add chartStart - xScale already returns positions in the chart coordinate space
305
+ return xMax
306
+ }
307
+
308
+ const calculateBarLastDatePosition_Date = (): number => {
309
+ const domain = xScale.domain()
310
+ const lastDate = domain[domain.length - 1]
311
+ const lastDatePosition = xScale(lastDate)
312
+ const offset = barWidth * totalBarsInGroup
313
+ // Don't add chartStart - xScale already returns positions in chart coordinate space
314
+ return Number(lastDatePosition + offset)
315
+ }
316
+
317
+ const calculateBarLastDatePosition_DateTime = (): number => {
318
+ const domain = xScale.domain()
319
+ const lastDate = domain[domain.length - 1]
320
+ const lastDatePosition = xScale(lastDate)
321
+ // For date-time bars, don't add chartStart - xScale returns positions in chart coordinate space
322
+ // Also don't subtract barOffset since we want to extend to the edge
323
+ return Number(lastDatePosition)
324
+ }
130
325
 
131
- if (visualizationType === 'Bar' || config.visualizationType === 'Combo') {
132
- to = region.toType !== 'Last Date' ? xScale(parseDate(region.to).getTime()) + barWidth * totalBarsInGroup : to
326
+ // ============================================
327
+ // MAIN ROUTING FUNCTIONS
328
+ // ============================================
329
+
330
+ const getFromValue = (region: Region): number => {
331
+ const isLine = isLineLike(visualizationType)
332
+ const isBar = isBarLike(visualizationType)
333
+
334
+ // LINE/AREA CHARTS
335
+ if (isLine) {
336
+ if (xAxis.type === 'categorical') {
337
+ return getLineFromValue_Categorical(region)
338
+ } else if (xAxis.type === 'date') {
339
+ return getLineFromValue_Date(region)
340
+ } else if (xAxis.type === 'date-time') {
341
+ return getLineFromValue_DateTime(region)
133
342
  }
134
343
  }
135
- if (region.toType === 'Last Date') {
136
- const lastDate = domain[domain.length - 1]
137
- to = Number(
138
- xScale(lastDate) +
139
- ((visualizationType === 'Bar' || visualizationType === 'Combo') && config.xAxis.type === 'date'
140
- ? barWidth * totalBarsInGroup
141
- : 0)
142
- )
143
- }
144
-
145
- if (visualizationType === 'Line' || visualizationType === 'Area Chart') {
146
- let scalePadding = Number(config.yAxis.size)
147
- if (xScale.bandwidth) {
148
- scalePadding += xScale.bandwidth() / 2
344
+
345
+ // BAR CHARTS
346
+ if (isBar) {
347
+ if (xAxis.type === 'categorical') {
348
+ return getBarFromValue_Categorical(region)
349
+ } else if (xAxis.type === 'date') {
350
+ return getBarFromValue_Date(region)
351
+ } else if (xAxis.type === 'date-time') {
352
+ return getBarFromValue_DateTime(region)
149
353
  }
150
- to = to + scalePadding
151
354
  }
152
355
 
153
- if (visualizationType === 'Bar' && config.xAxis.type === 'date-time' && region.toType !== 'Last Date') {
154
- to = to - (barWidth * totalBarsInGroup) / 2
356
+ return 0
357
+ }
358
+
359
+ const getToValue = (region: Region): number => {
360
+ const isLine = isLineLike(visualizationType)
361
+ const isBar = isBarLike(visualizationType)
362
+
363
+ // LINE/AREA CHARTS
364
+ if (isLine) {
365
+ if (xAxis.type === 'categorical') {
366
+ return getLineToValue_Categorical(region)
367
+ } else if (xAxis.type === 'date') {
368
+ return getLineToValue_Date(region)
369
+ } else if (xAxis.type === 'date-time') {
370
+ return getLineToValue_DateTime(region)
371
+ }
155
372
  }
156
373
 
157
- if ((visualizationType === 'Bar' || visualizationType === 'Combo') && xAxis.type === 'categorical') {
158
- to = to + (visualizationType === 'Bar' || visualizationType === 'Combo' ? barWidth * totalBarsInGroup : 0)
374
+ // BAR CHARTS
375
+ if (isBar) {
376
+ if (xAxis.type === 'categorical') {
377
+ return getBarToValue_Categorical(region)
378
+ } else if (xAxis.type === 'date') {
379
+ return getBarToValue_Date(region)
380
+ } else if (xAxis.type === 'date-time') {
381
+ return getBarToValue_DateTime(region)
382
+ }
159
383
  }
160
- return to
384
+
385
+ return 0
161
386
  }
162
387
 
163
- const getWidth = (to, from) => to - from
388
+ const getWidth = (to: number, from: number): number => Math.max(0, to - from)
164
389
 
165
- if (regions && orientation === 'vertical') {
166
- return regions.map(region => {
167
- const from = getFromValue(region)
168
- const to = getToValue(region)
169
- const width = getWidth(to, from)
390
+ if (!regions || orientation !== 'vertical') return null
170
391
 
171
- if (!from) return null
172
- if (!to) return null
392
+ const chartStart = Number(config.yAxis.size || 0)
393
+ const chartEnd = xMax !== undefined ? chartStart + xMax : chartStart + 1000
173
394
 
174
- const HighlightedArea = () => {
175
- return <rect x={from} y={0} width={width} height={yMax} fill={region.background} opacity={0.3} />
176
- }
395
+ return regions.map((region: Region) => {
396
+ const from = getFromValue(region)
397
+ const to = getToValue(region)
177
398
 
178
- return (
179
- <Group
180
- height={100}
181
- fill='red'
182
- className='regions regions-group--line zzz'
183
- key={region.label}
184
- onMouseMove={handleTooltipMouseOver}
185
- onMouseLeave={handleTooltipMouseOff}
186
- handleTooltipClick={handleTooltipClick}
187
- tooltipData={JSON.stringify(tooltipData)}
188
- showTooltip={showTooltip}
189
- >
190
- <HighlightedArea />
191
- <Text x={from + width / 2} y={5} fill={region.color} verticalAnchor='start' textAnchor='middle'>
192
- {region.label}
193
- </Text>
194
- </Group>
195
- )
196
- })
197
- }
399
+ // Validate computed positions
400
+ if (from === undefined || isNaN(from) || to === undefined || isNaN(to)) {
401
+ return null
402
+ }
403
+
404
+ // Clip region to visible chart area
405
+ const clippedFrom = Math.max(chartStart, from)
406
+ const clippedTo = Math.min(chartEnd, to)
407
+ const width = getWidth(clippedTo, clippedFrom)
408
+
409
+ if (width <= 0) return null
410
+
411
+ return (
412
+ <Group height={100} fill='red' className='regions regions-group--line' key={region.label} pointerEvents='none'>
413
+ <HighlightedArea x={clippedFrom} width={width} yMax={yMax} background={region.background} />
414
+ <Text x={clippedFrom + width / 2} y={5} fill={region.color} verticalAnchor='start' textAnchor='middle'>
415
+ {region.label}
416
+ </Text>
417
+ </Group>
418
+ )
419
+ })
198
420
  }
199
421
 
200
422
  export default Regions
@@ -1,6 +1,6 @@
1
1
  export type Link = { source: string; target: string; value: number; id: string }
2
2
 
3
- export type Data = {
3
+ type Data = {
4
4
  links: Link[]
5
5
  }
6
6
 
@@ -42,8 +42,8 @@ const ScatterPlot = ({ xScale, yScale }) => {
42
42
  ? `${config.runtime.seriesLabels[s] || ''}<br/>`
43
43
  : ''
44
44
  }
45
- ${config.xAxis.label}: ${formatNumber(item[config.xAxis.dataKey], 'bottom')} <br/>
46
- ${config.yAxis.label}: ${formatNumber(item[s], 'left')}<br/>
45
+ ${config.runtime?.xAxis?.label || config.xAxis.label}: ${formatNumber(item[config.xAxis.dataKey], 'bottom')} <br/>
46
+ ${config.runtime?.yAxis?.label || config.yAxis.label}: ${formatNumber(item[s], 'left')}<br/>
47
47
  ${additionalColumns
48
48
  .map(
49
49
  ([label, name, options]) =>