@automattic/charts 1.10.3 → 1.11.0

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 (30) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/TOKENS.md +72 -0
  3. package/dist/index.cjs +143 -42
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +54 -41
  6. package/dist/index.d.cts +17 -7
  7. package/dist/index.d.ts +17 -7
  8. package/dist/index.js +143 -42
  9. package/dist/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +7 -7
  12. package/src/charts/heatmap-chart/heatmap-chart.module.scss +18 -13
  13. package/src/charts/heatmap-chart/heatmap-chart.tsx +11 -7
  14. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +3 -3
  15. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +13 -7
  16. package/src/charts/leaderboard-chart/hooks/index.ts +1 -0
  17. package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +115 -0
  18. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +39 -12
  19. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +67 -14
  20. package/src/charts/leaderboard-chart/test/leaderboard-chart-fit-rows.test.tsx +365 -0
  21. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +73 -2
  22. package/src/charts/leaderboard-chart/types.ts +12 -0
  23. package/src/charts/line-chart/line-chart.module.scss +2 -2
  24. package/src/charts/private/grid-control/grid-control.module.scss +1 -1
  25. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
  26. package/src/charts/private/x-zoom.module.scss +12 -10
  27. package/src/components/tooltip/base-tooltip.module.scss +3 -2
  28. package/src/components/trend-indicator/trend-indicator.module.scss +6 -5
  29. package/src/providers/chart-context/themes.ts +33 -19
  30. package/src/types.ts +2 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.11.0] - 2026-07-27
