@automattic/charts 1.6.0 → 1.8.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 (54) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -2
  3. package/dist/index.cjs +380 -60
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +12 -7
  6. package/dist/index.d.cts +119 -30
  7. package/dist/index.d.ts +122 -33
  8. package/dist/index.js +380 -60
  9. package/dist/index.js.map +1 -1
  10. package/package.json +22 -21
  11. package/src/charts/area-chart/types.ts +1 -1
  12. package/src/charts/bar-chart/bar-chart.tsx +197 -43
  13. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +70 -0
  14. package/src/charts/bar-chart/private/comparison-bars.tsx +154 -0
  15. package/src/charts/bar-chart/private/comparison-constants.ts +33 -0
  16. package/src/charts/bar-chart/private/index.ts +9 -0
  17. package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +47 -0
  18. package/src/charts/bar-chart/private/test/comparison-bars.test.tsx +183 -0
  19. package/src/charts/bar-chart/private/use-bar-chart-options.ts +46 -5
  20. package/src/charts/bar-chart/test/bar-chart.test.tsx +191 -1
  21. package/src/charts/geo-chart/geo-chart.tsx +8 -10
  22. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +27 -14
  23. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +7 -4
  24. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +51 -4
  25. package/src/charts/line-chart/line-chart.tsx +1 -1
  26. package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +18 -2
  27. package/src/charts/line-chart/private/line-chart-annotation.tsx +1 -1
  28. package/src/charts/line-chart/types.ts +1 -1
  29. package/src/charts/pie-chart/pie-chart.module.scss +0 -4
  30. package/src/charts/pie-chart/pie-chart.tsx +3 -8
  31. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +0 -5
  32. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +5 -10
  33. package/src/charts/private/center/center.module.scss +4 -0
  34. package/src/charts/private/center/center.tsx +33 -0
  35. package/src/charts/private/center/index.ts +2 -0
  36. package/src/charts/private/center/test/center.test.tsx +60 -0
  37. package/src/charts/private/chart-layout/chart-layout.tsx +1 -2
  38. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +0 -2
  39. package/src/charts/private/svg-empty-state/svg-empty-state.tsx +2 -4
  40. package/src/charts/private/x-zoom.tsx +2 -2
  41. package/src/components/legend/hooks/use-chart-legend-items.ts +6 -2
  42. package/src/components/legend/legend.tsx +1 -2
  43. package/src/components/legend/utils/label-transform-factory.ts +1 -1
  44. package/src/components/tooltip/accessible-tooltip.tsx +2 -6
  45. package/src/index.ts +6 -5
  46. package/src/providers/chart-context/global-charts-provider.tsx +2 -0
  47. package/src/providers/chart-context/test/chart-context.test.tsx +13 -1
  48. package/src/providers/chart-context/themes.ts +9 -1
  49. package/src/providers/chart-context/types.ts +9 -2
  50. package/src/types.ts +120 -11
  51. package/src/utils/get-styles.ts +36 -14
  52. package/src/utils/index.ts +6 -1
  53. package/src/utils/test/get-styles.test.ts +47 -1
  54. package/src/visx/types.ts +30 -1
package/dist/index.cjs CHANGED
@@ -43,7 +43,6 @@ let _visx_gradient = require("@visx/gradient");
43
43
  let _visx_curve = require("@visx/curve");
44
44
  let _visx_responsive = require("@visx/responsive");
45
45
  let _visx_annotation = require("@visx/annotation");
46
- let _wordpress_icons = require("@wordpress/icons");
47
46
  let _visx_pattern = require("@visx/pattern");
48
47
  let _visx_tooltip = require("@visx/tooltip");
49
48
  let react_google_charts = require("react-google-charts");
@@ -52,6 +51,7 @@ dompurify = __toESM(dompurify, 1);
52
51
  let _babel_runtime_helpers_esm_extends = require("@babel/runtime/helpers/esm/extends");
53
52
  _babel_runtime_helpers_esm_extends = __toESM(_babel_runtime_helpers_esm_extends);
54
53
  require("@babel/runtime/helpers/extends");
54
+ let _wordpress_icons = require("@wordpress/icons");
55
55
  let _visx_shape = require("@visx/shape");
56
56
  //#region src/charts/private/single-chart-context/single-chart-context.tsx
57
57
  const ChartInstanceContext = (0, react$1.createContext)(null);
@@ -266,6 +266,20 @@ function getSeriesLineStyles(seriesData, index, providerTheme) {
266
266
  return seriesData.options?.seriesLineStyle ?? themeSemanticLineStyle ?? themeSeriesLineStyle ?? {};
267
267
  }
