@cdc/chart 4.25.11 → 4.26.2

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 (181) hide show
  1. package/CLAUDE.local.md +79 -0
  2. package/dist/{cdcchart-dgT_1dIT.es.js → cdcchart-DQ00cQCm.es.js} +1 -20
  3. package/dist/cdcchart.js +51401 -50814
  4. package/examples/default.json +378 -0
  5. package/examples/feature/__data__/horizon-chart-data.json +373 -0
  6. package/examples/feature/annotations/index.json +3 -6
  7. package/examples/feature/horizon/horizon-chart.json +395 -0
  8. package/examples/feature/pie/planet-pie-example-config.json +48 -2
  9. package/examples/line-chart-states.json +1085 -0
  10. package/examples/private/123.json +694 -0
  11. package/examples/private/DEV-12100.json +1303 -0
  12. package/examples/private/anchor-issue.json +4094 -0
  13. package/examples/private/backwards-slider.json +10430 -0
  14. package/examples/private/cat-y.json +1235 -0
  15. package/examples/private/data-points.json +228 -0
  16. package/examples/private/georgia.csv +160 -0
  17. package/examples/private/height.json +3915 -0
  18. package/examples/private/links.json +569 -0
  19. package/examples/private/quadrant.txt +30 -0
  20. package/examples/private/test-forecast.json +5510 -0
  21. package/examples/private/timeline-data.json +1 -0
  22. package/examples/private/timeline.json +389 -0
  23. package/examples/private/warming-stripe-test.json +2578 -0
  24. package/examples/private/warming-stripes.json +4763 -0
  25. package/examples/radar-chart-simple.json +133 -0
  26. package/examples/radar-chart.json +148 -0
  27. package/examples/tech-adoption-with-links.json +560 -0
  28. package/index.html +1 -36
  29. package/package.json +59 -60
  30. package/src/CdcChartComponent.tsx +206 -89
  31. package/src/_stories/Chart.Anchors.stories.tsx +10 -0
  32. package/src/_stories/Chart.BoxPlot.stories.tsx +7 -0
  33. package/src/_stories/Chart.CI.stories.tsx +13 -0
  34. package/src/_stories/Chart.Combo.stories.tsx +17 -0
  35. package/src/_stories/Chart.CustomColors.stories.tsx +4 -0
  36. package/src/_stories/Chart.DynamicSeries.stories.tsx +19 -0
  37. package/src/_stories/Chart.Filters.stories.tsx +4 -0
  38. package/src/_stories/Chart.Forecast.stories.tsx +4 -0
  39. package/src/_stories/Chart.HTMLInDataTable.stories.tsx +22 -0
  40. package/src/_stories/Chart.Legend.Gradient.stories.tsx +28 -0
  41. package/src/_stories/Chart.Patterns.stories.tsx +4 -0
  42. package/src/_stories/Chart.PreserveDecimals.stories.tsx +25 -0
  43. package/src/_stories/Chart.Regions.Categorical.stories.tsx +161 -0
  44. package/src/_stories/Chart.Regions.DateScale.stories.tsx +216 -0
  45. package/src/_stories/Chart.Regions.DateTimeScale.stories.tsx +312 -0
  46. package/src/_stories/Chart.ScatterPlot.stories.tsx +4 -0
  47. package/src/_stories/Chart.SmallMultiples.stories.tsx +16 -0
  48. package/src/_stories/Chart.stories.tsx +45 -0
  49. package/src/_stories/Chart.tooltip.stories.tsx +7 -0
  50. package/src/_stories/ChartAnnotation.stories.tsx +10 -0
  51. package/src/_stories/ChartAxisLabels.stories.tsx +4 -0
  52. package/src/_stories/ChartAxisTitles.stories.tsx +10 -0
  53. package/src/_stories/ChartBar.Editor.stories.tsx +11 -6
  54. package/src/_stories/ChartBrush.Editor.stories.tsx +295 -0
  55. package/src/_stories/ChartBrush.Matrix.Continuous.stories.tsx +41 -0
  56. package/src/_stories/ChartBrush.Matrix.Date.stories.tsx +114 -0
  57. package/src/_stories/ChartBrush.Matrix.DateTime.stories.tsx +78 -0
  58. package/src/_stories/ChartBrush.stories.tsx +57 -0
  59. package/src/_stories/ChartEditor.Editor.stories.tsx +3 -5
  60. package/src/_stories/ChartEditor.stories.tsx +7 -0
  61. package/src/_stories/ChartLine.QuadrantAngles.stories.tsx +89 -0
  62. package/src/_stories/ChartLine.Suppression.stories.tsx +7 -0
  63. package/src/_stories/ChartLine.Symbols.stories.tsx +4 -0
  64. package/src/_stories/ChartPrefixSuffix.stories.tsx +46 -1
  65. package/src/_stories/TechAdoptionWithLinks.stories.tsx +34 -0
  66. package/src/_stories/_mock/brush_continuous.json +86 -0
  67. package/src/_stories/_mock/brush_date_large.json +176 -0
  68. package/src/_stories/_mock/brush_enabled.json +326 -0
  69. package/src/_stories/_mock/brush_mock.json +2 -69
  70. package/src/_stories/_mock/horizontal-bars-dynamic-y-axis.json +413 -0
  71. package/src/_stories/_mock/line_chart_angle_near_zero_fall.json +195 -0
  72. package/src/_stories/_mock/line_chart_angle_near_zero_rise.json +195 -0
  73. package/src/_stories/_mock/line_chart_angle_q1_steep_upward.json +195 -0
  74. package/src/_stories/_mock/line_chart_angle_q2_gentle_downward.json +195 -0
  75. package/src/_stories/_mock/line_chart_angle_q3_steep_downward.json +195 -0
  76. package/src/_stories/_mock/line_chart_angle_q4_gentle_upward.json +195 -0
  77. package/src/_stories/_mock/line_chart_quadrant_angles.json +264 -0
  78. package/src/components/Annotations/components/AnnotationDraggable.styles.css +11 -17
  79. package/src/components/Annotations/components/AnnotationDraggable.tsx +240 -116
  80. package/src/components/Annotations/components/AnnotationDropdown.styles.css +1 -2
  81. package/src/components/Annotations/components/AnnotationDropdown.tsx +8 -12
  82. package/src/components/Annotations/components/AnnotationList.styles.css +4 -10
  83. package/src/components/Annotations/components/AnnotationList.tsx +5 -4
  84. package/src/components/Annotations/components/findNearestDatum.ts +75 -85
  85. package/src/components/Annotations/helpers/getVisibleAnnotations.ts +38 -0
  86. package/src/components/AreaChart/components/AreaChart.Stacked.jsx +1 -2
  87. package/src/components/Axis/BottomAxis.tsx +270 -0
  88. package/src/components/Axis/Categorical.Axis.tsx +6 -7
  89. package/src/components/Axis/LeftAxis.tsx +404 -0
  90. package/src/components/Axis/LeftAxisGridlines.tsx +77 -0
  91. package/src/components/Axis/PairedBarAxis.tsx +186 -0
  92. package/src/components/Axis/README.md +94 -0
  93. package/src/components/Axis/RightAxis.tsx +108 -0
  94. package/src/components/Axis/axis.constants.ts +21 -0
  95. package/src/components/Axis/index.ts +7 -0
  96. package/src/components/BarChart/components/BarChart.Horizontal.tsx +178 -24
  97. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +3 -1
  98. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +1 -0
  99. package/src/components/BarChart/components/BarChart.Vertical.tsx +6 -8
  100. package/src/components/BarChart/components/BarChart.tsx +7 -1
  101. package/src/components/BarChart/components/context.tsx +1 -0
  102. package/src/components/BarChart/helpers/useBarChart.ts +14 -2
  103. package/src/components/Brush/BrushSelector.tsx +1390 -0
  104. package/src/components/Brush/MiniChartPreview.tsx +400 -0
  105. package/src/components/DeviationBar.jsx +9 -7
  106. package/src/components/EditorPanel/EditorPanel.tsx +2734 -2595
  107. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +60 -22
  108. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +56 -34
  109. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +137 -30
  110. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +2 -0
  111. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +353 -0
  112. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +0 -1
  113. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +30 -25
  114. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +42 -28
  115. package/src/components/EditorPanel/components/Panels/index.tsx +2 -0
  116. package/src/components/EditorPanel/useEditorPermissions.ts +81 -39
  117. package/src/components/HorizonChart/HorizonChart.tsx +131 -0
  118. package/src/components/HorizonChart/components/HorizonBand.tsx +160 -0
  119. package/src/components/HorizonChart/helpers/calculateHorizonBands.ts +27 -0
  120. package/src/components/HorizonChart/helpers/getHorizonLayerColors.ts +40 -0
  121. package/src/components/HorizonChart/index.tsx +3 -0
  122. package/src/components/Legend/Legend.Component.tsx +52 -4
  123. package/src/components/Legend/Legend.tsx +4 -3
  124. package/src/components/Legend/LegendValueRange.tsx +77 -0
  125. package/src/components/Legend/helpers/createFormatLabels.tsx +164 -2
  126. package/src/components/Legend/helpers/generateValueRanges.ts +92 -0
  127. package/src/components/Legend/helpers/index.ts +10 -6
  128. package/src/components/LineChart/helpers/README.md +292 -0
  129. package/src/components/LineChart/helpers/labelPositioning.test.ts +245 -0
  130. package/src/components/LineChart/helpers/labelPositioning.ts +304 -0
  131. package/src/components/LineChart/index.tsx +44 -8
  132. package/src/components/LinearChart/README.md +109 -0
  133. package/src/components/LinearChart/VisualizationRenderer.tsx +267 -0
  134. package/src/components/LinearChart/linearChart.constants.ts +84 -0
  135. package/src/components/LinearChart/tests/LinearChart.test.tsx +201 -0
  136. package/src/components/LinearChart/tests/mockConfigContext.ts +129 -0
  137. package/src/components/LinearChart/utils/tickFormatting.ts +146 -0
  138. package/src/components/LinearChart.tsx +338 -1082
  139. package/src/components/PairedBarChart.jsx +20 -3
  140. package/src/components/PieChart/PieChart.tsx +1 -1
  141. package/src/components/RadarChart/RadarAxis.tsx +78 -0
  142. package/src/components/RadarChart/RadarChart.tsx +298 -0
  143. package/src/components/RadarChart/RadarGrid.tsx +64 -0
  144. package/src/components/RadarChart/RadarPolygon.tsx +91 -0
  145. package/src/components/RadarChart/helpers.ts +83 -0
  146. package/src/components/RadarChart/index.tsx +3 -0
  147. package/src/components/Regions/components/Regions.tsx +365 -122
  148. package/src/components/ScatterPlot/ScatterPlot.jsx +2 -2
  149. package/src/components/SmallMultiples/SmallMultipleTile.tsx +5 -1
  150. package/src/components/WarmingStripes/WarmingStripes.tsx +230 -0
  151. package/src/components/WarmingStripes/WarmingStripesGradientLegend.css +35 -0
  152. package/src/components/WarmingStripes/WarmingStripesGradientLegend.tsx +104 -0
  153. package/src/components/WarmingStripes/index.tsx +3 -0
  154. package/src/data/initial-state.js +17 -2
  155. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +57 -0
  156. package/src/helpers/getExcludedData.ts +4 -0
  157. package/src/helpers/getMinMax.ts +12 -7
  158. package/src/helpers/handleChartAriaLabels.ts +19 -19
  159. package/src/helpers/handleLineType.ts +22 -18
  160. package/src/helpers/sizeHelpers.ts +0 -20
  161. package/src/helpers/smallMultiplesHelpers.ts +1 -1
  162. package/src/hooks/useChartHoverAnalytics.tsx +10 -9
  163. package/src/hooks/useProgrammaticTooltip.ts +23 -2
  164. package/src/hooks/useScales.ts +18 -1
  165. package/src/hooks/useTooltip.tsx +34 -10
  166. package/src/scss/DataTable.scss +0 -4
  167. package/src/scss/main.scss +22 -3
  168. package/src/selectors/README.md +68 -0
  169. package/src/store/chart.reducer.ts +2 -0
  170. package/src/test/CdcChart.test.jsx +1 -1
  171. package/src/types/ChartConfig.ts +21 -0
  172. package/src/types/ChartContext.ts +1 -0
  173. package/src/types/Horizon.ts +64 -0
  174. package/src/types/Label.ts +1 -0
  175. package/src/utils/analyticsTracking.ts +19 -0
  176. package/LICENSE +0 -201
  177. package/src/components/Annotations/components/helpers/index.tsx +0 -46
  178. package/src/components/Brush/BrushChart.tsx +0 -128
  179. package/src/components/Brush/BrushController.tsx +0 -71
  180. package/src/components/Brush/types.tsx +0 -8
  181. package/src/components/BrushChart.tsx +0 -223
