@automattic/charts 1.7.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 (37) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/index.cjs +358 -51
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +6 -4
  5. package/dist/index.d.cts +16 -0
  6. package/dist/index.d.ts +16 -0
  7. package/dist/index.js +358 -51
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/charts/bar-chart/bar-chart.tsx +196 -42
  11. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +70 -0
  12. package/src/charts/bar-chart/private/comparison-bars.tsx +154 -0
  13. package/src/charts/bar-chart/private/comparison-constants.ts +33 -0
  14. package/src/charts/bar-chart/private/index.ts +9 -0
  15. package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +47 -0
  16. package/src/charts/bar-chart/private/test/comparison-bars.test.tsx +183 -0
  17. package/src/charts/bar-chart/private/use-bar-chart-options.ts +46 -5
  18. package/src/charts/bar-chart/test/bar-chart.test.tsx +191 -1
  19. package/src/charts/geo-chart/geo-chart.tsx +5 -9
  20. package/src/charts/pie-chart/pie-chart.module.scss +0 -4
  21. package/src/charts/pie-chart/pie-chart.tsx +3 -8
  22. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +0 -5
  23. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +3 -8
  24. package/src/charts/private/center/center.module.scss +4 -0
  25. package/src/charts/private/center/center.tsx +33 -0
  26. package/src/charts/private/center/index.ts +2 -0
  27. package/src/charts/private/center/test/center.test.tsx +60 -0
  28. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +0 -2
  29. package/src/charts/private/svg-empty-state/svg-empty-state.tsx +2 -4
  30. package/src/providers/chart-context/global-charts-provider.tsx +2 -0
  31. package/src/providers/chart-context/test/chart-context.test.tsx +13 -1
  32. package/src/providers/chart-context/themes.ts +8 -0
  33. package/src/providers/chart-context/types.ts +2 -0
  34. package/src/types.ts +16 -0
  35. package/src/utils/get-styles.ts +36 -13
  36. package/src/utils/index.ts +6 -1
  37. package/src/utils/test/get-styles.test.ts +47 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ 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.8.0] - 2026-06-24