268
268
  /**
269
+ * Utility to get consolidated bar styles for a series by semantic type.
270
+ * Mirrors getSeriesLineStyles: a series with `options.type` (e.g. 'comparison')
271
+ * resolves to `theme.barChart.barStyles[ type ]`.
272
+ *
273
+ * @param {SeriesData} seriesData - The series data containing styling options
274
+ * @param {number} index - The index of the series in the data array
275
+ * @param {ChartTheme} providerTheme - The chart theme configuration
276
+ * @return {BarStyles} The consolidated bar styles for the series
277
+ */
278
+ function getSeriesBarStyles(seriesData, index, providerTheme) {
279
+ const type = seriesData.options?.type;
280
+ return (type && providerTheme?.barChart?.barStyles?.[type]) ?? {};
281
+ }
282
+ /**
269
283
  * Utility function to get shape styles for a legend item
270
284
  *
271
285
  * @param {SeriesData} series - The series data containing styling options
@@ -277,13 +291,17 @@ function getSeriesLineStyles(seriesData, index, providerTheme) {
277
291
  function getItemShapeStyles(series, index, theme, legendShape) {
278
292
  const seriesShapeStyles = series.options?.legendShapeStyle ?? {};
279
293
  const lineStyles = legendShape === "line" ? getSeriesLineStyles(series, index, theme) : {};
294
+ const barOpacity = legendShape !== "line" ? getSeriesBarStyles(series, index, theme).opacity : void 0;
295
+ const barShapeStyles = barOpacity !== void 0 ? { opacity: barOpacity } : {};
280
296
  const themeShapeStyles = theme.legend?.shapeStyles?.[index];
281
- const itemShapeStyles = {
297
+ const explicitStyles = {
282
298
  ...seriesShapeStyles,
283
299
  ...lineStyles
284
300
  };
285
- if (Object.values(itemShapeStyles).some((value) => value !== void 0 && value !== null && value !== "")) return itemShapeStyles;
286
- return themeShapeStyles ?? {};
301
+ return {
302
+ ...Object.values(explicitStyles).some((value) => value !== void 0 && value !== null && value !== "") ? explicitStyles : themeShapeStyles ?? {},
303
+ ...barShapeStyles
304
+ };
287
305
  }
288
306
  //#endregion
289
307
  //#region src/utils/is-safari.ts
@@ -713,7 +731,7 @@ const defaultTheme = {
713
731
  leaderboardChart: {
714
732
  rowGap: 12,
715
733
  columnGap: 4,
716
- labelSpacing: 1.5,
734
+ labelSpacing: "xs",
717
735
  deltaColors: [
718
736
  "#FF8C8F",
719
737
  "#757575",
@@ -729,6 +747,10 @@ const defaultTheme = {
729
747
  strokeDasharray: "4 4",
730
748
  strokeLinecap: "square"
731
749
  } } },
750
+ barChart: { barStyles: { comparison: {
751
+ widthFactor: 1.5,
752
+ opacity: .5
753
+ } } },
732
754
  sparkline: {
733
755
  margin: {
734
756
  top: 2,
@@ -835,6 +857,7 @@ const GlobalChartsProvider = ({ children, theme }) => {
835
857
  overrideColor: overrideColor || isSeriesData && data?.options?.stroke || isPointPercentageData && data?.color
836
858
  }),
837
859
  lineStyles: isSeriesData ? getSeriesLineStyles(data, index, providerTheme) : {},
860
+ barStyles: isSeriesData ? getSeriesBarStyles(data, index, providerTheme) : {},
838
861
  glyph: providerTheme.glyphs?.[index],
839
862
  shapeStyles: isSeriesData ? getItemShapeStyles(data, index, providerTheme, legendShape) : {}
840
863
  };
@@ -2639,6 +2662,32 @@ const DefaultGlyph = (props) => {
2639
2662
  });
2640
2663
  };
2641
2664
  //#endregion
2665
+ //#region src/charts/private/center/center.module.scss
2666
+ var center_module_default = { "center": "a8ccharts-w3qxlG-center" };
2667
+ //#endregion
2668
+ //#region src/charts/private/center/center.tsx
2669
+ /**
2670
+ * Centers its children on both axes and fills its parent.
2671
+ *
2672
+ * A thin wrapper around `Stack` with `align="center"` and `justify="center"`
2673
+ * defaults (both overridable) plus `width: 100%; height: 100%`. Reads more
2674
+ * honestly than a `Stack` with both axes centered, and lets call sites drop
2675
+ * ad-hoc `*__centering` classes. Forwards its ref and spreads remaining props
2676
+ * onto the underlying `Stack`.
2677
+ *
2678
+ * @param props - Stack props; `align`/`justify` default to `"center"`.
2679
+ * @param ref - Forwarded to the underlying element.
2680
+ * @return The centered layout element.
2681
+ */
2682
+ const Center = (0, react$1.forwardRef)(({ align = "center", justify = "center", className, ...props }, ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
2683
+ ref,
2684
+ align,
2685
+ justify,
2686
+ className: (0, clsx.default)(center_module_default.center, className),
2687
+ ...props
2688
+ }));
2689
+ Center.displayName = "Center";
2690
+ //#endregion
2642
2691
  //#region src/charts/private/svg-empty-state/svg-empty-state.module.scss
2643
2692
  var svg_empty_state_module_default = { "svg-empty-state": "a8ccharts-udGPVq-svg-empty-state" };
2644
2693
  //#endregion
@@ -2663,9 +2712,7 @@ const SvgEmptyState = ({ x, y, width, height, children }) => {
2663
2712
  y: y - height / 2,
2664
2713
  width,
2665
2714
  height,
2666
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
2667
- align: "center",
2668
- justify: "center",
2715
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
2669
2716
  className: svg_empty_state_module_default["svg-empty-state"],
2670
2717
  children
2671
2718
  })