9
+ ### Added
10
+ - Charts: add a `fitRows` prop to LeaderboardChart so a chart in a fixed-height container shows only the rows that fit instead of scrolling. [#50688]
11
+
12
+ ### Changed
13
+ - Charts: standardise package CSS custom properties on the --a8c-charts-{category}-{name} naming convention and document the token catalog; existing public override variables keep working via deprecated aliases. [#50656]
14
+
15
+ ### Fixed
16
+ - Leaderboard: Distinguish unavailable percentage changes from missing comparison data. [#50690]
17
+
8
18
  ## [1.10.3] - 2026-07-22
9
19
  ### Fixed
10
20
  - LeaderboardChart: Keep row spacing and column alignment identical whether or not a row is interactive. [#50657]
@@ -935,6 +945,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
935
945
  - Fixed lints following ESLint rule changes for TS [#40584]
936
946
  - Fixing a bug in Chart storybook data. [#40640]
937
947
 
948
+ [1.11.0]: https://github.com/Automattic/charts/compare/v1.10.3...v1.11.0
938
949
  [1.10.3]: https://github.com/Automattic/charts/compare/v1.10.2...v1.10.3
939
950
  [1.10.2]: https://github.com/Automattic/charts/compare/v1.10.1...v1.10.2
940
951
  [1.10.1]: https://github.com/Automattic/charts/compare/v1.10.0...v1.10.1
package/TOKENS.md ADDED
@@ -0,0 +1,72 @@
1
+ # Charts CSS custom-property catalog
2
+
3
+ `@automattic/charts` exposes its themeable values as CSS custom properties under a
4
+ single convention: `--a8c-charts-{category}-{name}`. `{category}` mirrors the WPDS
5
+ token type (`color`, `dimension`, `border-radius`).
6
+
7
+ ## How it resolves today
8
+
9
+ Nothing emits these variables at runtime yet (that is CHARTS-203). Each reference in
10
+ the library carries its default inline as a fallback:
11
+
12
+ ```scss
13
+ stroke: var(--a8c-charts-color-grid, var(--wpds-color-stroke-surface-neutral, #dbdbdb));
14
+ ```
15
+
16
+ So an `--a8c-charts-*` variable you set on a chart container wins; otherwise the
17
+ mapped `--wpds-*` token wins; otherwise the spec-value hex wins. Setting any
18
+ `--a8c-charts-*` variable on a chart ancestor overrides that role.
19
+
20
+ ## Tier 1 — semantic catalog (shared, themeable roles)
21
+
22
+ | Role | Maps to `--wpds-*` | Fallback |
23
+ |---|---|---|
24
+ | `--a8c-charts-color-grid` | `--wpds-color-stroke-surface-neutral` | `#dbdbdb` |
25
+ | `--a8c-charts-color-axis` | `--wpds-color-stroke-surface-neutral` | `#dbdbdb` |
26
+ | `--a8c-charts-color-tick` | `--wpds-color-stroke-surface-neutral` | `#dbdbdb` |
27
+ | `--a8c-charts-color-label` | `--wpds-color-foreground-content-neutral` | `#1e1e1e` |
28
+ | `--a8c-charts-color-label-secondary` | `--wpds-color-foreground-content-neutral-weak` | `#707070` |
29
+ | `--a8c-charts-color-label-inverse` | `--wpds-color-foreground-interactive-neutral-strong` | `#f0f0f0` |
30
+ | `--a8c-charts-color-label-on-fill` | _(none — white-on-series-fill, no WPDS fit)_ | `#FFFFFF` |
31
+ | `--a8c-charts-color-annotation` | `--wpds-color-foreground-content-neutral` | `#1e1e1e` |
32
+ | `--a8c-charts-color-trend-up` | `--wpds-color-foreground-content-success-weak` | `#008030` |
33
+ | `--a8c-charts-color-trend-down` | `--wpds-color-foreground-content-error-weak` | `#cc1818` |
34
+ | `--a8c-charts-color-trend-neutral` | `--wpds-color-foreground-content-neutral-weak` | `#707070` |
35
+ | `--a8c-charts-color-background` | `--wpds-color-background-surface-neutral-strong` | `#fff` |
36
+ | `--a8c-charts-color-surface-secondary` | `--wpds-color-background-surface-neutral-weak` | `#f4f4f4` |
37
+ | `--a8c-charts-color-track` | `--wpds-color-background-track-neutral-weak` | `#f0f0f0` |
38
+ | `--a8c-charts-color-tooltip-surface` | _(none — translucent dark surface, no WPDS fit)_ | `rgba(0,0,0,0.85)` |
39
+ | `--a8c-charts-color-focus` | `--wpds-color-stroke-focus` | `#3858e9` |
40
+
41
+ `--a8c-charts-color-axis` (axis line) and `--a8c-charts-color-tick` (tick marks)
42
+ resolve to the same value as `--a8c-charts-color-grid` by default. They are kept as
43
+ distinct roles so axis, tick marks, and gridlines can be themed independently; each
44
+ maps directly to its own `--wpds-*` token rather than chaining through `grid`.
45
+
46
+ ## Tier 2 — component-specific variables
47
+
48
+ Instance styling knobs, on the same convention but not shared semantic roles:
49
+ `--a8c-charts-border-radius-leaderboard-bar`,
50
+ `--a8c-charts-dimension-leaderboard-bar-hover-inset`,
51
+ `--a8c-charts-color-heatmap-*`, `--a8c-charts-dimension-heatmap-*`,
52
+ `--a8c-charts-heatmap-cell-intensity`.
53
+
54
+ `--a8c-charts-heatmap-cell-intensity` is the one variable without a `{category}`
55
+ segment: it holds a unitless 0–1 scalar consumed inside `color-mix()`, not a colour.
56
+
57
+ The heatmap Tier-2 variables (`--a8c-charts-color-heatmap-*`,
58
+ `--a8c-charts-dimension-heatmap-*`, `--a8c-charts-heatmap-cell-intensity`) are
59
+ **component-emitted** — the chart sets them from JS per render — not consumer
60
+ override points.
61
+
62
+ ## Public override variables
63
+
64
+ These are documented, supported override points. Deprecated names still work — they
65
+ are read as an inner fallback behind the new name:
66
+
67
+ | New | Deprecated alias |
68
+ |---|---|
69
+ | `--a8c-charts-color-trend-up` | `--charts-trend-up-color` |
70
+ | `--a8c-charts-color-trend-down` | `--charts-trend-down-color` |
71
+ | `--a8c-charts-color-trend-neutral` | `--charts-trend-neutral-color` |
72
+ | `--a8c-charts-border-radius-leaderboard-bar` | `--a8c--charts--leaderboard--bar--border-radius` |
package/dist/index.cjs CHANGED
@@ -725,9 +725,9 @@ const getChartColor = (index, colorCache) => {
725
725
  * Default theme configuration
726
726
  */
727
727
  const defaultTheme = {
728
- backgroundColor: "var(--wpds-color-background-surface-neutral-strong, #fff)",
728
+ backgroundColor: "var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff))",
729
729
  labelBackgroundColor: "transparent",
730
- labelTextColor: "#FFFFFF",
730
+ labelTextColor: "var(--a8c-charts-color-label-on-fill, #FFFFFF)",
731
731
  colors: [
732
732
  "#98C8DF",
733
733
  "#006DAB",
@@ -736,59 +736,59 @@ const defaultTheme = {
736
736
  "#FF8C8F"
737
737
  ],
738
738
  gridStyles: {
739
- stroke: "var(--wpds-color-stroke-surface-neutral, #dbdbdb)",
739
+ stroke: "var(--a8c-charts-color-grid, var(--wpds-color-stroke-surface-neutral, #dbdbdb))",
740
740
  strokeWidth: 1
741
741
  },
742
742
  tickLength: 4,
743
743
  gridColor: "",
744
744
  gridColorDark: "",
745
745
  xTickLineStyles: {
746
- stroke: "var(--wpds-color-stroke-surface-neutral, #dbdbdb)",
746
+ stroke: "var(--a8c-charts-color-tick, var(--wpds-color-stroke-surface-neutral, #dbdbdb))",
747
747
  strokeWidth: 1
748
748
  },
749
749
  xAxisLineStyles: {
750
- stroke: "var(--wpds-color-stroke-surface-neutral, #dbdbdb)",
750
+ stroke: "var(--a8c-charts-color-axis, var(--wpds-color-stroke-surface-neutral, #dbdbdb))",
751
751
  strokeWidth: 1
752
752
  },
753
753
  legend: {
754
- labelStyles: { color: "var(--wpds-color-foreground-content-neutral, #1e1e1e)" },
754
+ labelStyles: { color: "var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e))" },
755
755
  containerStyles: {},
756
756
  shapeStyles: []
757
757
  },
758
758
  seriesLineStyles: [],
759
759
  glyphs: [],
760
760
  svgLabelSmall: {
761
- fill: "var(--wpds-color-foreground-content-neutral, #1e1e1e)",
761
+ fill: "var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e))",
762
762
  fontFamily: "inherit"
763
763
  },
764
764
  svgLabelBig: { fontFamily: "inherit" },
765
765
  annotationStyles: {
766
766
  label: {
767
- anchorLineStroke: "var(--wpds-color-foreground-content-neutral, #1e1e1e)",
768
- backgroundFill: "var(--wpds-color-background-surface-neutral-strong, #fff)"
767
+ anchorLineStroke: "var(--a8c-charts-color-annotation, var(--wpds-color-foreground-content-neutral, #1e1e1e))",
768
+ backgroundFill: "var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff))"
769
769
  },
770
- connector: { stroke: "var(--wpds-color-foreground-content-neutral, #1e1e1e)" },
770
+ connector: { stroke: "var(--a8c-charts-color-annotation, var(--wpds-color-foreground-content-neutral, #1e1e1e))" },
771
771
  circleSubject: {
772
772
  stroke: "transparent",
773
- fill: "var(--wpds-color-foreground-content-neutral, #1e1e1e)",
773
+ fill: "var(--a8c-charts-color-annotation, var(--wpds-color-foreground-content-neutral, #1e1e1e))",
774
774
  radius: 5
775
775
  }
776
776
  },
777
- geoChart: { featureFillColor: "var(--wpds-color-background-surface-neutral-weak, #f4f4f4)" },
777
+ geoChart: { featureFillColor: "var(--a8c-charts-color-surface-secondary, var(--wpds-color-background-surface-neutral-weak, #f4f4f4))" },
778
778
  leaderboardChart: {
779
779
  rowGap: 12,
780
780
  columnGap: 4,
781
781
  labelSpacing: "xs",
782
782
  deltaColors: [
783
- "var(--wpds-color-foreground-content-error-weak, #cc1818)",
784
- "var(--wpds-color-foreground-content-neutral-weak, #707070)",
785
- "var(--wpds-color-foreground-content-success-weak, #008030)"
783
+ "var(--a8c-charts-color-trend-down, var(--wpds-color-foreground-content-error-weak, #cc1818))",
784
+ "var(--a8c-charts-color-trend-neutral, var(--wpds-color-foreground-content-neutral-weak, #707070))",
785
+ "var(--a8c-charts-color-trend-up, var(--wpds-color-foreground-content-success-weak, #008030))"
786
786
  ]
787
787
  },
788
788
  conversionFunnelChart: {
789
- backgroundColor: "var(--wpds-color-background-surface-neutral-weak, #f4f4f4)",
790
- positiveChangeColor: "var(--wpds-color-foreground-content-success-weak, #008030)",
791
- negativeChangeColor: "var(--wpds-color-foreground-content-error-weak, #cc1818)"
789
+ backgroundColor: "var(--a8c-charts-color-surface-secondary, var(--wpds-color-background-surface-neutral-weak, #f4f4f4))",
790
+ positiveChangeColor: "var(--a8c-charts-color-trend-up, var(--wpds-color-foreground-content-success-weak, #008030))",
791
+ negativeChangeColor: "var(--a8c-charts-color-trend-down, var(--wpds-color-foreground-content-error-weak, #cc1818))"
792
792
  },
793
793
  lineChart: { lineStyles: { comparison: {
794
794
  strokeDasharray: "4 4",
@@ -6427,9 +6427,9 @@ const HeatmapLegend = ({ steps = 5, lessLabel, moreLabel }) => {
6427
6427
  "aria-hidden": "true",
6428
6428
  className: heatmap_chart_module_default["heatmap-chart__legend-swatch"],
6429
6429
  style: {
6430
- "--heatmap-primary": primaryColorHex,
6431
- "--heatmap-bg": backgroundColor,
6432
- "--intensity": intensity
6430
+ "--a8c-charts-color-heatmap-primary": primaryColorHex,
6431
+ "--a8c-charts-color-heatmap-background": backgroundColor,
6432
+ "--a8c-charts-heatmap-cell-intensity": intensity
6433
6433
  }
6434
6434
  }, index);
6435
6435
  })
@@ -6587,17 +6587,17 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6587
6587
  children: (0, _wordpress_i18n.__)("No data available", "jetpack-charts")
6588
6588
  })
6589
6589
  });
6590
- const columnTrack = compact ? "var(--heatmap-cell-size)" : `minmax(${minCellWidth ?? 0}px, ${maxCellWidth ? `${maxCellWidth}px` : "1fr"})`;
6591
- const rowTrack = compact ? "var(--heatmap-cell-size)" : `minmax(${minCellHeight ?? 0}px, ${maxCellHeight ? `${maxCellHeight}px` : "1fr"})`;
6590
+ const columnTrack = compact ? "var(--a8c-charts-dimension-heatmap-cell-size)" : `minmax(${minCellWidth ?? 0}px, ${maxCellWidth ? `${maxCellWidth}px` : "1fr"})`;
6591
+ const rowTrack = compact ? "var(--a8c-charts-dimension-heatmap-cell-size)" : `minmax(${minCellHeight ?? 0}px, ${maxCellHeight ? `${maxCellHeight}px` : "1fr"})`;
6592
6592
  const gridStyle = {
6593
- "--heatmap-primary": primaryColorHex,
6594
- "--heatmap-bg": theme.backgroundColor,
6593
+ "--a8c-charts-color-heatmap-primary": primaryColorHex,
6594
+ "--a8c-charts-color-heatmap-background": theme.backgroundColor,
6595
6595
  gridTemplateColumns: `auto repeat(${columns}, ${columnTrack})`,
6596
6596
  gridTemplateRows: `auto repeat(${rows}, ${rowTrack})`
6597
6597
  };
6598
6598
  if (compact) {
6599
- gridStyle["--heatmap-cell-gap"] = `${compactCellGap}px`;
6600
- gridStyle["--heatmap-cell-size"] = `${compactCellSize}px`;
6599
+ gridStyle["--a8c-charts-dimension-heatmap-cell-gap"] = `${compactCellGap}px`;
6600
+ gridStyle["--a8c-charts-dimension-heatmap-cell-size"] = `${compactCellSize}px`;
6601
6601
  }
6602
6602
  const activeDescendant = selectedIndex !== void 0 ? `${chartId}-cell-${Math.floor(selectedIndex / rows)}-${selectedIndex % rows}` : void 0;
6603
6603
  const heightCapped = !compact && Boolean(maxCellHeight);
@@ -6674,7 +6674,7 @@ const HeatmapChartInternal = ({ data, chartId: providedChartId, width = 0, heigh
6674
6674
  [heatmap_chart_module_default["heatmap-chart__cell--strong"]]: present && cellHasLightText(normalized),
6675
6675
  [heatmap_chart_module_default["heatmap-chart__cell--selected"]]: selectedIndex === flatIndex
6676
6676
  }),
6677
- style: present ? { "--intensity": normalized } : void 0,
6677
+ style: present ? { "--a8c-charts-heatmap-cell-intensity": normalized } : void 0,
6678
6678
  onMouseMove: handleCellMouseMove,
6679
6679
  onMouseLeave: handleCellMouseLeave,
6680
6680
  children: drawValues && present && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -9007,6 +9007,90 @@ function UnconnectedGrid(props, forwardedRef) {
9007
9007
  }
9008
9008
  var component_default = contextConnect(UnconnectedGrid, "Grid");
9009
9009
  //#endregion
9010
+ //#region src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts
9011
+ /**
9012
+ * Fractional layout rounding can put a row's bottom a hair past the container's
9013
+ * without any visible clipping. Allow that much slack before hiding a row.
9014
+ *
9015
+ * Exported so tests and stories assert against the real tolerance rather than
9016
+ * restating it.
9017
+ */
9018
+ const SUBPIXEL_TOLERANCE = .5;
9019
+ /**
9020
+ * Counts how many leading rows fit inside the content container.
9021
+ *
9022
+ * Rows are read from the DOM rather than derived from constants: row height
9023
+ * depends on the theme's row gap, label wrapping, and whatever a caller renders
9024
+ * as a label, so any restated number would drift.
9025
+ *
9026
+ * Every row is a direct grid child marked with `data-row-index`; interactive rows
9027
+ * use a button wrapper and non-interactive rows use a div wrapper.
9028
+ *
9029
+ * @param enabled - Whether to measure at all. When false every row fits.
9030
+ * @param rowCount - Total number of rows rendered.
9031
+ * @param data - Rendered row data, whose geometry may change without changing the count.
9032
+ * @return Ref for the content container and the number of leading rows that fit.
9033
+ */
9034
+ function useFittedRowCount(enabled, rowCount, data) {
9035
+ const contentRef = (0, react$1.useRef)(null);
9036
+ const [fittedCount, setFittedCount] = (0, react$1.useState)(rowCount);
9037
+ const [isMeasurable, setIsMeasurable] = (0, react$1.useState)(true);
9038
+ const measure = (0, react$1.useCallback)(() => {
9039
+ const content = contentRef.current;
9040
+ if (!content) return;
9041
+ content.scrollTop = 0;
9042
+ const rows = content.querySelectorAll(":scope > [data-leaderboard-grid] > [data-row-index]");
9043
+ const rowBottoms = [];
9044
+ rows.forEach((row) => {
9045
+ const index = Number(row.getAttribute("data-row-index"));
9046
+ if (!Number.isInteger(index) || index < 0 || index >= rowCount) return;
9047
+ const { bottom } = row.getBoundingClientRect();
9048
+ rowBottoms[index] = Math.max(rowBottoms[index] ?? -Infinity, bottom);
9049
+ });
9050
+ if (rowBottoms.length === 0) {
9051
+ setIsMeasurable(false);
9052
+ setFittedCount(rowCount);
9053
+ return;
9054
+ }
9055
+ setIsMeasurable(true);
9056
+ const contentBottom = content.getBoundingClientRect().bottom + SUBPIXEL_TOLERANCE;
9057
+ let fits = 0;
9058
+ while (fits < rowBottoms.length && rowBottoms[fits] <= contentBottom) fits++;
9059
+ setFittedCount((current) => current === fits ? current : fits);
9060
+ }, [rowCount]);
9061
+ (0, react$1.useLayoutEffect)(() => {
9062
+ if (!enabled) {
9063
+ setFittedCount(rowCount);
9064
+ return;
9065
+ }
9066
+ measure();
9067
+ }, [
9068
+ enabled,
9069
+ rowCount,
9070
+ data,
9071
+ measure
9072
+ ]);
9073
+ (0, react$1.useLayoutEffect)(() => {
9074
+ if (!enabled) return;
9075
+ const content = contentRef.current;
9076
+ if (!content) return;
9077
+ const observer = new ResizeObserver(measure);
9078
+ observer.observe(content);
9079
+ const grid = content.querySelector(":scope > [data-leaderboard-grid]");
9080
+ if (grid) observer.observe(grid);
9081
+ return () => observer.disconnect();
9082
+ }, [
9083
+ enabled,
9084
+ rowCount,
9085
+ measure
9086
+ ]);
9087
+ return {
9088
+ contentRef,
9089
+ fittedCount: enabled ? fittedCount : rowCount,
9090
+ isMeasurable
9091
+ };
9092
+ }
9093
+ //#endregion
9010
9094
  //#region src/charts/leaderboard-chart/hooks/use-leaderboard-legend-items.ts
9011
9095
  /**
9012
9096
  * Hook to create legend items from leaderboard data
@@ -9067,11 +9151,13 @@ var leaderboard_chart_module_default = {
9067
9151
  "deltaPlaceholder": "a8ccharts-GovfoW-deltaPlaceholder",
9068
9152
  "deltaValue": "a8ccharts-GovfoW-deltaValue",
9069
9153
  "emptyState": "a8ccharts-GovfoW-emptyState",
9154
+ "fitEmptyState": "a8ccharts-GovfoW-fitEmptyState",
9070
9155
  "interactiveRow": "a8ccharts-GovfoW-interactiveRow",
9071
9156
  "is-overlay": "a8ccharts-GovfoW-is-overlay",
9072
9157
  "label": "a8ccharts-GovfoW-label",
9073
9158
  "leaderboardChart": "a8ccharts-GovfoW-leaderboardChart",
9074
9159
  "leaderboardChart__content": "a8ccharts-GovfoW-leaderboardChart__content",
9160
+ "leaderboardChart__content--fit": "a8ccharts-GovfoW-leaderboardChart__content--fit",
9075
9161
  "leaderboardChart--loading": "a8ccharts-GovfoW-leaderboardChart--loading",
9076
9162
  "leaderboardChart--responsive": "a8ccharts-GovfoW-leaderboardChart--responsive",
9077
9163
  "overlap": "a8ccharts-GovfoW-overlap",
@@ -9116,8 +9202,9 @@ const defaultDeltaFormatter = (value) => {
9116
9202
  * @param share - The bar's share of the row width, as a percentage.
9117
9203
  * @return A CSS width value.
9118
9204
  */
9119
- const getBarWidth = (share) => `calc(${share}% - var(--a8c--charts--leaderboard--bar--hover-inset, 0px) * ${share} / 100)`;
9120
- const hasComparisonValue = (entry) => entry.previousValue != null && entry.previousShare != null && entry.delta != null;
9205
+ const getBarWidth = (share) => `calc(${share}% - var(--a8c-charts-dimension-leaderboard-bar-hover-inset, 0px) * ${share} / 100)`;
9206
+ const hasPreviousValue = (entry) => entry.previousValue != null && entry.previousShare != null;
9207
+ const hasDeltaValue = (entry) => hasPreviousValue(entry) && entry.delta != null;
9121
9208
  const BarLabel = ({ label }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: typeof label === "string" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Text$2, {
9122
9209
  className: leaderboard_chart_module_default.label,
9123
9210
  children: label
@@ -9135,7 +9222,7 @@ const BarWithLabel = ({ entry, withComparison, withOverlayLabel, primaryColor, s
9135
9222
  backgroundColor: primaryColor
9136
9223
  }
9137
9224
  }),
9138
- showComparisonBar && hasComparisonValue(entry) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9225
+ showComparisonBar && hasPreviousValue(entry) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9139
9226
  className: (0, clsx.default)(leaderboard_chart_module_default.bar, { [leaderboard_chart_module_default["bar--animated"]]: animation }),
9140
9227
  style: {
9141
9228
  width: getBarWidth(entry.previousShare),
@@ -9161,6 +9248,7 @@ const BarWithLabel = ({ entry, withComparison, withOverlayLabel, primaryColor, s
9161
9248
  * @param props.valueFormatter - Custom formatter for values
9162
9249
  * @param props.deltaFormatter - Custom formatter for delta values
9163
9250
  * @param props.loading - Whether the chart is in loading state
9251
+ * @param props.fitRows - Whether to show only the rows that fit the chart's height
9164
9252
  * @param props.animation - Whether the chart should animate on load
9165
9253
  * @param props.showLegend - Whether to show legend
9166
9254
  * @param props.legend - Legend configuration (orientation, position, alignment, shape, shapeStyles, interactive)
@@ -9171,7 +9259,7 @@ const BarWithLabel = ({ entry, withComparison, withOverlayLabel, primaryColor, s
9171
9259
  * @param props.style - Custom styling for the chart container
9172
9260
  * @return JSX element representing the leaderboard chart
9173
9261
  */
9174
- const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propWidth, height: propHeight, withComparison = false, withOverlayLabel = false, primaryColor, secondaryColor, valueFormatter = defaultValueFormatter, deltaFormatter = defaultDeltaFormatter, animation, loading = false, showLegend = false, legend = {}, legendLabels, gap = "md", className, style, children }) => {
9262
+ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propWidth, height: propHeight, withComparison = false, withOverlayLabel = false, primaryColor, secondaryColor, valueFormatter = defaultValueFormatter, deltaFormatter = defaultDeltaFormatter, animation, loading = false, fitRows = false, showLegend = false, legend = {}, legendLabels, gap = "md", className, style, children }) => {
9175
9263
  const legendInteractive = legend.interactive ?? false;
9176
9264
  const legendPosition = legend.position ?? "bottom";
9177
9265
  const chartId = useChartId(providedChartId);
@@ -9240,6 +9328,8 @@ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propW
9240
9328
  }), [withComparison, withOverlayLabel])
9241
9329
  });
9242
9330
  const prefersReducedMotion = usePrefersReducedMotion();
9331
+ const { contentRef, fittedCount, isMeasurable } = useFittedRowCount(fitRows && !allSeriesHidden, data?.length ?? 0, data);
9332
+ const shouldFitRows = fitRows && isMeasurable;
9243
9333
  if (!data || data.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SingleChartContext.Provider, {
9244
9334
  value: { chartId },
9245
9335
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartLayout, {
@@ -9292,20 +9382,27 @@ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propW
9292
9382
  height: propHeight || void 0
9293
9383
  },
9294
9384
  trailingContent: nonLegendChildren,
9295
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9296
- className: leaderboard_chart_module_default.leaderboardChart__content,
9297
- children: allSeriesHidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9385
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9386
+ ref: contentRef,
9387
+ className: (0, clsx.default)(leaderboard_chart_module_default.leaderboardChart__content, { [leaderboard_chart_module_default["leaderboardChart__content--fit"]]: shouldFitRows }),
9388
+ children: [shouldFitRows && fittedCount === 0 && !allSeriesHidden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9389
+ className: (0, clsx.default)(leaderboard_chart_module_default.emptyState, leaderboard_chart_module_default.fitEmptyState),
9390
+ children: (0, _wordpress_i18n.__)("Not enough space to display data", "jetpack-charts")
9391
+ }), allSeriesHidden ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9298
9392
  className: leaderboard_chart_module_default.emptyState,
9299
9393
  children: (0, _wordpress_i18n.__)("All series are hidden. Click legend items to show data.", "jetpack-charts")
9300
9394
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(component_default, {
9301
9395
  templateColumns: "minmax(0, 1fr) auto",
9302
9396
  rowGap,
9303
9397
  columnGap,
9304
- children: data.map((entry) => {
9398
+ "data-leaderboard-grid": true,
9399
+ children: data.map((entry, rowIndex) => {
9400
+ const rowStyle = shouldFitRows && rowIndex >= fittedCount ? { visibility: "hidden" } : void 0;
9305
9401
  const showComparisonColumn = withComparison && isComparisonVisible;
9306
- const hasDeltaValue = hasComparisonValue(entry);
9307
- const showComparisonValue = showComparisonColumn && hasDeltaValue;
9308
- const showComparisonPlaceholder = showComparisonColumn && !hasDeltaValue;
9402
+ const hasPreviousPeriodValue = hasPreviousValue(entry);
9403
+ const hasDelta = hasDeltaValue(entry);
9404
+ const showComparisonValue = showComparisonColumn && hasDelta;
9405
+ const showComparisonPlaceholder = showComparisonColumn && !hasDelta;
9309
9406
  const colorIndex = showComparisonValue ? Math.sign(entry.delta) + 1 : 1;
9310
9407
  const deltaColor = deltaColors[colorIndex];
9311
9408
  const rowCells = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
@@ -9337,16 +9434,18 @@ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propW
9337
9434
  style: { color: deltaColor },
9338
9435
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9339
9436
  "aria-hidden": "true",
9340
- children: "-"
9437
+ children: "—"
9341
9438
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(component_default$1, {
9342
9439
  as: "span",
9343
- children: (0, _wordpress_i18n.__)("No comparison data", "jetpack-charts")
9440
+ children: hasPreviousPeriodValue ? (0, _wordpress_i18n.__)("Percentage change unavailable", "jetpack-charts") : (0, _wordpress_i18n.__)("No comparison data", "jetpack-charts")
9344
9441
  })]
9345
9442
  })
9346
9443
  ]
9347
9444
  })] });
9348
9445
  if (entry.onClick) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
9349
9446
  type: "button",
9447
+ "data-row-index": rowIndex,
9448
+ style: rowStyle,
9350
9449
  className: (0, clsx.default)(leaderboard_chart_module_default.row, leaderboard_chart_module_default.interactiveRow),
9351
9450
  onClick: entry.onClick,
9352
9451
  "aria-label": entry.ariaLabel,
@@ -9357,11 +9456,13 @@ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propW
9357
9456
  })]
9358
9457
  }, entry.id);
9359
9458
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9459
+ "data-row-index": rowIndex,
9460
+ style: rowStyle,
9360
9461
  className: leaderboard_chart_module_default.row,
9361
9462
  children: rowCells
9362
9463
  }, entry.id);
9363
9464
  })
9364
- })
9465
+ })]
9365
9466
  })
9366
9467
  })
9367
9468
  });