@automattic/charts 1.7.0 → 1.8.1

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 +13 -0
  2. package/dist/index.cjs +347 -61
  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 +348 -62
  8. package/dist/index.js.map +1 -1
  9. package/package.json +2 -2
  10. package/src/charts/bar-chart/bar-chart.tsx +210 -49
  11. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +70 -0
  12. package/src/charts/bar-chart/private/comparison-bars.tsx +155 -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 +49 -5
  18. package/src/charts/bar-chart/test/bar-chart.test.tsx +329 -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,17 @@ 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.1] - 2026-06-26
9
+ ### Fixed
10
+ - Fix Bar Chart comparison mode — pair the keyboard tooltip with the focused bar, keep the value axis zero-based, and make the tooltip label/value separator translatable. [#49959]
11
+
12
+ ## [1.8.0] - 2026-06-24
13
+ ### Added
14
+ - 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]
15
+
16
+ ### Changed
17
+ - Add an internal Center layout primitive and use it for centered chart wrappers. [#49164]
18
+
8
19
  ## [1.7.0] - 2026-06-23
9
20
  ### Added
10
21
  - 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 +889,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
878
889
  - Fixed lints following ESLint rule changes for TS [#40584]
879
890
  - Fixing a bug in Chart storybook data. [#40640]
880
891
 
892
+ [1.8.1]: https://github.com/Automattic/charts/compare/v1.8.0...v1.8.1
893
+ [1.8.0]: https://github.com/Automattic/charts/compare/v1.7.0...v1.8.0
881
894
  [1.7.0]: https://github.com/Automattic/charts/compare/v1.6.0...v1.7.0
882
895
  [1.6.0]: https://github.com/Automattic/charts/compare/v1.5.3...v1.6.0
883
896
  [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(0, ...allValues), Math.max(0, ...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,144 @@ 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
+ "aria-hidden": "true",
4657
+ children: rects
4658
+ });
4659
+ };
4660
+ //#endregion
4460
4661
  //#region src/charts/bar-chart/bar-chart.tsx
4461
4662
  const validateData$2 = (data) => {
4462
4663
  if (!data?.length) return "No data available";
@@ -4464,6 +4665,10 @@ const validateData$2 = (data) => {
4464
4665
  return null;
4465
4666
  };
4466
4667
  const getPatternId = (chartId, index) => `bar-pattern-${chartId}-${index}`;
4668
+ const renderTooltipRow = (label, value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4669
+ className: bar_chart_module_default["bar-chart__tooltip-row"],
4670
+ children: (0, _wordpress_i18n.sprintf)((0, _wordpress_i18n.__)("%1$s: %2$s", "jetpack-charts"), label, value)
4671
+ });
4467
4672
  const BarChartInternal = ({ data, chartId: providedChartId, width, height, className, margin, withTooltips = false, showLegend = false, legend = {}, gridVisibility: gridVisibilityProp, renderTooltip, options = {}, orientation = "vertical", withPatterns = false, showZeroValues = false, animation, children, gap = "md" }) => {
4468
4673
  const legendInteractive = legend.interactive ?? false;
4469
4674
  const horizontal = orientation === "horizontal";
@@ -4485,13 +4690,14 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4485
4690
  }, [height]);
4486
4691
  const [selectedIndex, setSelectedIndex] = (0, react$1.useState)(void 0);
4487
4692
  const [isNavigating, setIsNavigating] = (0, react$1.useState)(false);
4693
+ const primarySeriesForNav = dataWithVisibleZeros.filter((s) => s.options?.type !== "comparison");
4488
4694
  const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
4489
4695
  selectedIndex,
4490
4696
  setSelectedIndex,
4491
4697
  isNavigating,
4492
4698
  setIsNavigating,
4493
4699
  chartRef,
4494
- totalPoints: Math.max(0, ...data.map((series) => series.data?.length || 0)) * data.length
4700
+ totalPoints: Math.max(0, ...primarySeriesForNav.map((s) => s.data?.length || 0)) * primarySeriesForNav.length
4495
4701
  });
4496
4702
  const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4497