@@ -3063,6 +3110,19 @@ const LineChartAnnotationsOverlay = ({ children }) => {
3063
3110
  })
3064
3111
  });
3065
3112
  };
3113
+ const CloseIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
3114
+ width: "16",
3115
+ height: "16",
3116
+ viewBox: "0 0 24 24",
3117
+ fill: "none",
3118
+ stroke: "currentColor",
3119
+ strokeWidth: "2",
3120
+ strokeLinecap: "round",
3121
+ strokeLinejoin: "round",
3122
+ "aria-hidden": "true",
3123
+ focusable: "false",
3124
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 6l12 12M18 6L6 18" })
3125
+ });
3066
3126
  const LineChartAnnotationLabelWithPopover = ({ title, subtitle, renderLabel, renderLabelPopover }) => {
3067
3127
  const popoverId = (0, react$1.useId)();
3068
3128
  const buttonRef = (0, react$1.useRef)(null);
@@ -3124,10 +3184,7 @@ const LineChartAnnotationLabelWithPopover = ({ title, subtitle, renderLabel, ren
3124
3184
  popovertargetaction: "hide",
3125
3185
  className: line_chart_module_default["line-chart__annotation-label-popover-close-button"],
3126
3186
  "aria-label": (0, _wordpress_i18n.__)("Close", "jetpack-charts"),
3127
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_wordpress_icons.Icon, {
3128
- icon: _wordpress_icons.close,
3129
- size: 16
3130
- })
3187
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CloseIcon, {})
3131
3188
  })]
3132
3189
  })
3133
3190
  })]
@@ -4335,6 +4392,10 @@ const TruncatedXTickComponent = createTruncatedTickComponent("x");
4335
4392
  const TruncatedYTickComponent = createTruncatedTickComponent("y");
4336
4393
  //#endregion
4337
4394
  //#region src/charts/bar-chart/private/use-bar-chart-options.ts