@@ -5,175 +5,418 @@ 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
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
13
64
  }
14
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
+
15
72
  // TODO: should regions be removed on categorical axis?
16
- const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax }) => {
73
+ const Regions: React.FC<RegionsProps> = ({ xScale, barWidth = 0, totalBarsInGroup = 1, yMax, xMax }) => {
17
74
  const { parseDate, config } = useContext<ChartContext>(ConfigContext)
18
75
 
19
- const { runtime, regions, visualizationType, orientation, xAxis } = config
20
- 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)
21
98
 
22
- const getFromValue = region => {
23
- let from
99
+ let closestValue: unknown
24
100
 
25
- // Fixed Date
26
- 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
27
153
  const date = new Date(region.from)
28
154
  const parsedDate = parseDate(formatDate(config.xAxis.dateParseFormat, date)).getTime()
29
- from = xScale(parsedDate)
30
155
 
31
- if (visualizationType === 'Bar' && xAxis.type === 'date-time') {
32
- from = from - (barWidth * totalBarsInGroup) / 2
33
- }
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
34
165
  }
166
+ return from + scalePadding
167
+ }
35
168
 
36
- // 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 => {
37
191
  if (region.fromType === 'Previous Days') {
38
- const previousDays = Number(region.from) || 0
39
- const categoricalDomain = domain.map(d => formatDate(config.xAxis.dateParseFormat, new Date(d)))
40
- 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
41
- const to =
42
- config.xAxis.type === 'categorical'
43
- ? formatDate(config.xAxis.dateParseFormat, d)
44
- : formatDate(config.xAxis.dateParseFormat, d)
45
- const toDate = new Date(to)
46
- from = new Date(toDate.setDate(toDate.getDate() - Number(previousDays)))
47
-
48
- if (xAxis.type === 'date') {
49
- from = new Date(formatDate(xAxis.dateParseFormat, from)).getTime()
50
-
51
- let closestDate = domain[0]
52
- let minDiff = Math.abs(from - closestDate)
53
-
54
- for (let i = 1; i < domain.length; i++) {
55
- const diff = Math.abs(from - domain[i])
56
- if (diff < minDiff) {
57
- minDiff = diff
58
- closestDate = domain[i]
59
- }
60
- }
61
- from = closestDate
62
- }
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
+ }
63
201
 