9
+ ### Added
10
+ - Add comparison mode to the Bar Chart — a translucent shadow bar (standard slot width, 50% opacity) rendered behind each primary bar, paired by group. Primary bars are narrowed to 1/widthFactor of the slot (default widthFactor 1.5 → ~67% width, centered), with widthFactor as the single control. [#49676]
11
+
12
+ ### Changed
13
+ - Add an internal Center layout primitive and use it for centered chart wrappers. [#49164]
14
+
8
15
  ## [1.7.0] - 2026-06-23
9
16
  ### Added
10
17
  - Leaderboard interactive rows gain an opt-in `ariaLabel` for image-only labels, and the hover affordance now shrinks each bar in proportion to its length so small-share rows stay consistent. [#49812]
@@ -878,6 +885,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
878
885
  - Fixed lints following ESLint rule changes for TS [#40584]
879
886
  - Fixing a bug in Chart storybook data. [#40640]
880
887
 
888
+ [1.8.0]: https://github.com/Automattic/charts/compare/v1.7.0...v1.8.0
881
889
  [1.7.0]: https://github.com/Automattic/charts/compare/v1.6.0...v1.7.0
882
890
  [1.6.0]: https://github.com/Automattic/charts/compare/v1.5.3...v1.6.0
883
891
  [1.5.3]: https://github.com/Automattic/charts/compare/v1.5.2...v1.5.3
package/dist/index.cjs CHANGED
@@ -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
@@ -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
  })
@@ -4345,6 +4392,10 @@ const TruncatedXTickComponent = createTruncatedTickComponent("x");
4345
4392
  const TruncatedYTickComponent = createTruncatedTickComponent("y");
4346
4393
  //#endregion
4347
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;
4348
4399
  const formatDateTick = (timestamp) => {
4349
4400
  return new Date(timestamp).toLocaleDateString(void 0, {
4350
4401
  month: "short",
@@ -4372,8 +4423,8 @@ function useBarChartOptions(data, horizontal, options = {}) {
4372
4423
  const defaultOptions = (0, react$1.useMemo)(() => {
4373
4424
  const bandScale = {
4374
4425
  type: "band",
4375
- padding: .2,
4376
- paddingInner: .1
4426
+ padding: BASE_BAND_PADDING,
4427
+ paddingInner: BASE_BAND_PADDING_INNER
4377
4428
  };
4378
4429
  const linearScale = {
4379
4430
  type: "linear",
@@ -4412,13 +4463,29 @@ function useBarChartOptions(data, horizontal, options = {}) {
4412
4463
  }, [data]);
4413
4464
  return (0, react$1.useMemo)(() => {
4414
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
+ }
4415
4480
  const xScale = {
4416
4481
  ...baseXScale,
4417
- ...options.xScale || {}
4482
+ ...options.xScale || {},
4483
+ ...horizontal ? valueScaleDomainOverride : {}
4418
4484
  };
4419
4485
  const yScale = {
4420
4486
  ...baseYScale,
4421
- ...options.yScale || {}
4487
+ ...options.yScale || {},
4488
+ ...!horizontal ? valueScaleDomainOverride : {}
4422
4489
  };
4423
4490
  const providedToolTipLabelFormatter = horizontal ? options.axis?.y?.tickFormat : options.axis?.x?.tickFormat;
4424
4491
  const { labelOverflow: xLabelOverflow, ...xAxisOptions } = options.axis?.x || {};
@@ -4453,10 +4520,143 @@ function useBarChartOptions(data, horizontal, options = {}) {
4453
4520
  }, [
4454
4521
  defaultOptions,
4455
4522
  options,
4456
- horizontal
4523
+ horizontal,
4524
+ data
4457
4525
  ]);
4458
4526
  }
4459
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
4460
4660
  //#region src/charts/bar-chart/bar-chart.tsx
4461
4661
  const validateData$2 = (data) => {
4462
4662
  if (!data?.length) return "No data available";
@@ -4485,13 +4685,14 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4485
4685
  }, [height]);
4486
4686
  const [selectedIndex, setSelectedIndex] = (0, react$1.useState)(void 0);
4487
4687
  const [isNavigating, setIsNavigating] = (0, react$1.useState)(false);
4688
+ const primarySeriesForNav = dataWithVisibleZeros.filter((s) => s.options?.type !== "comparison");
4488
4689
  const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
4489
4690
  selectedIndex,
4490
4691
  setSelectedIndex,
4491
4692
  isNavigating,
4492
4693
  setIsNavigating,
4493
4694
  chartRef,
4494
- 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
4495
4696
  });
4496
4697
  const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4497
4698
  const seriesWithVisibility = (0, react$1.useMemo)(() => {
@@ -4514,6 +4715,69 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4514
4715
  const allSeriesHidden = (0, react$1.useMemo)(() => {
4515
4716
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
4516
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
+ ]);
4517
4781
  const getBarBackground = (0, react$1.useCallback)((index) => () => withPatterns ? `url(#${getPatternId(chartId, index)})` : getElementStyles({
4518
4782
  data: dataSorted[index],
4519
4783
  index
@@ -4523,26 +4787,70 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4523
4787
  dataSorted,
4524
4788
  chartId
4525
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
+ ]);
4526
4798
  const renderDefaultTooltip = (0, react$1.useCallback)(({ tooltipData }) => {
4527
4799
  const nearestDatum = tooltipData?.nearestDatum?.datum;
4528
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
+ });
4529
4837
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4530
4838
  className: bar_chart_module_default["bar-chart__tooltip"],
4531
4839
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4532
4840
  className: bar_chart_module_default["bar-chart__tooltip-header"],
4533
- children: tooltipData?.nearestDatum?.key
4841
+ children: primaryKey
4534
4842
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4535
4843
  className: bar_chart_module_default["bar-chart__tooltip-row"],
4536
4844
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4537
4845
  className: bar_chart_module_default["bar-chart__tooltip-label"],
4538
- children: [chartOptions.tooltip.labelFormatter(nearestDatum.label || (nearestDatum.date ? nearestDatum.date.getTime() : 0), 0, []), ":"]
4846
+ children: [categoryLabel, ":"]
4539
4847
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4540
4848
  className: bar_chart_module_default["bar-chart__tooltip-value"],
4541
4849
  children: (0, _automattic_number_formatters.formatNumber)(nearestDatum.value)
4542
4850
  })]
4543
4851
  })]
4544
4852
  });
4545
- }, [chartOptions.tooltip]);
4853
+ }, [chartOptions.tooltip, comparisonEntries]);
4546
4854
  const renderPattern = (0, react$1.useCallback)((index, color) => {
4547
4855
  const patternType = index % 4;
4548
4856
  const id = getPatternId(chartId, index);
@@ -4579,8 +4887,10 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4579
4887
  }
4580
4888
  }, [chartId]);