4395
+ /** Outer padding of the category band scale (space at the chart edges). */
4396
+ const BASE_BAND_PADDING = .2;
4397
+ /** Inner padding of the category band scale (the base gap between ticks). */
4398
+ const BASE_BAND_PADDING_INNER = .1;
4338
4399
  const formatDateTick = (timestamp) => {
4339
4400
  return new Date(timestamp).toLocaleDateString(void 0, {
4340
4401
  month: "short",
@@ -4362,8 +4423,8 @@ function useBarChartOptions(data, horizontal, options = {}) {
4362
4423
  const defaultOptions = (0, react$1.useMemo)(() => {
4363
4424
  const bandScale = {
4364
4425
  type: "band",
4365
- padding: .2,
4366
- paddingInner: .1
4426
+ padding: BASE_BAND_PADDING,
4427
+ paddingInner: BASE_BAND_PADDING_INNER
4367
4428
  };
4368
4429
  const linearScale = {
4369
4430
  type: "linear",
@@ -4402,13 +4463,29 @@ function useBarChartOptions(data, horizontal, options = {}) {
4402
4463
  }, [data]);
4403
4464
  return (0, react$1.useMemo)(() => {
4404
4465
  const { xTickFormat, yTickFormat, tooltipLabelFormatter: defaultTooltipLabelFormatter, xAccessor, yAccessor, gridVisibility, xScale: baseXScale, yScale: baseYScale } = defaultOptions[horizontal ? "horizontal" : "vertical"];
4466
+ let valueScaleDomainOverride = {};
4467
+ if (data.some((s) => s.options?.type === "comparison")) {
4468
+ if (!(!horizontal ? options.yScale?.domain : options.xScale?.domain)) {
4469
+ const allValues = [];
4470
+ data.forEach((series) => {
4471
+ series.data.forEach((d) => {
4472
+ const enhanced = d;
4473
+ const v = enhanced.visualValue !== void 0 ? enhanced.visualValue : d.value;
4474
+ if (typeof v === "number" && Number.isFinite(v)) allValues.push(v);
4475
+ });
4476
+ });
4477
+ if (allValues.length > 0) valueScaleDomainOverride = { domain: [Math.min(...allValues), Math.max(...allValues)] };
4478
+ }
4479
+ }
4405
4480
  const xScale = {
4406
4481
  ...baseXScale,
4407
- ...options.xScale || {}
4482
+ ...options.xScale || {},
4483
+ ...horizontal ? valueScaleDomainOverride : {}
4408
4484
  };
4409
4485
  const yScale = {
4410
4486
  ...baseYScale,
4411
- ...options.yScale || {}
4487
+ ...options.yScale || {},
4488
+ ...!horizontal ? valueScaleDomainOverride : {}
4412
4489
  };
4413
4490
  const providedToolTipLabelFormatter = horizontal ? options.axis?.y?.tickFormat : options.axis?.x?.tickFormat;
4414
4491
  const { labelOverflow: xLabelOverflow, ...xAxisOptions } = options.axis?.x || {};
@@ -4443,10 +4520,143 @@ function useBarChartOptions(data, horizontal, options = {}) {
4443
4520
  }, [
4444
4521
  defaultOptions,
4445
4522
  options,
4446
- horizontal
4523
+ horizontal,
4524
+ data
4447
4525
  ]);
4448
4526
  }
4449
4527
  //#endregion
4528
+ //#region src/charts/bar-chart/private/comparison-bars-geometry.ts
4529
+ /**
4530
+ * Output position of a value scale's baseline: zero if in-domain, else the
4531
+ * nearest range edge. Mirrors visx's getScaleBaseline so comparison shadows
4532
+ * sit on the same baseline as primary bars.
4533
+ *
4534
+ * @param {ValueScale} scale - The continuous value scale.
4535
+ * @return {number} The baseline output position in pixels.
4536
+ */
4537
+ function getValueScaleBaseline(scale) {
4538
+ const [a, b] = scale.range().map((r) => Number(r) || 0);
4539
+ const isDescending = b < a;
4540
+ const maybeZero = scale(0);
4541
+ const [minOutput, maxOutput] = isDescending ? [b, a] : [a, b];
4542
+ if (isDescending) return Number.isFinite(maybeZero) ? Math.min(Math.max(minOutput, maybeZero), maxOutput) : maxOutput;
4543
+ return Number.isFinite(maybeZero) ? Math.min(Math.max(maybeZero, minOutput), maxOutput) : minOutput;
4544
+ }
4545
+ /**
4546
+ * Compute the rect for a comparison "shadow" bar, centered on the paired
4547
+ * primary bar slot and scaled by `widthFactor`.
4548
+ *
4549
+ * @param {object} params - Geometry inputs.
4550
+ * @param {boolean} params.horizontal - True for a horizontal bar chart, false for vertical.
4551
+ * @param {number} params.bandPosition - bandScale(category): start px of the category band.
4552
+ * @param {number} params.slotOffset - groupScale(primaryKey): offset of the primary slot within the band.
4553
+ * @param {number} params.slotThickness - groupScale.bandwidth(): primary bar thickness in px.
4554
+ * @param {number} params.valuePosition - valueScale(value): output px for the bar's data value.
4555
+ * @param {number} params.baseline - getValueScaleBaseline(valueScale): zero-line output px.
4556
+ * @param {number} params.widthFactor - Shadow thickness multiplier, e.g. 1.5 for 150% width.
4557
+ * @return {ComparisonRect} The {x, y, width, height} of the shadow rect.
4558
+ */
4559
+ function computeComparisonRect(params) {
4560
+ const { horizontal, bandPosition, slotOffset, slotThickness, valuePosition, baseline, widthFactor } = params;
4561
+ const slotStart = bandPosition + slotOffset;
4562
+ const shadowThickness = slotThickness * widthFactor;
4563
+ const shadowStart = slotStart + slotThickness / 2 - shadowThickness / 2;
4564
+ const valueStart = Math.min(valuePosition, baseline);
4565
+ const valueLength = Math.abs(baseline - valuePosition);
4566
+ if (horizontal) return {
4567
+ x: valueStart,
4568
+ y: shadowStart,
4569
+ width: valueLength,
4570
+ height: shadowThickness
4571
+ };
4572
+ return {
4573
+ x: shadowStart,
4574
+ y: valueStart,
4575
+ width: shadowThickness,
4576
+ height: valueLength
4577
+ };
4578
+ }
4579
+ /**
4580
+ * Fraction of each per-series step left as a gap between bars within a single tick.
4581
+ * Larger = more space between adjacent series; the shadow spans `1 - COMPARISON_INNER_GAP` of the step.
4582
+ */
4583
+ const COMPARISON_INNER_GAP = .1;
4584
+ /**
4585
+ * Upper clamp on the computed group padding, so bars can never collapse to zero width
4586
+ * even at very large `widthFactor` values.
4587
+ */
4588
+ const MAX_GROUP_PADDING = .9;
4589
+ /**
4590
+ * Factor applied to the category band's `paddingInner` in comparison mode to tighten the
4591
+ * gap between ticks. `0.75` = a 25% reduction of the tick-gap padding.
4592
+ */
4593
+ const COMPARISON_TICK_GAP_FACTOR = .75;
4594
+ //#endregion
4595
+ //#region src/charts/bar-chart/private/comparison-bars.tsx
4596
+ const ComparisonBars = ({ comparisonEntries, primaryKeys, groupPadding, horizontal, xAccessor, yAccessor, getElementStyles, resolveFill }) => {
4597
+ const context = (0, react$1.useContext)(_visx_xychart.DataContext);
4598
+ const xScale = context?.xScale;
4599
+ const yScale = context?.yScale;
4600
+ if (!xScale || !yScale || primaryKeys.length === 0) return null;
4601
+ const bandScale = horizontal ? yScale : xScale;
4602
+ const valueScale = horizontal ? xScale : yScale;
4603
+ const bandwidth = bandScale.bandwidth ? bandScale.bandwidth() : 0;
4604
+ if (!bandwidth) return null;
4605
+ const groupScale = (0, _visx_scale.scaleBand)({
4606
+ domain: primaryKeys,
4607
+ range: [0, bandwidth],
4608
+ padding: groupPadding
4609
+ });
4610
+ const slotThickness = groupScale.bandwidth();
4611
+ const baseline = getValueScaleBaseline(valueScale);
4612
+ const bandAccessor = horizontal ? yAccessor : xAccessor;
4613
+ const valueAccessor = horizontal ? xAccessor : yAccessor;
4614
+ const rects = [];
4615
+ comparisonEntries.forEach((entry) => {
4616
+ const { series, index, primaryKey } = entry;
4617
+ const slotOffset = groupScale(primaryKey);
4618
+ if (slotOffset == null || !Number.isFinite(slotOffset)) return;
4619
+ const { barStyles } = getElementStyles({
4620
+ data: series,
4621
+ index
4622
+ });
4623
+ const opacity = barStyles?.opacity ?? .5;
4624
+ const widthFactor = barStyles?.widthFactor ?? 1.5;
4625
+ const fill = resolveFill(entry);
4626
+ series.data.forEach((datum, i) => {
4627
+ const bandPosition = Number(bandScale(bandAccessor(datum)));
4628
+ const valuePosition = Number(valueScale(Number(valueAccessor(datum))));
4629
+ if (!Number.isFinite(bandPosition) || !Number.isFinite(valuePosition)) {
4630
+ if (process.env.NODE_ENV !== "production" && !Number.isFinite(bandPosition)) console.warn(`[Charts] ComparisonBars: datum key "${String(bandAccessor(datum))}" did not match any primary category. Shadow will not be rendered. Ensure comparison series data uses the same label/date keys as the primary series.`);
4631
+ return;
4632
+ }
4633
+ const rect = computeComparisonRect({
4634
+ horizontal,
4635
+ bandPosition,
4636
+ slotOffset,
4637
+ slotThickness,
4638
+ valuePosition,
4639
+ baseline,
4640
+ widthFactor
4641
+ });
4642
+ rects.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
4643
+ x: rect.x,
4644
+ y: rect.y,
4645
+ width: rect.width,
4646
+ height: rect.height,
4647
+ fill,
4648
+ opacity
4649
+ }, `${index}-${i}`));
4650
+ });
4651
+ });
4652
+ if (rects.length === 0) return null;
4653
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
4654
+ className: "bar-chart__comparison-bars",
4655
+ pointerEvents: "none",
4656
+ children: rects
4657
+ });
4658
+ };
4659
+ //#endregion
4450
4660
  //#region src/charts/bar-chart/bar-chart.tsx
4451
4661
  const validateData$2 = (data) => {
4452
4662
  if (!data?.length) return "No data available";
@@ -4475,13 +4685,14 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4475
4685
  }, [height]);