64
- // Here the domain is in the xScale.dateParseFormat
65
- if (xAxis.type === 'categorical') {
66
- let closestDate = domain[0]
67
- let minDiff = Math.abs(new Date(from).getTime() - new Date(closestDate).getTime())
68
-
69
- for (let i = 1; i < domain.length; i++) {
70
- const diff = Math.abs(new Date(from).getTime() - new Date(domain[i]).getTime())
71
- if (diff < minDiff) {
72
- minDiff = diff
73
- closestDate = domain[i]
74
- }
75
- }
76
- from = closestDate
77
- }
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
+ // ============================================
78
233
 
79
- from = xScale(from)
234
+ const getBarFromValue_Categorical = (region: Region): number => {
235
+ if (region.fromType === 'Previous Days') {
236
+ return calculatePreviousDaysFrom(region, 'categorical')
80
237
  }
238
+ return xScale(region.from)
239
+ }
81
240
 
82
- if (xAxis.type === 'categorical' && region.fromType !== 'Previous Days') {
83
- from = xScale(region.from)
241
+ const getBarToValue_Categorical = (region: Region): number => {
242
+ if (region.toType === 'Last Date') {
243
+ return calculateBarLastDatePosition_Categorical()
84
244
  }
245
+ let to = xScale(region.to)
246
+ return to + barWidth * totalBarsInGroup
247
+ }
85
248
 
86
- if (visualizationType === 'Line' || visualizationType === 'Area Chart') {
87
- let scalePadding = Number(config.yAxis.size)
88
- if (xScale.bandwidth) {
89
- scalePadding += xScale.bandwidth() / 2
90
- }
91
- from = from + scalePadding
249
+ const getBarFromValue_Date = (region: Region): number => {
250
+ if (region.fromType === 'Previous Days') {
251
+ return calculatePreviousDaysFrom(region, 'date')
92
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
+ }
93
262
 
94
- if (visualizationType === 'Bar' && config.xAxis.type === 'date-time' && region.fromType === 'Previous Days') {
95
- from = from - (barWidth * totalBarsInGroup) / 2
263
+ const getBarToValue_Date = (region: Region): number => {
264
+ if (region.toType === 'Last Date') {
265
+ return calculateBarLastDatePosition_Date()
96
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)
97
274
 
98
- return from
275
+ return to + barWidth * totalBarsInGroup
99
276
  }
100
277
 
101
- const getToValue = region => {
102
- 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
+ }
103
289
 
104
- // when xScale is categorical leading zeros are removed, ie. 03/15/2016 is 3/15/2016
105
- if (xAxis.type === 'categorical') {
106
- to = xScale(region.to)
290
+ const getBarToValue_DateTime = (region: Region): number => {
291
+ if (region.toType === 'Last Date') {
292
+ return calculateBarLastDatePosition_DateTime()
107
293
  }
294
+ let to = xScale(parseDate(region.to).getTime())
295
+ return to - getBarOffset()
296
+ }
108
297
 
109
- if (isDateScale(xAxis)) {
110
- if (!region?.toType || region.toType === 'Fixed') {
111
- to = xScale(parseDate(region.to).getTime())
112
- }
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
+ }
113
325
 
114
- if (visualizationType === 'Bar' || config.visualizationType === 'Combo') {
115
- 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)
116
342
  }
117
343
  }
118
- if (region.toType === 'Last Date') {
119
- const lastDate = domain[domain.length - 1]
120
- to = Number(
121
- xScale(lastDate) +
122
- ((visualizationType === 'Bar' || visualizationType === 'Combo') && config.xAxis.type === 'date'
123
- ? barWidth * totalBarsInGroup
124
- : 0)
125
- )
126
- }
127
-
128
- if (visualizationType === 'Line' || visualizationType === 'Area Chart') {
129
- let scalePadding = Number(config.yAxis.size)
130
- if (xScale.bandwidth) {
131
- 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)
132
353
  }
133
- to = to + scalePadding
134
354
  }
135
355
 
136
- if (visualizationType === 'Bar' && config.xAxis.type === 'date-time' && region.toType !== 'Last Date') {
137
- 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
+ }
138
372
  }
139
373
 
140
- if ((visualizationType === 'Bar' || visualizationType === 'Combo') && xAxis.type === 'categorical') {
141
- 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
+ }
142
383
  }
