@cdc/chart 4.26.7 → 4.26.10

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 (255) hide show
  1. package/CONFIG.md +502 -368
  2. package/dist/cdcchart-CYc-Kayd.es.js +22488 -0
  3. package/dist/cdcchart-DCKn8NmI.es.js +20695 -0
  4. package/dist/cdcchart-Dyui9nk_.es.js +45828 -0
  5. package/dist/cdcchart.js +4 -97406
  6. package/examples/__data__/vega-remote-data.json +6 -0
  7. package/examples/dendrogram.json +18 -0
  8. package/examples/feature/bar/bar-chart-race-annual-change.json +68 -0
  9. package/examples/feature/heatmap/average-age-categorical.json +2 -2
  10. package/examples/feature/heatmap/calendar-heatmap.json +2 -2
  11. package/examples/feature/heatmap/cell-values-bottom-axis.json +2 -2
  12. package/examples/feature/heatmap/sparse-aggregation.json +2 -2
  13. package/examples/feature/line/line-chart-race.json +50 -0
  14. package/examples/feature/sankey/sankey-example-data.json +40 -1441
  15. package/examples/feature/sankey/sankey_chart_data.csv +9 -20
  16. package/examples/network.json +31 -0
  17. package/examples/private/box-plot.json +573 -0
  18. package/examples/private/custom-color-issue.json +625 -0
  19. package/examples/private/custom-colors.json +522 -0
  20. package/examples/private/cutoff.json +10750 -0
  21. package/examples/private/overlay-legend.json +485 -0
  22. package/examples/private/pie-test.json +469 -0
  23. package/examples/vega-remote.json +22 -0
  24. package/package.json +11 -5
  25. package/src/CdcChartComponent.tsx +405 -208
  26. package/src/_stories/Chart.BarRace.stories.tsx +443 -0
  27. package/src/_stories/Chart.BoxPlotNumericSort.stories.tsx +189 -0
  28. package/src/_stories/Chart.BoxPlotStatsParity.stories.tsx +207 -0
  29. package/src/_stories/Chart.ColorDistribution.stories.tsx +299 -0
  30. package/src/_stories/Chart.DataCutoff.stories.tsx +147 -0
  31. package/src/_stories/Chart.DataTableDownloadFilename.stories.tsx +60 -0
  32. package/src/_stories/Chart.Dendrogram.stories.tsx +373 -0
  33. package/src/_stories/Chart.Forecast.CILegend.stories.tsx +236 -0
  34. package/src/_stories/Chart.HeatMap.stories.tsx +170 -4
  35. package/src/_stories/Chart.Legend.AxisAlign.stories.tsx +119 -0
  36. package/src/_stories/Chart.LineRace.stories.tsx +149 -0
  37. package/src/_stories/Chart.Network.stories.tsx +432 -0
  38. package/src/_stories/Chart.PaletteConversion.Editor.stories.tsx +208 -0
  39. package/src/_stories/Chart.Patterns.smoke.stories.tsx +31 -0
  40. package/src/_stories/Chart.PercentageValue.stories.tsx +311 -0
  41. package/src/_stories/Chart.PieLabels.stories.tsx +4 -2
  42. package/src/_stories/Chart.RankByValueMutation.stories.tsx +206 -0
  43. package/src/_stories/Chart.RequiredFields.stories.tsx +681 -0
  44. package/src/_stories/Chart.Sankey.stories.tsx +417 -0
  45. package/src/_stories/Chart.SeriesColorAssignments.stories.tsx +47 -0
  46. package/src/_stories/Chart.SmallMultiplesNumericTileOrder.stories.tsx +137 -0
  47. package/src/_stories/Chart.TooltipSeriesLabels.stories.tsx +43 -0
  48. package/src/_stories/Chart.VegaRemoteData.stories.tsx +31 -0
  49. package/src/_stories/ChartAnnotation.Editor.stories.tsx +78 -0
  50. package/src/_stories/ChartAxisTitles.smoke.stories.tsx +48 -0
  51. package/src/_stories/ChartBar.Editor.stories.tsx +499 -57
  52. package/src/_stories/ChartBrush.Editor.stories.tsx +46 -67
  53. package/src/_stories/ChartBrush.Regression.stories.tsx +135 -0
  54. package/src/_stories/ChartEditor.Editor.stories.tsx +33 -3
  55. package/src/_stories/ChartLine.EffectCompatibility.stories.tsx +120 -0
  56. package/src/_stories/_mock/annotation_event-line_mock.json +2 -2
  57. package/src/_stories/_mock/editor-test-configs.json +20 -10
  58. package/src/_stories/_mock/editor-test-datasets.json +38 -42
  59. package/src/_stories/_mock/heatmap-average-age-categorical.json +2 -2
  60. package/src/_stories/_mock/heatmap.json +2 -2
  61. package/src/_stories/_mock/legend-axis-align-stacked-bar.json +125 -0
  62. package/src/_stories/_mock/sankey_mock.json +23 -27
  63. package/src/components/Annotations/components/AnnotationDraggable.styles.css +52 -0
  64. package/src/components/Annotations/components/AnnotationDraggable.tsx +639 -139
  65. package/src/components/Annotations/components/AnnotationDropdown.styles.css +44 -4
  66. package/src/components/Annotations/components/AnnotationDropdown.tsx +10 -32
  67. package/src/components/Annotations/helpers/resolveAnnotationLayout.ts +212 -0
  68. package/src/components/Annotations/tests/AnnotationDraggable.test.tsx +612 -19
  69. package/src/components/Annotations/tests/AnnotationDropdown.test.tsx +50 -0
  70. package/src/components/Annotations/tests/resolveAnnotationLayout.test.ts +203 -0
  71. package/src/components/Axis/BottomAxis.tsx +1 -1
  72. package/src/components/Axis/Categorical.Axis.tsx +3 -3
  73. package/src/components/Axis/RightAxis.tsx +21 -13
  74. package/src/components/BarChart/components/BarChart.Horizontal.tsx +38 -6
  75. package/src/components/BarChart/components/BarChart.StackedHorizontal.tsx +12 -3
  76. package/src/components/BarChart/components/BarChart.StackedVertical.test.ts +28 -0
  77. package/src/components/BarChart/components/BarChart.StackedVertical.tsx +29 -25
  78. package/src/components/BarChart/components/BarChart.Vertical.tsx +37 -26
  79. package/src/components/BarChart/components/PortionPatternOverlay.tsx +61 -0
  80. package/src/components/BarChart/components/context.tsx +6 -0
  81. package/src/components/BarChart/helpers/getPatternUrl.ts +2 -5
  82. package/src/components/BarChart/helpers/index.ts +8 -0
  83. package/src/components/BarChart/helpers/portionPattern.ts +243 -0
  84. package/src/components/BarChart/helpers/tests/addMinimumBarHeights.test.ts +42 -0
  85. package/src/components/BarChart/helpers/tests/getPatternUrl.test.ts +13 -0
  86. package/src/components/BarChart/helpers/tests/portionPattern.test.ts +280 -0
  87. package/src/components/BarChart/helpers/tests/useBarChart.test.tsx +104 -0
  88. package/src/components/BarChart/helpers/useBarChart.ts +60 -59
  89. package/src/components/BarChartRace/BarChartRace.test.tsx +199 -0
  90. package/src/components/BarChartRace/BarChartRace.tsx +230 -0
  91. package/src/components/BarChartRace/BarChartRaceFallback.test.tsx +61 -0
  92. package/src/components/BarChartRace/BarChartRaceFallback.tsx +48 -0
  93. package/src/components/BarChartRace/bar-chart-race.css +160 -0
  94. package/src/components/BarChartRace/helpers.test.ts +114 -0
  95. package/src/components/BarChartRace/helpers.ts +185 -0
  96. package/src/components/BarChartRace/index.ts +3 -0
  97. package/src/components/BoxPlot/helpers/index.ts +11 -82
  98. package/src/components/Brush/BrushSelector.tsx +9 -23
  99. package/src/components/Brush/MiniChartPreview.tsx +33 -1
  100. package/src/components/Dendrogram/components/Dendrogram.tsx +374 -0
  101. package/src/components/Dendrogram/dendrogram.css +113 -0
  102. package/src/components/Dendrogram/helpers/layoutDendrogram.test.ts +95 -0
  103. package/src/components/Dendrogram/helpers/layoutDendrogram.ts +56 -0
  104. package/src/components/Dendrogram/helpers/prepareDendrogramData.test.ts +135 -0
  105. package/src/components/Dendrogram/helpers/prepareDendrogramData.ts +166 -0
  106. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.test.ts +196 -0
  107. package/src/components/Dendrogram/helpers/resolveDendrogramLabels.ts +418 -0
  108. package/src/components/Dendrogram/index.tsx +1 -0
  109. package/src/components/Dendrogram/types/index.ts +66 -0
  110. package/src/components/EditorPanel/EditorPanel.tsx +1005 -760
  111. package/src/components/EditorPanel/components/Panels/Panel.Annotate.tsx +50 -5
  112. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.css +10 -0
  113. package/src/components/EditorPanel/components/Panels/Panel.Dendrogram.tsx +211 -0
  114. package/src/components/EditorPanel/components/Panels/Panel.ForestPlotSettings.tsx +19 -8
  115. package/src/components/EditorPanel/components/Panels/Panel.General.tsx +88 -54
  116. package/src/components/EditorPanel/components/Panels/Panel.Network.css +9 -0
  117. package/src/components/EditorPanel/components/Panels/Panel.Network.tsx +202 -0
  118. package/src/components/EditorPanel/components/Panels/Panel.PatternSettings.tsx +233 -33
  119. package/src/components/EditorPanel/components/Panels/Panel.Radar.tsx +6 -1
  120. package/src/components/EditorPanel/components/Panels/Panel.Sankey.tsx +89 -105
  121. package/src/components/EditorPanel/components/Panels/Panel.Series.tsx +2 -2
  122. package/src/components/EditorPanel/components/Panels/Panel.SmallMultiples.tsx +1 -1
  123. package/src/components/EditorPanel/components/Panels/Panel.Visual.tsx +8 -66
  124. package/src/components/EditorPanel/components/Panels/RacingControls.test.tsx +131 -0
  125. package/src/components/EditorPanel/components/Panels/RacingControls.tsx +185 -0
  126. package/src/components/EditorPanel/components/Panels/SeriesColorAssignments.tsx +2 -2
  127. package/src/components/EditorPanel/components/Panels/index.tsx +4 -0
  128. package/src/components/EditorPanel/helpers/getVisualizationTypeConfigUpdate.ts +20 -7
  129. package/src/components/EditorPanel/helpers/tests/getVisualizationTypeConfigUpdate.test.ts +25 -2
  130. package/src/components/EditorPanel/helpers/updateFieldRankByValue.ts +2 -1
  131. package/src/components/EditorPanel/useEditorPermissions.test.tsx +62 -0
  132. package/src/components/EditorPanel/useEditorPermissions.ts +70 -24
  133. package/src/components/Forecasting/Forecasting.tsx +15 -6
  134. package/src/components/HeatMap/components/HeatMap.tsx +511 -178
  135. package/src/components/HeatMap/heatmap.constants.ts +7 -3
  136. package/src/components/HeatMap/heatmap.css +101 -2
  137. package/src/components/HeatMap/helpers/layout.ts +80 -0
  138. package/src/components/HeatMap/tests/HeatMap.test.tsx +339 -16
  139. package/src/components/HeatMap/tests/helpers.test.ts +79 -0
  140. package/src/components/Legend/Legend.Component.tsx +110 -90
  141. package/src/components/Legend/LegendGroup/LegendGroup.styles.css +7 -0
  142. package/src/components/Legend/LegendGroup/LegendGroup.tsx +47 -34
  143. package/src/components/Legend/helpers/createFormatLabels.test.ts +226 -0
  144. package/src/components/Legend/helpers/createFormatLabels.tsx +45 -51
  145. package/src/components/LineChart/components/LineChart.Circle.tsx +3 -3
  146. package/src/components/LineChart/helpers.test.ts +126 -38
  147. package/src/components/LineChart/helpers.ts +9 -4
  148. package/src/components/LineChart/index.tsx +69 -24
  149. package/src/components/LineChart/tests/LineChart.test.tsx +630 -0
  150. package/src/components/LineChartRace/LineChartRace.test.tsx +275 -0
  151. package/src/components/LineChartRace/LineChartRace.tsx +134 -0
  152. package/src/components/LineChartRace/LineChartRaceFallback.tsx +28 -0
  153. package/src/components/LineChartRace/helpers.test.ts +88 -0
  154. package/src/components/LineChartRace/helpers.ts +157 -0
  155. package/src/components/LineChartRace/index.ts +3 -0
  156. package/src/components/LineChartRace/line-chart-race.css +16 -0
  157. package/src/components/LinearChart/ValueAxisAnchors.tsx +58 -0
  158. package/src/components/LinearChart/VisualizationRenderer.tsx +18 -23
  159. package/src/components/LinearChart/tests/LinearChart.test.tsx +412 -13
  160. package/src/components/LinearChart/tests/VisualizationRenderer.test.tsx +141 -0
  161. package/src/components/LinearChart/tests/mockConfigContext.ts +2 -1
  162. package/src/components/LinearChart.tsx +81 -51
  163. package/src/components/Network/components/Network.tsx +372 -0
  164. package/src/components/Network/helpers/layoutNetwork.test.ts +68 -0
  165. package/src/components/Network/helpers/layoutNetwork.ts +87 -0
  166. package/src/components/Network/helpers/prepareNetworkData.test.ts +116 -0
  167. package/src/components/Network/helpers/prepareNetworkData.ts +101 -0
  168. package/src/components/Network/helpers/resolveNetworkLabels.test.ts +112 -0
  169. package/src/components/Network/helpers/resolveNetworkLabels.ts +206 -0
  170. package/src/components/Network/index.tsx +3 -0
  171. package/src/components/Network/network.css +107 -0
  172. package/src/components/Network/types/index.ts +56 -0
  173. package/src/components/PairedBarChart.jsx +5 -10
  174. package/src/components/PairedBarChart.test.tsx +71 -0
  175. package/src/components/PieChart/PieChart.tsx +9 -8
  176. package/src/components/RacingChartRenderer.tsx +44 -0
  177. package/src/components/Regions/components/Regions.test.tsx +179 -0
  178. package/src/components/Regions/components/Regions.tsx +104 -69
  179. package/src/components/Regions/index.tsx +2 -3
  180. package/src/components/Sankey/components/Sankey.tsx +264 -437
  181. package/src/components/Sankey/helpers/getSankeyTooltip.ts +12 -0
  182. package/src/components/Sankey/helpers/prepareSankeyData.test.ts +71 -0
  183. package/src/components/Sankey/helpers/prepareSankeyData.ts +90 -0
  184. package/src/components/Sankey/sankey.scss +57 -116
  185. package/src/components/Sankey/types/index.ts +41 -4
  186. package/src/components/ScatterPlot/ScatterPlot.jsx +28 -4
  187. package/src/components/ScatterPlot/ScatterPlot.test.tsx +60 -0
  188. package/src/components/SmallMultiples/SmallMultiples.tsx +12 -12
  189. package/src/components/SmallMultiples/tests/SmallMultipleTile.test.tsx +7 -5
  190. package/src/components/raceTiming.ts +18 -0
  191. package/src/data/initial-state.js +77 -31
  192. package/src/helpers/applyChartTypeContract.ts +71 -0
  193. package/src/helpers/axisLabelFontSize.ts +2 -2
  194. package/src/helpers/boxPlotStats.ts +60 -0
  195. package/src/helpers/bumpChartTooltip.ts +2 -1
  196. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.test.ts +37 -0
  197. package/src/helpers/calculateHorizontalBarCategoryLabelWidth.ts +3 -2
  198. package/src/helpers/calculateRightYAxisWidth.ts +60 -0
  199. package/src/helpers/colorAssignmentHelpers.ts +2 -2
  200. package/src/helpers/createNewChartConfig.ts +56 -0
  201. package/src/helpers/dataDrivenYAxisCategories.ts +18 -0
  202. package/src/helpers/forecastLegend.ts +88 -0
  203. package/src/helpers/getBoxPlotConfig.ts +18 -37
  204. package/src/helpers/getChartTypeDefaultPalette.ts +29 -0
  205. package/src/helpers/getColorCodeCategoryColorMap.ts +26 -0
  206. package/src/helpers/getColorScale.ts +31 -22
  207. package/src/helpers/getDynamicYAxisCategories.ts +69 -0
  208. package/src/helpers/getMinMax.ts +9 -7
  209. package/src/helpers/getNewRuntime.ts +5 -2
  210. package/src/helpers/getStackedSeriesMax.ts +51 -0
  211. package/src/helpers/getV21ChartDistributionColors.ts +57 -0
  212. package/src/helpers/handleRankByValue.ts +15 -15
  213. package/src/helpers/seriesColumnSettings.ts +10 -0
  214. package/src/helpers/sizeHelpers.ts +4 -3
  215. package/src/helpers/smallMultiplesHelpers.ts +10 -13
  216. package/src/helpers/tests/applyChartTypeContract.test.ts +37 -0
  217. package/src/helpers/tests/boxPlotStats.test.ts +31 -0
  218. package/src/helpers/tests/bumpChartTooltip.test.ts +29 -0
  219. package/src/helpers/tests/calculateRightYAxisWidth.test.ts +105 -0
  220. package/src/helpers/tests/countNumOfTicks.test.ts +11 -0
  221. package/src/helpers/tests/createNewChartConfig.test.ts +182 -0
  222. package/src/helpers/tests/getBoxPlotConfig.test.ts +70 -0
  223. package/src/helpers/tests/getColorCodeCategoryColorMap.test.ts +113 -0
  224. package/src/helpers/tests/getColorScale.test.ts +226 -1
  225. package/src/helpers/tests/getDynamicYAxisCategories.test.ts +61 -0
  226. package/src/helpers/tests/getMinMax.test.ts +41 -0
  227. package/src/helpers/tests/getStackedSeriesMax.test.ts +26 -0
  228. package/src/helpers/tests/handleRankByValue.test.ts +55 -37
  229. package/src/helpers/tests/seriesColumnSettings.test.ts +23 -0
  230. package/src/helpers/tests/smallMultiplesHelpers.test.ts +37 -1
  231. package/src/helpers/tests/tooltipHelpers.test.ts +80 -0
  232. package/src/helpers/tooltipHelpers.ts +16 -0
  233. package/src/hooks/tests/useTooltip.test.tsx +353 -1
  234. package/src/hooks/usePrefersReducedMotion.ts +22 -0
  235. package/src/hooks/useReduceData.ts +4 -36
  236. package/src/hooks/useScales.ts +15 -0
  237. package/src/hooks/useTooltip.tsx +95 -64
  238. package/src/scss/main.scss +74 -2
  239. package/src/store/chart.reducer.ts +4 -3
  240. package/src/test/CdcChart.dataTable.test.tsx +490 -16
  241. package/src/test/CdcChart.dendrogram.test.tsx +277 -0
  242. package/src/test/CdcChart.finalization.test.tsx +143 -0
  243. package/src/test/CdcChart.network.test.tsx +271 -0
  244. package/src/types/ChartConfig.ts +91 -54
  245. package/src/types/ChartContext.ts +5 -0
  246. package/src/types/Horizon.ts +1 -1
  247. package/LICENSE +0 -201
  248. package/dist/cdcchart-BWVIUcAF.es.js +0 -4802
  249. package/src/_stories/Chart.Sankey.smoke.stories.tsx +0 -62
  250. package/src/components/Sankey/components/ColumnList.tsx +0 -19
  251. package/src/components/Sankey/helpers/getSankeyTooltip.tsx +0 -34
  252. package/src/components/Sankey/useSankeyAlert.tsx +0 -60
  253. package/src/data/legacy-defaults.ts +0 -18
  254. package/src/helpers/ensureSpecialChartAxisTypes.ts +0 -11
  255. package/src/helpers/tests/ensureSpecialChartAxisTypes.test.ts +0 -30