4476
4686
  const [selectedIndex, setSelectedIndex] = (0, react$1.useState)(void 0);
4477
4687
  const [isNavigating, setIsNavigating] = (0, react$1.useState)(false);
4688
+ const primarySeriesForNav = dataWithVisibleZeros.filter((s) => s.options?.type !== "comparison");
4478
4689
  const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
4479
4690
  selectedIndex,
4480
4691
  setSelectedIndex,
4481
4692
  isNavigating,
4482
4693
  setIsNavigating,
4483
4694
  chartRef,
4484
- totalPoints: Math.max(0, ...data.map((series) => series.data?.length || 0)) * data.length
4695
+ totalPoints: Math.max(0, ...primarySeriesForNav.map((s) => s.data?.length || 0)) * primarySeriesForNav.length
4485
4696
  });
4486
4697
  const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4487
4698
  const seriesWithVisibility = (0, react$1.useMemo)(() => {
@@ -4504,6 +4715,69 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4504
4715
  const allSeriesHidden = (0, react$1.useMemo)(() => {
4505
4716
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
4506
4717
  }, [seriesWithVisibility]);
4718
+ const primaryEntries = (0, react$1.useMemo)(() => seriesWithVisibility.filter(({ isVisible, series }) => isVisible && series.options?.type !== "comparison"), [seriesWithVisibility]);
4719
+ const primaryKeys = (0, react$1.useMemo)(() => primaryEntries.map(({ series }) => series.label), [primaryEntries]);
4720
+ const comparisonEntries = (0, react$1.useMemo)(() => {
4721
+ const primaryByGroup = new Map(primaryEntries.map(({ series, index }) => [series.group, {
4722
+ label: series.label,
4723
+ index
4724
+ }]));
4725
+ const entries = [];
4726
+ seriesWithVisibility.forEach(({ series, index, isVisible }) => {
4727
+ if (!isVisible || series.options?.type !== "comparison") return;
4728
+ const primary = primaryByGroup.get(series.group) ?? (primaryEntries.length === 1 ? {
4729
+ label: primaryEntries[0].series.label,
4730
+ index: primaryEntries[0].index
4731
+ } : void 0);
4732
+ if (!primary || !primaryKeys.includes(primary.label)) return;
4733
+ entries.push({
4734
+ series,
4735
+ index,
4736
+ primaryKey: primary.label,
4737
+ primaryIndex: primary.index
4738
+ });
4739
+ });
4740
+ return entries;
4741
+ }, [
4742
+ seriesWithVisibility,
4743
+ primaryEntries,
4744
+ primaryKeys
4745
+ ]);
4746
+ const comparisonWidthFactor = (0, react$1.useMemo)(() => {
4747
+ if (comparisonEntries.length === 0) return void 0;
4748
+ return getElementStyles({
4749
+ data: comparisonEntries[0].series,
4750
+ index: comparisonEntries[0].index
4751
+ }).barStyles?.widthFactor ?? 1.5;
4752
+ }, [comparisonEntries, getElementStyles]);
4753
+ const groupPadding = (0, react$1.useMemo)(() => {
4754
+ const basePadding = chartOptions.barGroup.padding;
4755
+ if (!comparisonWidthFactor || comparisonWidthFactor <= 1) return basePadding;
4756
+ const p = 1 - (1 - COMPARISON_INNER_GAP) / comparisonWidthFactor;
4757
+ return Math.min(Math.max(p, basePadding), MAX_GROUP_PADDING);
4758
+ }, [chartOptions.barGroup.padding, comparisonWidthFactor]);
4759
+ const { xScale, yScale } = (0, react$1.useMemo)(() => {
4760
+ if (comparisonEntries.length === 0) return {
4761
+ xScale: chartOptions.xScale,
4762
+ yScale: chartOptions.yScale
4763
+ };
4764
+ const tighten = (scale) => ({
4765
+ ...scale,
4766
+ paddingInner: (scale.paddingInner ?? .1) * COMPARISON_TICK_GAP_FACTOR
4767
+ });
4768
+ return horizontal ? {
4769
+ xScale: chartOptions.xScale,
4770
+ yScale: tighten(chartOptions.yScale)
4771
+ } : {
4772
+ xScale: tighten(chartOptions.xScale),
4773
+ yScale: chartOptions.yScale
4774
+ };
4775
+ }, [
4776
+ comparisonEntries.length,
4777
+ chartOptions.xScale,
4778
+ chartOptions.yScale,
4779
+ horizontal
4780
+ ]);
4507
4781
  const getBarBackground = (0, react$1.useCallback)((index) => () => withPatterns ? `url(#${getPatternId(chartId, index)})` : getElementStyles({
4508
4782
  data: dataSorted[index],
4509
4783
  index
@@ -4513,26 +4787,70 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4513
4787
  dataSorted,
4514
4788
  chartId
4515
4789
  ]);
4790
+ const resolveComparisonFill = (0, react$1.useCallback)((entry) => withPatterns ? `url(#${getPatternId(chartId, entry.primaryIndex)})` : getElementStyles({
4791
+ data: entry.series,
4792
+ index: entry.index
4793
+ }).color, [
4794
+ withPatterns,
4795
+ chartId,
4796
+ getElementStyles
4797
+ ]);
4516
4798
  const renderDefaultTooltip = (0, react$1.useCallback)(({ tooltipData }) => {
4517
4799
  const nearestDatum = tooltipData?.nearestDatum?.datum;
4518
4800
  if (!nearestDatum) return null;
4801
+ const primaryKey = tooltipData?.nearestDatum?.key;
4802
+ const categoryLabel = chartOptions.tooltip.labelFormatter(nearestDatum.label || (nearestDatum.date ? nearestDatum.date.getTime() : 0), 0, []);
4803
+ const comparisonEntry = comparisonEntries.find((entry) => entry.primaryKey === primaryKey);
4804
+ const comparisonDatum = comparisonEntry?.series.data.find((point) => {
4805
+ const p = point;
4806
+ return nearestDatum.label != null ? p.label === nearestDatum.label : !!nearestDatum.date && !!p.date && p.date.getTime() === nearestDatum.date.getTime();
4807
+ });
4808
+ if (comparisonEntry && comparisonDatum && comparisonDatum.value != null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4809
+ className: bar_chart_module_default["bar-chart__tooltip"],
4810
+ children: [
4811
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4812
+ className: bar_chart_module_default["bar-chart__tooltip-header"],
4813
+ children: categoryLabel
4814
+ }),
4815
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4816
+ className: bar_chart_module_default["bar-chart__tooltip-row"],
4817
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4818
+ className: bar_chart_module_default["bar-chart__tooltip-label"],
4819
+ children: [primaryKey, ":"]
4820
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4821
+ className: bar_chart_module_default["bar-chart__tooltip-value"],
4822
+ children: (0, _automattic_number_formatters.formatNumber)(nearestDatum.value)
4823
+ })]
4824
+ }),
4825
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4826
+ className: bar_chart_module_default["bar-chart__tooltip-row"],
4827
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4828
+ className: bar_chart_module_default["bar-chart__tooltip-label"],
4829
+ children: [comparisonEntry.series.label, ":"]
4830
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4831
+ className: bar_chart_module_default["bar-chart__tooltip-value"],
4832
+ children: (0, _automattic_number_formatters.formatNumber)(comparisonDatum.value)
4833
+ })]
4834
+ })
4835
+ ]
4836
+ });
4519
4837
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4520
4838
  className: bar_chart_module_default["bar-chart__tooltip"],