143
- return to
384
+
385
+ return 0
144
386
  }
145
387
 
146
- const getWidth = (to, from) => to - from
388
+ const getWidth = (to: number, from: number): number => Math.max(0, to - from)
147
389
 
148
- if (regions && orientation === 'vertical') {
149
- return regions.map(region => {
150
- const from = getFromValue(region)
151
- const to = getToValue(region)
152
- const width = getWidth(to, from)
390
+ if (!regions || orientation !== 'vertical') return null
153
391
 
154
- if (!from) return null
155
- if (!to) return null
392
+ const chartStart = Number(config.yAxis.size || 0)
393
+ const chartEnd = xMax !== undefined ? chartStart + xMax : chartStart + 1000
156
394
 
157
- const HighlightedArea = () => {
158
- return <rect x={from} y={0} width={width} height={yMax} fill={region.background} opacity={0.3} />
159
- }
395
+ return regions.map((region: Region) => {
396
+ const from = getFromValue(region)
397
+ const to = getToValue(region)
160
398
 
161
- return (
162
- <Group
163
- height={100}
164
- fill='red'
165
- className='regions regions-group--line zzz'
166
- key={region.label}
167
- pointerEvents='none'
168
- >
169
- <HighlightedArea />
170
- <Text x={from + width / 2} y={5} fill={region.color} verticalAnchor='start' textAnchor='middle'>
171
- {region.label}
172
- </Text>
173
- </Group>
174
- )
175
- })
176
- }
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
+ })
177
420
  }
178
421
 
179
422
  export default Regions
@@ -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]) =>
@@ -114,7 +114,11 @@ const SmallMultipleTile: React.FC<SmallMultipleTileProps> = ({
114
114
  hideYAxisLabel: !isFirstInRow,
115
115
  legend: {
116
116
  ...tileConfig.legend,
117
- hide: true
117
+ hide: true // Hide legends for all small multiple tiles
118
+ },
119
+ xAxis: {
120
+ ...tileConfig.xAxis,
121
+ brushActive: false // Hide brush slider for all small multiple tiles
118
122
  },
119
123
  showAreaUnderLine: config.smallMultiples?.showAreaUnderLine || false
120
124
  }