4581
4889
  const createPatternBorderStyle = (0, react$1.useCallback)((index, color) => {
4890
+ const patternId = getPatternId(chartId, index);
4582
4891
  return `
4583
- .visx-bar[fill="url(#${getPatternId(chartId, index)})"] {
4892
+ .visx-bar[fill="url(#${patternId})"],
4893
+ .bar-chart__comparison-bars rect[fill="url(#${patternId})"] {
4584
4894
  stroke: ${color};
4585
4895
  stroke-width: 1;
4586
4896
  }
@@ -4588,11 +4898,13 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4588
4898
  }, [chartId]);
4589
4899
  const createKeyboardHighlightStyle = (0, react$1.useCallback)(() => {
4590
4900
  if (selectedIndex === void 0) return "";
4591
- const maxDataPoints = Math.max(...data.map((s) => s.data.length));
4592
- const dataPointIndex = Math.floor(selectedIndex / data.length);
4593
- const seriesIndex = selectedIndex % data.length;
4594
- if (dataPointIndex >= maxDataPoints || seriesIndex >= data.length) return "";
4595
- 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 "";
4596
4908
  return `
4597
4909
  .bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${seriesIndex * maxDataPoints + dataPointIndex + 1}) {
4598
4910
  stroke: #005fcc;
@@ -4601,7 +4913,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4601
4913
  `;
4602
4914
  }, [
4603
4915
  selectedIndex,
4604
- data,
4916
+ primaryEntries,
4605
4917
  chartId
4606
4918
  ]);
4607
4919
  const error = validateData$2(dataSorted);
@@ -4674,8 +4986,8 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4674
4986
  ...defaultMargin,
4675
4987
  ...margin
4676
4988
  },
4677
- xScale: chartOptions.xScale,
4678
- yScale: chartOptions.yScale,
4989
+ xScale,
4990
+ yScale,
4679
4991
  horizontal,
4680
4992
  pointerEventsDataKey: "nearest",
4681
4993
  children: [
@@ -4699,18 +5011,25 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4699
5011
  height: chartHeight,
4700
5012
  children: (0, _wordpress_i18n.__)("All series are hidden. Click legend items to show data.", "jetpack-charts")
4701
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
+ }),
4702
5024
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.BarGroup, {
4703
- padding: chartOptions.barGroup.padding,
4704
- children: seriesWithVisibility.map(({ series: seriesData, index, isVisible }) => {
4705
- if (!isVisible) return null;
4706
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.BarSeries, {
4707
- dataKey: seriesData?.label,
4708
- data: seriesData.data,
4709
- yAccessor: chartOptions.accessors.yAccessor,
4710
- xAccessor: chartOptions.accessors.xAccessor,
4711
- colorAccessor: getBarBackground(index)
4712
- }, seriesData?.label);
4713
- })
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))
4714
5033
  }),
4715
5034
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.x }),
4716
5035
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.y }),
@@ -5323,9 +5642,7 @@ const DEFAULT_BACKGROUND_COLOR = "#ffffff";
5323
5642
  */
5324
5643
  const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", renderPlaceholder }) => {
5325
5644
  const { getElementStyles, theme: { geoChart: { featureFillColor }, backgroundColor } } = useGlobalChartsContext();
5326
- const loadingPlaceholder = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
5327
- align: "center",
5328
- justify: "center",
5645
+ const loadingPlaceholder = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
5329
5646
  className: (0, clsx.default)("geo-chart", geo_chart_module_default.container, className),
5330
5647
  style: {
5331
5648
  width,
@@ -5383,9 +5700,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
5383
5700
  defaultFillColorHex,
5384
5701
  sanitizedData.hasHtmlTooltips
5385
5702
  ]);
5386
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
5387
- align: "center",
5388
- justify: "center",
5703
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
5389
5704
  className: (0, clsx.default)("geo-chart", geo_chart_module_default.container, className),
5390
5705
  style: {
5391
5706
  width,
@@ -8432,7 +8747,6 @@ function RadialWipeAnimation({ id, radius, innerRadius = 0, durationMs = 1e3, wi
8432
8747
  //#region src/charts/pie-chart/pie-chart.module.scss
8433
8748
  var pie_chart_module_default = {
8434
8749
  "pie-chart": "a8ccharts-gnszbG-pie-chart",
8435
- "pie-chart__centering": "a8ccharts-gnszbG-pie-chart__centering",
8436
8750
  "pie-chart--responsive": "a8ccharts-gnszbG-pie-chart--responsive"
8437
8751
  };
8438
8752
  //#endregion
@@ -8597,11 +8911,8 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
8597
8911
  const innerRadius = thickness === 0 ? 0 : outerRadius * (1 - thickness);
8598
8912
  const maxCornerRadius = (outerRadius - innerRadius) / 2;
8599
8913
  const cornerRadius = cornerScale ? Math.min(cornerScale * outerRadius, maxCornerRadius) : 0;
8600
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
8914
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
8601
8915
  ref: containerRef,
8602
- align: "center",
8603
- justify: "center",
8604
- className: pie_chart_module_default["pie-chart__centering"],
8605
8916
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
8606
8917
  viewBox: `0 0 ${width} ${height}`,
8607
8918
  preserveAspectRatio: "xMidYMid meet",
@@ -8714,7 +9025,6 @@ var pie_semi_circle_chart_module_default = {
8714
9025
  "label": "a8ccharts-YtTOxW-label",
8715
9026
  "note": "a8ccharts-YtTOxW-note",
8716
9027
  "pie-semi-circle-chart": "a8ccharts-YtTOxW-pie-semi-circle-chart",
8717
- "pie-semi-circle-chart__centering": "a8ccharts-YtTOxW-pie-semi-circle-chart__centering",
8718
9028
  "pie-semi-circle-chart--responsive": "a8ccharts-YtTOxW-pie-semi-circle-chart--responsive"
8719
9029
  };
8720
9030
  //#endregion
@@ -8894,11 +9204,8 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
8894
9204
  const height = width / 2;
8895
9205
  const radius = height;
8896
9206
  const innerRadius = radius * (1 - thickness);
8897
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
9207
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
8898
9208
  ref: containerRef,
8899
- align: "center",
8900
- justify: "center",
8901
- className: pie_semi_circle_chart_module_default["pie-semi-circle-chart__centering"],
8902
9209
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
8903
9210
  width,
8904
9211
  height,