4521
4839
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4522
4840
  className: bar_chart_module_default["bar-chart__tooltip-header"],
4523
- children: tooltipData?.nearestDatum?.key
4841
+ children: primaryKey
4524
4842
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4525
4843
  className: bar_chart_module_default["bar-chart__tooltip-row"],
4526
4844
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4527
4845
  className: bar_chart_module_default["bar-chart__tooltip-label"],
4528
- children: [chartOptions.tooltip.labelFormatter(nearestDatum.label || (nearestDatum.date ? nearestDatum.date.getTime() : 0), 0, []), ":"]
4846
+ children: [categoryLabel, ":"]
4529
4847
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4530
4848
  className: bar_chart_module_default["bar-chart__tooltip-value"],
4531
4849
  children: (0, _automattic_number_formatters.formatNumber)(nearestDatum.value)
4532
4850
  })]
4533
4851
  })]
4534
4852
  });
4535
- }, [chartOptions.tooltip]);
4853
+ }, [chartOptions.tooltip, comparisonEntries]);
4536
4854
  const renderPattern = (0, react$1.useCallback)((index, color) => {
4537
4855
  const patternType = index % 4;
4538
4856
  const id = getPatternId(chartId, index);
@@ -4569,8 +4887,10 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4569
4887
  }
4570
4888
  }, [chartId]);