@@ -7,12 +7,13 @@ import { getLegendClasses } from './helpers/getLegendClasses'
7
7
  import { useHighlightedBars } from '../../hooks/useHighlightedBars'
8
8
  import { getMarginTop, getGradientConfig, getMarginBottom } from './helpers/index'
9
9
  import { Label } from '../../types/Label'
10
- import { ChartConfig, ViewportSize } from '../../types/ChartConfig'
10
+ import { ChartConfig } from '../../types/ChartConfig'
11
11
  import { ColorScale } from '../../types/ChartContext'
12
12
  import { forwardRef } from 'react'
13
13
  import LegendSuppression from './Legend.Suppression'
14
14
  import LegendGradient from '@cdc/core/components/Legend/Legend.Gradient'
15
15
  import { DimensionsType } from '@cdc/core/types/Dimensions'
16
+ import type { ViewPort } from '@cdc/core/types/ViewPort'
16
17
  import { isLegendWrapViewport } from '@cdc/core/helpers/viewports'
17
18
  import LegendLineShape from './LegendLine.Shape'
18
19
  import LegendGroup from './LegendGroup'
@@ -28,7 +29,7 @@ const LEGEND_PADDING = 36
28
29
  interface LegendProps {
29
30
  colorScale: ColorScale
30
31
  config: ChartConfig
31
- currentViewport: ViewportSize
32
+ currentViewport: ViewPort
32
33
  formatLabels: (labels: Label[]) => Label[]
33
34
  formatNumber?: (value: number, axis?: string) => string
34
35
  highlight: Function
@@ -183,109 +184,122 @@ const Legend: React.FC<LegendProps> = forwardRef(
183
184
 
184
185
  <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
185
186
  {labels => {
187
+ const formattedLabels = formatLabels(labels as Label[]).filter(
188
+ label => dontFilterLegendItems || seriesWithData.includes(label.datum)
189
+ )
190
+ const visibleForecastLegendKeys = [
191
+ ...formattedLabels
192
+ .filter(label => !config.exclusions.active || !config.exclusions.keys?.includes(label.datum))
193
+ .map(label => label.datum),
194
+ ...highLightedLegendItems.map(item => item.legendLabel).filter(Boolean)
195
+ ]
196
+ const allForecastLegendItemsSelected =
197
+ config.visualizationType === 'Forecasting' &&
198
+ visibleForecastLegendKeys.length > 0 &&
199
+ visibleForecastLegendKeys.every(key => seriesHighlight.includes(key))
200
+ const showAllButton = seriesHighlight.length > 0 && !allForecastLegendItemsSelected
201
+
186
202
  return (
187
203
  <>
188
204
  <div className={innerClasses.join(' ')}>
189
- {formatLabels(labels as Label[])
190
- .filter(label => dontFilterLegendItems || seriesWithData.includes(label.datum))
191
- .map((label, i) => {
192
- let className = ['legend-item', `legend-text--${label.text.replace(' ', '').toLowerCase()}`]
193
- let itemName = label.datum
194
-
195
- // Filter excluded data keys from legend
196
- if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
197
- return null
198
- }
205
+ {formattedLabels.map((label, i) => {
206
+ let className = [
207
+ 'legend-item',
208
+ 'legend-item--interactive',
209
+ `legend-text--${label.text.replace(' ', '').toLowerCase()}`
210
+ ]
211
+ let itemName = label.datum
199
212
 
200
- if (runtime.seriesLabels) {
201
- let index = config.runtime.seriesLabelsAll.indexOf(itemName)
202
- itemName = config.runtime.seriesKeys[index]
213
+ // Filter excluded data keys from legend
214
+ if (config.exclusions.active && config.exclusions.keys?.includes(itemName)) {
215
+ return null
216
+ }
203
217
 
204
- if (runtime?.forecastingSeriesKeys?.length > 0) {
205
- itemName = label.text
206
- }
207
- }
218
+ if (runtime.seriesLabels && !runtime?.forecastingSeriesKeys?.length) {
219
+ let index = config.runtime.seriesLabelsAll.indexOf(itemName)
220
+ itemName = config.runtime.seriesKeys[index]
221
+ }
208
222
 
209
- if (seriesHighlight.length) {
210
- if (!seriesHighlight.includes(itemName)) {
211
- className.push('inactive')
212
- } else className.push('highlighted')
213
- }
223
+ if (seriesHighlight.length) {
224
+ if (!seriesHighlight.includes(itemName)) {
225
+ className.push('inactive')
226
+ } else className.push('highlighted')
227
+ }
214
228
 
215
- if (
216
- config.legend.style === 'gradient' ||
217
- config.legend.groupBy ||
218
- config.visualizationType === 'Horizon Chart'
219
- ) {
220
- return null
221
- }
229
+ if (
230
+ config.legend.style === 'gradient' ||
231
+ config.legend.groupBy ||
232
+ config.visualizationType === 'Horizon Chart'
233
+ ) {
234
+ return null
235
+ }
222
236
 
223
- return (
224
- <LegendItem
225
- className={className.join(' ')}
226
- tabIndex={0}
227
- key={`legend-quantile-${i}`}
228
- onKeyDown={e => {
229
- if (e.key === 'Enter') {
230
- e.preventDefault()
231
- publishAnalyticsEvent({
232
- vizType: config?.type,
233
- vizSubType: getVizSubType(config),
234
- vizTitle: getVizTitle(config),
235
- eventType: `chart_legend_item_toggled` as any,
236
- eventAction: 'keydown',
237
- eventLabel: interactionLabel,
238
- specifics:
239
- config.visualizationType === 'Bar'
240
- ? `label: ${label.text}, orientation: ${
241
- config.orientation === 'horizontal' ? 'horizontal' : 'vertical'
242
- }, mode: ${legend.behavior}`
243
- : `label: ${label.text}, mode: ${legend.behavior}`
244
- })
245
- highlight(label)
246
- }
247
- }}
248
- onClick={e => {
237
+ return (
238
+ <LegendItem
239
+ className={className.join(' ')}
240
+ tabIndex={0}
241
+ key={`legend-quantile-${i}`}
242
+ onKeyDown={e => {
243
+ if (e.key === 'Enter') {
249
244
  e.preventDefault()
250
245
  publishAnalyticsEvent({
251
246
  vizType: config?.type,
252
247
  vizSubType: getVizSubType(config),
248
+ vizTitle: getVizTitle(config),
253
249
  eventType: `chart_legend_item_toggled` as any,
254
- eventAction: 'click',
250
+ eventAction: 'keydown',
255
251
  eventLabel: interactionLabel,
256
252
  specifics:
257
253
  config.visualizationType === 'Bar'
258
254
  ? `label: ${label.text}, orientation: ${
259
255
  config.orientation === 'horizontal' ? 'horizontal' : 'vertical'
260
256
  }, mode: ${legend.behavior}`
261
- : `label: ${label.text}, mode: ${legend.behavior}`,
262
-
263
- vizTitle: getVizTitle(config)
257
+ : `label: ${label.text}, mode: ${legend.behavior}`
264
258
  })
265
259
  highlight(label)
266
- }}
267
- role='button'
268
- >
269
- <>
270
- {config.visualizationType === 'Line' && config.legend.style === 'lines' ? (
271
- <React.Fragment>
272
- <LegendLineShape index={i} label={label} config={config} />
273
- </React.Fragment>
274
- ) : (
275
- <>
276
- <LegendShape
277
- shape={config.legend.style === 'boxes' ? 'square' : 'circle'}
278
- fill={label.value}
279
- />
280
- </>
281
- )}
282
- </>
283
- <LegendLabel align='left' className='m-0'>
284
- {parse(label.text)}
285
- </LegendLabel>
286
- </LegendItem>
287
- )
288
- })}
260
+ }
261
+ }}
262
+ onClick={e => {
263
+ e.preventDefault()
264
+ publishAnalyticsEvent({
265
+ vizType: config?.type,
266
+ vizSubType: getVizSubType(config),
267
+ eventType: `chart_legend_item_toggled` as any,
268
+ eventAction: 'click',
269
+ eventLabel: interactionLabel,
270
+ specifics:
271
+ config.visualizationType === 'Bar'
272
+ ? `label: ${label.text}, orientation: ${
273
+ config.orientation === 'horizontal' ? 'horizontal' : 'vertical'
274
+ }, mode: ${legend.behavior}`
275
+ : `label: ${label.text}, mode: ${legend.behavior}`,
276
+
277
+ vizTitle: getVizTitle(config)
278
+ })
279
+ highlight(label)
280
+ }}
281
+ role='button'
282
+ >
283
+ <>
284
+ {config.visualizationType === 'Line' && config.legend.style === 'lines' ? (
285
+ <React.Fragment>
286
+ <LegendLineShape index={i} label={label} config={config} />
287
+ </React.Fragment>
288
+ ) : (
289
+ <>
290
+ <LegendShape
291
+ shape={config.legend.style === 'boxes' ? 'square' : 'circle'}
292
+ fill={label.value}
293
+ />
294
+ </>
295
+ )}
296
+ </>
297
+ <LegendLabel align='left' className='m-0'>
298
+ {parse(label.text)}
299
+ </LegendLabel>
300
+ </LegendItem>
301
+ )
302
+ })}
289
303
 
290
304
  {highLightedLegendItems.map((bar, i) => {
291
305
  // if duplicates only return first item
@@ -325,6 +339,12 @@ const Legend: React.FC<LegendProps> = forwardRef(
325
339
  })}
326
340
  </div>
327
341
 
342
+ {showAllButton && (
343
+ <Button size='sm' onClick={labels => handleShowAll(labels)} style={{ marginTop: '1rem' }}>
344
+ Show All
345
+ </Button>
346
+ )}
347
+
328
348
  <LegendSuppression config={config} isLegendBottom={isLegendBottom} />
329
349
 
330
350
  {/* Pattern Legend Items */}
@@ -338,6 +358,11 @@ const Legend: React.FC<LegendProps> = forwardRef(
338
358
  const patternId = `legend-pattern-${key}`
339
359
  const pColor = (pattern as any)?.color || '#666666'
340
360
  const patternSize = (pattern as any)?.patternSize ?? 10
361
+ const targetSeriesLabel =
362
+ pattern.application === 'portion' && pattern.dataKey
363
+ ? config.runtime?.seriesLabels?.[pattern.dataKey]
364
+ : undefined
365
+ const patternBackground = targetSeriesLabel ? colorScale(targetSeriesLabel) : 'white'
341
366
 
342
367
  return (
343
368
  <LegendItem
@@ -348,7 +373,7 @@ const Legend: React.FC<LegendProps> = forwardRef(
348
373
  >
349
374
  <LegendShape
350
375
  shape={config.legend.style === 'boxes' ? 'square' : 'circle'}
351
- fill='white'
376
+ fill={patternBackground}
352
377
  patternInfo={{
353
378
  pattern: pattern.shape || 'circles',
354
379
  patternId,
@@ -368,11 +393,6 @@ const Legend: React.FC<LegendProps> = forwardRef(
368
393
  )
369
394
  }}
370
395
  </LegendOrdinal>
371
- {seriesHighlight.length > 0 && (
372
- <Button onClick={labels => handleShowAll(labels)} style={{ marginTop: '1rem' }}>
373
- Show All
374
- </Button>
375
- )}
376
396
  </aside>
377
397
  )
378
398
  }
@@ -10,6 +10,7 @@
10
10
  font-weight: 400;
11
11
  margin-bottom: 0.5rem;
12
12
  }
13
+
13
14
  .group-label {
14
15
  font-family: Nunito, sans-serif;
15
16
  font-size: 1rem;
@@ -17,6 +18,12 @@
17
18
  }
18
19
  }
19
20
 
21
+ .legend-group__items {
22
+ display: flex;
23
+ flex-direction: column;
24
+ row-gap: 0.25rem;
25
+ }
26
+
20
27
  .legend-group-grid {
21
28
  display: flex;
22
29
  flex-wrap: wrap;
@@ -57,43 +57,56 @@ const LegendGroup = ({ formatLabels }) => {
57
57
  <p className='legend-group group-label'>{group}</p>
58
58
  </div>
59
59
  <LegendOrdinal scale={colorScale} itemDirection='row' labelMargin='0 20px 0 0' shapeMargin='0 10px 0'>
60
- {labels =>
61
- formatLabels(labels)
62
- .filter(label => {
63
- const groupBy = config.legend.groupBy || ''
64
- return data.some(d => d[groupBy] === group && d[label.text] !== undefined && d[label.text] !== 'NA')
65
- })
60
+ {labels => (
61
+ <div className='legend-group__items'>
62
+ {formatLabels(labels)
63
+ .filter(label => {
64
+ const groupBy = config.legend.groupBy || ''
65
+ return data.some(
66
+ d => d[groupBy] === group && d[label.text] !== undefined && d[label.text] !== 'NA'
67
+ )
68
+ })
66
69
 
67
- .map((label, i) => {
68
- let className = ['legend-group', 'group-item']
69
- if (seriesHighlight.length) {
70
- if (!seriesHighlight.includes(label.datum)) {
71
- className.push('inactive')
72
- } else {
73
- className.push('highlighted')
70
+ .map((label, i) => {
71
+ let className = ['legend-group', 'group-item', 'legend-item--interactive']
72
+ if (seriesHighlight.length) {
73
+ if (!seriesHighlight.includes(label.datum)) {
74
+ className.push('inactive')
75
+ } else {
76
+ className.push('highlighted')
77
+ }
74
78
  }
75
- }
76
79
 
77
- return (
78
- <LegendItem
79
- alignItems='start'
80
- className={className.join(' ')}
81
- onClick={e => {
82
- e.preventDefault()
83
- highlight(label)
84
- }}
85
- key={`legend-item-${i}`}
86
- tabIndex={0}
87
- role='button'
88
- >
89
- <LegendShape shape={config.legend.style === 'boxes' ? 'square' : 'circle'} fill={label.value} />
90
- <LegendLabel align='left' margin='0'>
91
- {label.text}
92
- </LegendLabel>
93
- </LegendItem>
94
- )
95
- })
96
- }
80
+ return (
81
+ <LegendItem
82
+ alignItems='start'
83
+ className={className.join(' ')}
84
+ onClick={e => {
85
+ e.preventDefault()
86
+ highlight(label)
87
+ }}
88
+ onKeyDown={e => {
89
+ if (e.key === 'Enter') {
90
+ e.preventDefault()
91
+ highlight(label)
92
+ }
93
+ }}
94
+ key={`legend-item-${i}`}
95
+ tabIndex={0}
96
+ role='button'
97
+ >
98
+ <LegendShape
99
+ shape={config.legend.style === 'boxes' ? 'square' : 'circle'}
100
+ fill={label.value}
101
+ />
102
+ <LegendLabel align='left' margin='0'>
103
+ {label.text}
104
+ </LegendLabel>
105
+ </LegendItem>
106
+ )
107
+ })}
108
+ </div>
109
+ )}
97
110
  </LegendOrdinal>
98
111
  </div>
99
112
  )
@@ -3,6 +3,13 @@ import { describe, expect, it, vi } from 'vitest'
3
3
  import { createFormatLabels } from './createFormatLabels'
4
4
  import { ChartConfig } from '../../../types/ChartConfig'
5
5
  import { Label } from '../../../types/Label'
6
+ import chroma from 'chroma-js'
7
+ import {
8
+ getForecastConfidenceIntervalGroups,
9
+ getForecastConfidenceIntervalKey,
10
+ getForecastLegendKey,
11
+ isForecastLegendSelectionActive
12
+ } from '../../../helpers/forecastLegend'
6
13
 
7
14
  const defaultLabels: Label[] = [
8
15
  { datum: 'Series A', index: 0, text: 'Series A', value: '#aa0000' },
@@ -86,3 +93,222 @@ describe('createFormatLabels small multiples legend colors', () => {
86
93
  expect(labels.map(label => label.value)).toEqual(['#aa0000', '#aa0000'])
87
94
  })
88
95
  })
96
+ describe('createFormatLabels category-colored bar legends', () => {
97
+ it('uses the same consecutive palette colors as category-colored bars', () => {
98
+ const config = buildConfig({
99
+ visualizationType: 'Bar',
100
+ visualizationSubType: 'regular',
101
+ general: {
102
+ palette: {
103
+ name: 'sequential_blue',
104
+ version: '2.0'
105
+ }
106
+ } as any,
107
+ legend: { colorCode: 'Group' } as any,
108
+ smallMultiples: undefined as any,
109
+ series: [{ dataKey: 'value', name: 'Value', type: 'Bar' }] as any
110
+ })
111
+ const tableData = [{ Group: 'Group A' }, { Group: 'Group B' }]
112
+
113
+ const labels = createFormatLabels(config, tableData, tableData, colorScale, vi.fn())(defaultLabels)
114
+
115
+ expect(labels.map(label => [label.text, label.value])).toEqual([
116
+ ['Group A', '#DBE8F7'],
117
+ ['Group B', '#BED5ED']
118
+ ])
119
+ })
120
+ })
121
+
122
+ const buildForecastConfig = (confidenceIntervals: any[]): ChartConfig =>
123
+ buildConfig({
124
+ visualizationType: 'Combo',
125
+ smallMultiples: undefined,
126
+ general: { palette: { name: 'qualitative_standard', version: '2.0' } } as any,
127
+ legend: { expandForecastConfidenceIntervals: true } as any,
128
+ series: [
129
+ {
130
+ dataKey: 'stage',
131
+ type: 'Forecasting',
132
+ confidenceIntervals,
133
+ stages: [
134
+ { key: 'Observed', color: 'sequential-blue' },
135
+ { key: 'Forecast', color: 'sequential-green' }
136
+ ]
137
+ }
138
+ ] as any,
139
+ runtime: {
140
+ seriesKeys: ['Observed', 'Forecast'],
141
+ forecastingSeriesKeys: [
142
+ {
143
+ confidenceIntervals,
144
+ stages: [
145
+ { key: 'Observed', color: 'sequential-blue' },
146
+ { key: 'Forecast', color: 'sequential-green' }
147
+ ]
148
+ }
149
+ ]
150
+ } as any
151
+ })
152
+
153
+ describe('createFormatLabels forecast confidence interval legends', () => {
154
+ it('orders resolved percentages numerically and assigns an independent identity to each interval and stage', () => {
155
+ const labels = formatLabels(
156
+ buildForecastConfig([
157
+ { low: 'Lower 90%', high: 'Upper 90%' },
158
+ { low: 'Lower 50%', high: 'Upper 50%' }
159
+ ])
160
+ )
161
+
162
+ expect(labels.map(label => [label.text, label.datum])).toEqual([
163
+ ['50% CI Observed', getForecastLegendKey('Observed', 'percentage:50')],
164
+ ['50% CI Forecast', getForecastLegendKey('Forecast', 'percentage:50')],
165
+ ['90% CI Observed', getForecastLegendKey('Observed', 'percentage:90')],
166
+ ['90% CI Forecast', getForecastLegendKey('Forecast', 'percentage:90')]
167
+ ])
168
+ expect(chroma(labels[0].value).alpha()).toBeCloseTo(0.75)
169
+ expect(chroma(labels[2].value).alpha()).toBeCloseTo(0.5)
170
+ })
171
+
172
+ it('collapses duplicate percentages while retaining their effective shade', () => {
173
+ const labels = formatLabels(
174
+ buildForecastConfig([
175
+ { low: 'Lower 90%', high: 'Upper 90%' },
176
+ { low: 'Low 90', high: 'High 90' }
177
+ ])
178
+ )
179
+
180
+ expect(labels.map(label => label.text)).toEqual(['90% CI Observed', '90% CI Forecast'])
181
+ expect(chroma(labels[0].value).alpha()).toBeCloseTo(0.75)
182
+ })
183
+
184
+ it('uses stable fallbacks and omits incomplete intervals', () => {
185
+ const labels = formatLabels(
186
+ buildForecastConfig([
187
+ { low: 'lower', high: 'upper' },
188
+ { low: '', high: 'Upper 80%' },
189
+ { low: 'minimum', high: 'maximum' }
190
+ ])
191
+ )
192
+
193
+ expect(labels.map(label => label.text)).toEqual([
194
+ 'CI 1 Observed',
195
+ 'CI 1 Forecast',
196
+ 'CI 2 Observed',
197
+ 'CI 2 Forecast'
198
+ ])
199
+ })
200
+
201
+ it('falls back when bounds do not contain the same single percentage', () => {
202
+ const labels = formatLabels(
203
+ buildForecastConfig([
204
+ { low: 'Lower 50%', high: 'Upper 95%' },
205
+ { low: 'Lower 50 to 90%', high: 'Upper 50 to 90%' }
206
+ ])
207
+ )
208
+
209
+ expect(labels.map(label => label.text)).toEqual([
210
+ 'CI 1 Observed',
211
+ 'CI 1 Forecast',
212
+ 'CI 2 Observed',
213
+ 'CI 2 Forecast'
214
+ ])
215
+ })
216
+
217
+ it('keeps a single complete interval stage-only', () => {
218
+ const labels = formatLabels(
219
+ buildForecastConfig([
220
+ { low: 'Lower 50%', high: 'Upper 50%' },
221
+ { low: '', high: 'Upper 90%' }
222
+ ])
223
+ )
224
+
225
+ expect(labels.map(label => [label.text, label.datum])).toEqual([
226
+ ['Observed', 'Observed'],
227
+ ['Forecast', 'Forecast']
228
+ ])
229
+ expect(labels.every(label => chroma(label.value).alpha() === 1)).toBe(true)
230
+ })
231
+
232
+ it.each([undefined, false])('keeps multi-interval legends stage-only when expansion is %s', expansionValue => {
233
+ const config = buildForecastConfig([
234
+ { low: 'Lower 50%', high: 'Upper 50%' },
235
+ { low: 'Lower 90%', high: 'Upper 90%' }
236
+ ])
237
+ config.legend.expandForecastConfidenceIntervals = expansionValue
238
+
239
+ expect(formatLabels(config).map(label => [label.text, label.datum])).toEqual([
240
+ ['Observed', 'Observed'],
241
+ ['Forecast', 'Forecast']
242
+ ])
243
+ })
244
+
245
+ it('does not infer percentages outside the valid percentage range', () => {
246
+ const labels = formatLabels(
247
+ buildForecastConfig([
248
+ { low: 'lower_2024', high: 'upper_2024' },
249
+ { low: 'Lower 50%', high: 'Upper 50%' }
250
+ ])
251
+ )
252
+
253
+ expect(labels.map(label => label.text)).toEqual([
254
+ '50% CI Observed',
255
+ '50% CI Forecast',
256
+ 'CI 1 Observed',
257
+ 'CI 1 Forecast'
258
+ ])
259
+ })
260
+
261
+ it('preserves interval-major numeric ordering when legacy legend ordering options are enabled', () => {
262
+ const config = buildForecastConfig([
263
+ { low: 'Lower 90%', high: 'Upper 90%' },
264
+ { low: 'Lower 50%', high: 'Upper 50%' }
265
+ ])
266
+ config.legend.reverseLabelOrder = true
267
+ config.legend.verticalSorted = true
268
+
269
+ expect(formatLabels(config).map(label => label.text)).toEqual([
270
+ '50% CI Observed',
271
+ '50% CI Forecast',
272
+ '90% CI Observed',
273
+ '90% CI Forecast'
274
+ ])
275
+ })
276
+ })
277
+
278
+ describe('forecast confidence interval interaction identity', () => {
279
+ const confidenceIntervals = [
280
+ { low: 'Lower 90%', high: 'Upper 90%' },
281
+ { low: 'Lower 50%', high: 'Upper 50%' }
282
+ ]
283
+
284
+ it('maps configured intervals to their normalized legend keys', () => {
285
+ expect(getForecastConfidenceIntervalKey(confidenceIntervals, 0)).toBe('percentage:90')
286
+ expect(getForecastConfidenceIntervalKey(confidenceIntervals, 1)).toBe('percentage:50')
287
+ expect(getForecastLegendKey('Forecast: west', 'percentage:50')).toBe(
288
+ 'forecast-ci:Forecast%3A%20west:percentage%3A50'
289
+ )
290
+ })
291
+
292
+ it('activates only a matching composite row while accepting legacy stage selections', () => {
293
+ const selectedKey = getForecastLegendKey('Forecast', 'percentage:50')
294
+
295
+ expect(isForecastLegendSelectionActive([selectedKey], 'Forecast', 'percentage:50')).toBe(true)
296
+ expect(isForecastLegendSelectionActive([selectedKey], 'Forecast', 'percentage:90')).toBe(false)
297
+ expect(isForecastLegendSelectionActive([selectedKey], 'Observed', 'percentage:50')).toBe(false)
298
+ expect(isForecastLegendSelectionActive(['Forecast'], 'Forecast', 'percentage:50')).toBe(true)
299
+ expect(isForecastLegendSelectionActive(['Forecast'], 'Forecast', 'percentage:90')).toBe(true)
300
+ })
301
+
302
+ it('shares one composite identity across duplicate resolved percentages', () => {
303
+ const duplicateIntervals = [
304
+ { low: 'Lower 90%', high: 'Upper 90%' },
305
+ { low: 'Low 90', high: 'High 90' }
306
+ ]
307
+ const groups = getForecastConfidenceIntervalGroups(duplicateIntervals)
308
+
309
+ expect(groups).toHaveLength(1)
310
+ expect(groups[0].intervalIndexes).toEqual([0, 1])
311
+ expect(getForecastConfidenceIntervalKey(duplicateIntervals, 0)).toBe('percentage:90')
312
+ expect(getForecastConfidenceIntervalKey(duplicateIntervals, 1)).toBe('percentage:90')
313
+ })
314
+ })