4703
  const seriesWithVisibility = (0, react$1.useMemo)(() => {
@@ -4514,6 +4720,70 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4514
4720
  const allSeriesHidden = (0, react$1.useMemo)(() => {
4515
4721
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
4516
4722
  }, [seriesWithVisibility]);
4723
+ const primaryEntries = (0, react$1.useMemo)(() => seriesWithVisibility.filter(({ isVisible, series }) => isVisible && series.options?.type !== "comparison"), [seriesWithVisibility]);
4724
+ const primaryKeys = (0, react$1.useMemo)(() => primaryEntries.map(({ series }) => series.label), [primaryEntries]);
4725
+ const primarySeries = (0, react$1.useMemo)(() => primaryEntries.map(({ series }) => series), [primaryEntries]);
4726
+ const comparisonEntries = (0, react$1.useMemo)(() => {
4727
+ const primaryByGroup = new Map(primaryEntries.map(({ series, index }) => [series.group, {
4728
+ label: series.label,
4729
+ index
4730
+ }]));
4731
+ const entries = [];
4732
+ seriesWithVisibility.forEach(({ series, index, isVisible }) => {
4733
+ if (!isVisible || series.options?.type !== "comparison") return;
4734
+ const primary = primaryByGroup.get(series.group) ?? (primaryEntries.length === 1 ? {
4735
+ label: primaryEntries[0].series.label,
4736
+ index: primaryEntries[0].index
4737
+ } : void 0);
4738
+ if (!primary || !primaryKeys.includes(primary.label)) return;
4739
+ entries.push({
4740
+ series,
4741
+ index,
4742
+ primaryKey: primary.label,
4743
+ primaryIndex: primary.index
4744
+ });
4745
+ });
4746
+ return entries;
4747
+ }, [
4748
+ seriesWithVisibility,
4749
+ primaryEntries,
4750
+ primaryKeys
4751
+ ]);
4752
+ const comparisonWidthFactor = (0, react$1.useMemo)(() => {
4753
+ if (comparisonEntries.length === 0) return void 0;
4754
+ return getElementStyles({
4755
+ data: comparisonEntries[0].series,
4756
+ index: comparisonEntries[0].index
4757
+ }).barStyles?.widthFactor ?? 1.5;
4758
+ }, [comparisonEntries, getElementStyles]);
4759
+ const groupPadding = (0, react$1.useMemo)(() => {
4760
+ const basePadding = chartOptions.barGroup.padding;
4761
+ if (!comparisonWidthFactor || comparisonWidthFactor <= 1) return basePadding;
4762
+ const p = 1 - (1 - COMPARISON_INNER_GAP) / comparisonWidthFactor;
4763
+ return Math.min(Math.max(p, basePadding), MAX_GROUP_PADDING);
4764
+ }, [chartOptions.barGroup.padding, comparisonWidthFactor]);
4765
+ const { xScale, yScale } = (0, react$1.useMemo)(() => {
4766
+ if (comparisonEntries.length === 0) return {
4767
+ xScale: chartOptions.xScale,
4768
+ yScale: chartOptions.yScale
4769
+ };
4770
+ const tighten = (scale) => ({
4771
+ ...scale,
4772
+ paddingInner: (scale.paddingInner ?? .1) * COMPARISON_TICK_GAP_FACTOR
4773
+ });
4774
+ return horizontal ? {
4775
+ xScale: chartOptions.xScale,
4776
+ yScale: tighten(chartOptions.yScale)
4777
+ } : {
4778
+ xScale: tighten(chartOptions.xScale),
4779
+ yScale: chartOptions.yScale
4780
+ };
4781
+ }, [
4782
+ comparisonEntries.length,
4783
+ chartOptions.xScale,
4784
+ chartOptions.yScale,
4785
+ horizontal
4786
+ ]);
4517
4787
  const getBarBackground = (0, react$1.useCallback)((index) => () => withPatterns ? `url(#${getPatternId(chartId, index)})` : getElementStyles({
4518
4788
  data: dataSorted[index],
4519
4789
  index
@@ -4523,26 +4793,43 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4523
4793
  dataSorted,
4524
4794
  chartId
4525
4795
  ]);
4796
+ const resolveComparisonFill = (0, react$1.useCallback)((entry) => withPatterns ? `url(#${getPatternId(chartId, entry.primaryIndex)})` : getElementStyles({
4797
+ data: entry.series,
4798
+ index: entry.index
4799
+ }).color, [
4800
+ withPatterns,
4801
+ chartId,
4802
+ getElementStyles
4803
+ ]);
4526
4804
  const renderDefaultTooltip = (0, react$1.useCallback)(({ tooltipData }) => {
4527
4805
  const nearestDatum = tooltipData?.nearestDatum?.datum;
4528
4806
  if (!nearestDatum) return null;
4807
+ const primaryKey = tooltipData?.nearestDatum?.key;
4808
+ const categoryLabel = chartOptions.tooltip.labelFormatter(nearestDatum.label || (nearestDatum.date ? nearestDatum.date.getTime() : 0), 0, []);
4809
+ const comparisonEntry = comparisonEntries.find((entry) => entry.primaryKey === primaryKey);
4810
+ const comparisonDatum = comparisonEntry?.series.data.find((point) => {
4811
+ const p = point;
4812
+ return nearestDatum.label != null ? p.label === nearestDatum.label : !!nearestDatum.date && !!p.date && p.date.getTime() === nearestDatum.date.getTime();
4813
+ });
4814
+ if (comparisonEntry && comparisonDatum && comparisonDatum.value != null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4815
+ className: bar_chart_module_default["bar-chart__tooltip"],
4816
+ children: [
4817
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4818
+ className: bar_chart_module_default["bar-chart__tooltip-header"],
4819
+ children: categoryLabel
4820
+ }),
4821
+ renderTooltipRow(primaryKey, (0, _automattic_number_formatters.formatNumber)(nearestDatum.value)),
4822
+ renderTooltipRow(comparisonEntry.series.label, (0, _automattic_number_formatters.formatNumber)(comparisonDatum.value))
4823
+ ]
4824
+ });
4529
4825
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4530
4826
  className: bar_chart_module_default["bar-chart__tooltip"],
4531
4827
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4532
4828
  className: bar_chart_module_default["bar-chart__tooltip-header"],
4533
- children: tooltipData?.nearestDatum?.key
4534
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4535
- className: bar_chart_module_default["bar-chart__tooltip-row"],
4536
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4537
- className: bar_chart_module_default["bar-chart__tooltip-label"],
4538
- children: [chartOptions.tooltip.labelFormatter(nearestDatum.label || (nearestDatum.date ? nearestDatum.date.getTime() : 0), 0, []), ":"]
4539
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4540
- className: bar_chart_module_default["bar-chart__tooltip-value"],
4541
- children: (0, _automattic_number_formatters.formatNumber)(nearestDatum.value)
4542
- })]
4543
- })]
4829
+ children: primaryKey
4830
+ }), renderTooltipRow(categoryLabel, (0, _automattic_number_formatters.formatNumber)(nearestDatum.value))]
4544
4831
  });