4571
4889
  const createPatternBorderStyle = (0, react$1.useCallback)((index, color) => {
4890
+ const patternId = getPatternId(chartId, index);
4572
4891
  return `
4573
- .visx-bar[fill="url(#${getPatternId(chartId, index)})"] {
4892
+ .visx-bar[fill="url(#${patternId})"],
4893
+ .bar-chart__comparison-bars rect[fill="url(#${patternId})"] {
4574
4894
  stroke: ${color};
4575
4895
  stroke-width: 1;
4576
4896
  }
@@ -4578,11 +4898,13 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4578
4898
  }, [chartId]);
4579
4899
  const createKeyboardHighlightStyle = (0, react$1.useCallback)(() => {
4580
4900
  if (selectedIndex === void 0) return "";
4581
- const maxDataPoints = Math.max(...data.map((s) => s.data.length));
4582
- const dataPointIndex = Math.floor(selectedIndex / data.length);
4583
- const seriesIndex = selectedIndex % data.length;
4584
- if (dataPointIndex >= maxDataPoints || seriesIndex >= data.length) return "";
4585
- if (dataPointIndex >= data[seriesIndex].data.length) return "";
4901
+ const primaryCount = primaryEntries.length;
4902
+ const maxDataPoints = Math.max(...primaryEntries.map((e) => e.series.data.length));
4903
+ const dataPointIndex = Math.floor(selectedIndex / primaryCount);
4904
+ const seriesIndex = selectedIndex % primaryCount;
4905
+ if (dataPointIndex >= maxDataPoints || seriesIndex >= primaryCount) return "";
4906
+ const seriesData = primaryEntries[seriesIndex]?.series;
4907
+ if (!seriesData || dataPointIndex >= seriesData.data.length) return "";
4586
4908
  return `
4587
4909
  .bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${seriesIndex * maxDataPoints + dataPointIndex + 1}) {
4588
4910
  stroke: #005fcc;
@@ -4591,7 +4913,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4591
4913
  `;
4592
4914
  }, [
4593
4915
  selectedIndex,
4594
- data,
4916
+ primaryEntries,
4595
4917
  chartId
4596
4918
  ]);
4597
4919
  const error = validateData$2(dataSorted);
@@ -4664,8 +4986,8 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4664
4986
  ...defaultMargin,
4665
4987
  ...margin
4666
4988
  },
4667
- xScale: chartOptions.xScale,
4668
- yScale: chartOptions.yScale,
4989
+ xScale,
4990
+ yScale,
4669
4991
  horizontal,
4670
4992
  pointerEventsDataKey: "nearest",
4671
4993
  children: [
@@ -4689,18 +5011,25 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4689
5011
  height: chartHeight,
4690
5012
  children: (0, _wordpress_i18n.__)("All series are hidden. Click legend items to show data.", "jetpack-charts")
4691
5013
  }) : null,
5014
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComparisonBars, {
5015
+ comparisonEntries,
5016
+ primaryKeys,
5017
+ groupPadding,
5018
+ horizontal,
5019
+ xAccessor: chartOptions.accessors.xAccessor,
5020
+ yAccessor: chartOptions.accessors.yAccessor,
5021
+ getElementStyles,
5022
+ resolveFill: resolveComparisonFill
5023
+ }),
4692
5024
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.BarGroup, {
4693
- padding: chartOptions.barGroup.padding,
4694
- children: seriesWithVisibility.map(({ series: seriesData, index, isVisible }) => {
4695
- if (!isVisible) return null;
4696
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.BarSeries, {
4697
- dataKey: seriesData?.label,
4698
- data: seriesData.data,
4699
- yAccessor: chartOptions.accessors.yAccessor,
4700
- xAccessor: chartOptions.accessors.xAccessor,
4701
- colorAccessor: getBarBackground(index)
4702
- }, seriesData?.label);
4703
- })
5025
+ padding: groupPadding,
5026
+ children: primaryEntries.map(({ series: seriesData, index }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.BarSeries, {
5027
+ dataKey: seriesData?.label,
5028
+ data: seriesData.data,
5029
+ yAccessor: chartOptions.accessors.yAccessor,
5030
+ xAccessor: chartOptions.accessors.xAccessor,
5031
+ colorAccessor: getBarBackground(index)
5032
+ }, seriesData?.label))
4704
5033
  }),
4705
5034
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.x }),
4706
5035
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.y }),
@@ -5313,9 +5642,7 @@ const DEFAULT_BACKGROUND_COLOR = "#ffffff";
5313
5642
  */
5314
5643
  const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", renderPlaceholder }) => {
5315
5644
  const { getElementStyles, theme: { geoChart: { featureFillColor }, backgroundColor } } = useGlobalChartsContext();
5316
- const loadingPlaceholder = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
5317
- align: "center",
5318
- justify: "center",
5645
+ const loadingPlaceholder = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
5319
5646
  className: (0, clsx.default)("geo-chart", geo_chart_module_default.container, className),
5320
5647
  style: {
5321
5648
  width,
@@ -5373,9 +5700,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
5373
5700
  defaultFillColorHex,
5374
5701
  sanitizedData.hasHtmlTooltips
5375
5702
  ]);
5376
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
5377
- align: "center",
5378
- justify: "center",
5703
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
5379
5704
  className: (0, clsx.default)("geo-chart", geo_chart_module_default.container, className),
5380
5705
  style: {
5381
5706
  width,
@@ -8142,13 +8467,15 @@ const defaultDeltaFormatter = (value) => {
8142
8467
  };
8143
8468
  /**
8144
8469
  * Build a bar's width. A hover-inset CSS variable (0 by default) is subtracted
8145
- * so interactive rows can pull the bar's right edge back by a fixed pixel amount
8146
- * on hover instead of a percentage scale keeping the bar↔value gap constant.
8470
+ * on hover, scaled by the bar's share so the pull-back is proportional to its
8471
+ * length: the full-length (100%) bar the one that reaches the value pulls
8472
+ * back the whole inset to keep its gap with the value, while shorter bars pull
8473
+ * back proportionally less, down to ~0 for a very short bar.
8147
8474
  *
8148
8475
  * @param share - The bar's share of the row width, as a percentage.
8149
8476
  * @return A CSS width value.
8150
8477
  */
8151
- const getBarWidth = (share) => `calc(${share}% - var(--a8c--charts--leaderboard--bar--hover-inset, 0px))`;
8478
+ const getBarWidth = (share) => `calc(${share}% - var(--a8c--charts--leaderboard--bar--hover-inset, 0px) * ${share} / 100)`;
8152
8479
  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, {
8153
8480
  className: leaderboard_chart_module_default.label,
8154
8481
  children: label
@@ -8357,6 +8684,7 @@ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propW
8357
8684
  type: "button",
8358
8685
  className: leaderboard_chart_module_default.interactiveRow,
8359
8686
  onClick: entry.onClick,
8687
+ "aria-label": entry.ariaLabel,
8360
8688
  children: [rowCells, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_wordpress_icons.Icon, {
8361
8689
  className: leaderboard_chart_module_default.chevron,
8362
8690
  icon: _wordpress_icons.chevronRight,
@@ -8419,7 +8747,6 @@ function RadialWipeAnimation({ id, radius, innerRadius = 0, durationMs = 1e3, wi
8419
8747
  //#region src/charts/pie-chart/pie-chart.module.scss
8420
8748
  var pie_chart_module_default = {
8421
8749
  "pie-chart": "a8ccharts-gnszbG-pie-chart",
8422
- "pie-chart__centering": "a8ccharts-gnszbG-pie-chart__centering",
8423
8750
  "pie-chart--responsive": "a8ccharts-gnszbG-pie-chart--responsive"
8424
8751
  };
8425
8752
  //#endregion
@@ -8584,11 +8911,8 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
8584
8911
  const innerRadius = thickness === 0 ? 0 : outerRadius * (1 - thickness);
8585
8912
  const maxCornerRadius = (outerRadius - innerRadius) / 2;
8586
8913
  const cornerRadius = cornerScale ? Math.min(cornerScale * outerRadius, maxCornerRadius) : 0;
8587
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
8914
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
8588
8915
  ref: containerRef,
8589
- align: "center",
8590
- justify: "center",
8591
- className: pie_chart_module_default["pie-chart__centering"],
8592
8916
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
8593
8917
  viewBox: `0 0 ${width} ${height}`,
8594
8918
  preserveAspectRatio: "xMidYMid meet",
@@ -8701,7 +9025,6 @@ var pie_semi_circle_chart_module_default = {
8701
9025
  "label": "a8ccharts-YtTOxW-label",
8702
9026
  "note": "a8ccharts-YtTOxW-note",
8703
9027
  "pie-semi-circle-chart": "a8ccharts-YtTOxW-pie-semi-circle-chart",
8704
- "pie-semi-circle-chart__centering": "a8ccharts-YtTOxW-pie-semi-circle-chart__centering",
8705
9028
  "pie-semi-circle-chart--responsive": "a8ccharts-YtTOxW-pie-semi-circle-chart--responsive"
8706
9029
  };
8707
9030
  //#endregion
@@ -8881,11 +9204,8 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
8881
9204
  const height = width / 2;
8882
9205
  const radius = height;
8883
9206
  const innerRadius = radius * (1 - thickness);
8884
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
9207
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
8885
9208
  ref: containerRef,
8886
- align: "center",
8887
- justify: "center",
8888
- className: pie_semi_circle_chart_module_default["pie-semi-circle-chart__centering"],
8889
9209
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
8890
9210
  width,
8891
9211
  height,