4545
- }, [chartOptions.tooltip]);
4832
+ }, [chartOptions.tooltip, comparisonEntries]);
4546
4833
  const renderPattern = (0, react$1.useCallback)((index, color) => {
4547
4834
  const patternType = index % 4;
4548
4835
  const id = getPatternId(chartId, index);
@@ -4579,8 +4866,10 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4579
4866
  }
4580
4867
  }, [chartId]);
4581
4868
  const createPatternBorderStyle = (0, react$1.useCallback)((index, color) => {
4869
+ const patternId = getPatternId(chartId, index);
4582
4870
  return `
4583
- .visx-bar[fill="url(#${getPatternId(chartId, index)})"] {
4871
+ .visx-bar[fill="url(#${patternId})"],
4872
+ .bar-chart__comparison-bars rect[fill="url(#${patternId})"] {
4584
4873
  stroke: ${color};
4585
4874
  stroke-width: 1;
4586
4875
  }
@@ -4588,11 +4877,13 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4588
4877
  }, [chartId]);
4589
4878
  const createKeyboardHighlightStyle = (0, react$1.useCallback)(() => {
4590
4879
  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 "";
4880
+ const primaryCount = primaryEntries.length;
4881
+ const maxDataPoints = Math.max(...primaryEntries.map((e) => e.series.data.length));
4882
+ const dataPointIndex = Math.floor(selectedIndex / primaryCount);
4883
+ const seriesIndex = selectedIndex % primaryCount;
4884
+ if (dataPointIndex >= maxDataPoints || seriesIndex >= primaryCount) return "";
4885
+ const seriesData = primaryEntries[seriesIndex]?.series;
4886
+ if (!seriesData || dataPointIndex >= seriesData.data.length) return "";
4596
4887
  return `
4597
4888
  .bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${seriesIndex * maxDataPoints + dataPointIndex + 1}) {
4598
4889
  stroke: #005fcc;
@@ -4601,7 +4892,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4601
4892
  `;
4602
4893
  }, [
4603
4894
  selectedIndex,
4604
- data,
4895
+ primaryEntries,
4605
4896
  chartId
4606
4897
  ]);
4607
4898
  const error = validateData$2(dataSorted);
@@ -4674,8 +4965,8 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4674
4965
  ...defaultMargin,
4675
4966
  ...margin
4676
4967
  },
4677
- xScale: chartOptions.xScale,
4678
- yScale: chartOptions.yScale,
4968
+ xScale,
4969
+ yScale,
4679
4970
  horizontal,
4680
4971
  pointerEventsDataKey: "nearest",
4681
4972
  children: [
@@ -4699,18 +4990,25 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4699
4990
  height: chartHeight,
4700
4991
  children: (0, _wordpress_i18n.__)("All series are hidden. Click legend items to show data.", "jetpack-charts")
4701
4992
  }) : null,
4993
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComparisonBars, {
4994
+ comparisonEntries,
4995
+ primaryKeys,
4996
+ groupPadding,
4997
+ horizontal,
4998
+ xAccessor: chartOptions.accessors.xAccessor,
4999
+ yAccessor: chartOptions.accessors.yAccessor,
5000
+ getElementStyles,
5001
+ resolveFill: resolveComparisonFill
5002
+ }),
4702
5003
  /* @__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
- })
5004
+ padding: groupPadding,
5005
+ children: primaryEntries.map(({ series: seriesData, index }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.BarSeries, {
5006
+ dataKey: seriesData?.label,
5007
+ data: seriesData.data,
5008
+ yAccessor: chartOptions.accessors.yAccessor,
5009
+ xAccessor: chartOptions.accessors.xAccessor,
5010
+ colorAccessor: getBarBackground(index)
5011
+ }, seriesData?.label))
4714
5012
  }),
4715
5013
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.x }),
4716
5014
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_visx_xychart.Axis, { ...chartOptions.axis.y }),
@@ -4722,7 +5020,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4722
5020
  selectedIndex,
4723
5021
  tooltipRef,
4724
5022
  keyboardFocusedClassName: bar_chart_module_default["bar-chart__tooltip--keyboard-focused"],
4725
- series: data,
5023
+ series: primarySeries,
4726
5024
  mode: "individual"
4727
5025
  })
4728
5026
  ]
@@ -5323,9 +5621,7 @@ const DEFAULT_BACKGROUND_COLOR = "#ffffff";
5323
5621
  */
5324
5622
  const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", renderPlaceholder }) => {
5325
5623
  const { getElementStyles, theme: { geoChart: { featureFillColor }, backgroundColor } } = useGlobalChartsContext();
5326
- const loadingPlaceholder = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
5327
- align: "center",
5328
- justify: "center",
5624
+ const loadingPlaceholder = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
5329
5625
  className: (0, clsx.default)("geo-chart", geo_chart_module_default.container, className),
5330
5626
  style: {
5331
5627
  width,
@@ -5383,9 +5679,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
5383
5679
  defaultFillColorHex,
5384
5680
  sanitizedData.hasHtmlTooltips
5385
5681
  ]);
5386
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
5387
- align: "center",
5388
- justify: "center",
5682
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
5389
5683
  className: (0, clsx.default)("geo-chart", geo_chart_module_default.container, className),
5390
5684
  style: {
5391
5685
  width,
@@ -8432,7 +8726,6 @@ function RadialWipeAnimation({ id, radius, innerRadius = 0, durationMs = 1e3, wi
8432
8726
  //#region src/charts/pie-chart/pie-chart.module.scss
8433
8727
  var pie_chart_module_default = {
8434
8728
  "pie-chart": "a8ccharts-gnszbG-pie-chart",
8435
- "pie-chart__centering": "a8ccharts-gnszbG-pie-chart__centering",
8436
8729
  "pie-chart--responsive": "a8ccharts-gnszbG-pie-chart--responsive"
8437
8730
  };
8438
8731
  //#endregion
@@ -8597,11 +8890,8 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
8597
8890
  const innerRadius = thickness === 0 ? 0 : outerRadius * (1 - thickness);
8598
8891
  const maxCornerRadius = (outerRadius - innerRadius) / 2;
8599
8892
  const cornerRadius = cornerScale ? Math.min(cornerScale * outerRadius, maxCornerRadius) : 0;
8600
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
8893
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
8601
8894
  ref: containerRef,
8602
- align: "center",
8603
- justify: "center",
8604
- className: pie_chart_module_default["pie-chart__centering"],
8605
8895
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
8606
8896
  viewBox: `0 0 ${width} ${height}`,
8607
8897
  preserveAspectRatio: "xMidYMid meet",
@@ -8714,7 +9004,6 @@ var pie_semi_circle_chart_module_default = {
8714
9004
  "label": "a8ccharts-YtTOxW-label",
8715
9005
  "note": "a8ccharts-YtTOxW-note",
8716
9006
  "pie-semi-circle-chart": "a8ccharts-YtTOxW-pie-semi-circle-chart",
8717
- "pie-semi-circle-chart__centering": "a8ccharts-YtTOxW-pie-semi-circle-chart__centering",
8718
9007
  "pie-semi-circle-chart--responsive": "a8ccharts-YtTOxW-pie-semi-circle-chart--responsive"
8719
9008
  };
8720
9009
  //#endregion
@@ -8894,11 +9183,8 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
8894
9183
  const height = width / 2;
8895
9184
  const radius = height;
8896
9185
  const innerRadius = radius * (1 - thickness);
8897
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Stack, {
9186
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Center, {
8898
9187
  ref: containerRef,
8899
- align: "center",
8900
- justify: "center",
8901
- className: pie_semi_circle_chart_module_default["pie-semi-circle-chart__centering"],
8902
9188
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
8903
9189
  width,
8904
9190
  height,