@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/dist/index.js CHANGED
@@ -262,6 +262,20 @@ function getSeriesLineStyles(seriesData, index, providerTheme) {
262
262
  return seriesData.options?.seriesLineStyle ?? themeSemanticLineStyle ?? themeSeriesLineStyle ?? {};
263
263
  }
264
264
  /**
265
+ * Utility to get consolidated bar styles for a series by semantic type.
266
+ * Mirrors getSeriesLineStyles: a series with `options.type` (e.g. 'comparison')
267
+ * resolves to `theme.barChart.barStyles[ type ]`.
268
+ *
269
+ * @param {SeriesData} seriesData - The series data containing styling options
270
+ * @param {number} index - The index of the series in the data array
271
+ * @param {ChartTheme} providerTheme - The chart theme configuration
272
+ * @return {BarStyles} The consolidated bar styles for the series
273
+ */
274
+ function getSeriesBarStyles(seriesData, index, providerTheme) {
275
+ const type = seriesData.options?.type;
276
+ return (type && providerTheme?.barChart?.barStyles?.[type]) ?? {};
277
+ }
278
+ /**
265
279
  * Utility function to get shape styles for a legend item
266
280
  *
267
281
  * @param {SeriesData} series - The series data containing styling options
@@ -273,13 +287,17 @@ function getSeriesLineStyles(seriesData, index, providerTheme) {
273
287
  function getItemShapeStyles(series, index, theme, legendShape) {
274
288
  const seriesShapeStyles = series.options?.legendShapeStyle ?? {};
275
289
  const lineStyles = legendShape === "line" ? getSeriesLineStyles(series, index, theme) : {};
290
+ const barOpacity = legendShape !== "line" ? getSeriesBarStyles(series, index, theme).opacity : void 0;
291
+ const barShapeStyles = barOpacity !== void 0 ? { opacity: barOpacity } : {};
276
292
  const themeShapeStyles = theme.legend?.shapeStyles?.[index];
277
- const itemShapeStyles = {
293
+ const explicitStyles = {
278
294
  ...seriesShapeStyles,
279
295
  ...lineStyles
280
296
  };
281
- if (Object.values(itemShapeStyles).some((value) => value !== void 0 && value !== null && value !== "")) return itemShapeStyles;
282
- return themeShapeStyles ?? {};
297
+ return {
298
+ ...Object.values(explicitStyles).some((value) => value !== void 0 && value !== null && value !== "") ? explicitStyles : themeShapeStyles ?? {},
299
+ ...barShapeStyles
300
+ };
283
301
  }
284
302
  //#endregion
285
303
  //#region src/utils/is-safari.ts
@@ -725,6 +743,10 @@ const defaultTheme = {
725
743
  strokeDasharray: "4 4",
726
744
  strokeLinecap: "square"
727
745
  } } },
746
+ barChart: { barStyles: { comparison: {
747
+ widthFactor: 1.5,
748
+ opacity: .5
749
+ } } },
728
750
  sparkline: {
729
751
  margin: {
730
752
  top: 2,
@@ -831,6 +853,7 @@ const GlobalChartsProvider = ({ children, theme }) => {
831
853
  overrideColor: overrideColor || isSeriesData && data?.options?.stroke || isPointPercentageData && data?.color
832
854
  }),
833
855
  lineStyles: isSeriesData ? getSeriesLineStyles(data, index, providerTheme) : {},
856
+ barStyles: isSeriesData ? getSeriesBarStyles(data, index, providerTheme) : {},
834
857
  glyph: providerTheme.glyphs?.[index],
835
858
  shapeStyles: isSeriesData ? getItemShapeStyles(data, index, providerTheme, legendShape) : {}
836
859
  };
@@ -2635,6 +2658,32 @@ const DefaultGlyph = (props) => {
2635
2658
  });
2636
2659
  };
2637
2660
  //#endregion
2661
+ //#region src/charts/private/center/center.module.scss
2662
+ var center_module_default = { "center": "a8ccharts-w3qxlG-center" };
2663
+ //#endregion
2664
+ //#region src/charts/private/center/center.tsx
2665
+ /**
2666
+ * Centers its children on both axes and fills its parent.
2667
+ *
2668
+ * A thin wrapper around `Stack` with `align="center"` and `justify="center"`
2669
+ * defaults (both overridable) plus `width: 100%; height: 100%`. Reads more
2670
+ * honestly than a `Stack` with both axes centered, and lets call sites drop
2671
+ * ad-hoc `*__centering` classes. Forwards its ref and spreads remaining props
2672
+ * onto the underlying `Stack`.
2673
+ *
2674
+ * @param props - Stack props; `align`/`justify` default to `"center"`.
2675
+ * @param ref - Forwarded to the underlying element.
2676
+ * @return The centered layout element.
2677
+ */
2678
+ const Center = forwardRef(({ align = "center", justify = "center", className, ...props }, ref) => /* @__PURE__ */ jsx(Stack, {
2679
+ ref,
2680
+ align,
2681
+ justify,
2682
+ className: clsx(center_module_default.center, className),
2683
+ ...props
2684
+ }));
2685
+ Center.displayName = "Center";
2686
+ //#endregion
2638
2687
  //#region src/charts/private/svg-empty-state/svg-empty-state.module.scss
2639
2688
  var svg_empty_state_module_default = { "svg-empty-state": "a8ccharts-udGPVq-svg-empty-state" };
2640
2689
  //#endregion
@@ -2659,9 +2708,7 @@ const SvgEmptyState = ({ x, y, width, height, children }) => {
2659
2708
  y: y - height / 2,
2660
2709
  width,
2661
2710
  height,
2662
- children: /* @__PURE__ */ jsx(Stack, {
2663
- align: "center",
2664
- justify: "center",
2711
+ children: /* @__PURE__ */ jsx(Center, {
2665
2712
  className: svg_empty_state_module_default["svg-empty-state"],
2666
2713
  children
2667
2714
  })
@@ -4341,6 +4388,10 @@ const TruncatedXTickComponent = createTruncatedTickComponent("x");
4341
4388
  const TruncatedYTickComponent = createTruncatedTickComponent("y");
4342
4389
  //#endregion
4343
4390
  //#region src/charts/bar-chart/private/use-bar-chart-options.ts
4391
+ /** Outer padding of the category band scale (space at the chart edges). */
4392
+ const BASE_BAND_PADDING = .2;
4393
+ /** Inner padding of the category band scale (the base gap between ticks). */
4394
+ const BASE_BAND_PADDING_INNER = .1;
4344
4395
  const formatDateTick = (timestamp) => {
4345
4396
  return new Date(timestamp).toLocaleDateString(void 0, {
4346
4397
  month: "short",
@@ -4368,8 +4419,8 @@ function useBarChartOptions(data, horizontal, options = {}) {
4368
4419
  const defaultOptions = useMemo(() => {
4369
4420
  const bandScale = {
4370
4421
  type: "band",
4371
- padding: .2,
4372
- paddingInner: .1
4422
+ padding: BASE_BAND_PADDING,
4423
+ paddingInner: BASE_BAND_PADDING_INNER
4373
4424
  };
4374
4425
  const linearScale = {
4375
4426
  type: "linear",
@@ -4408,13 +4459,29 @@ function useBarChartOptions(data, horizontal, options = {}) {
4408
4459
  }, [data]);
4409
4460
  return useMemo(() => {
4410
4461
  const { xTickFormat, yTickFormat, tooltipLabelFormatter: defaultTooltipLabelFormatter, xAccessor, yAccessor, gridVisibility, xScale: baseXScale, yScale: baseYScale } = defaultOptions[horizontal ? "horizontal" : "vertical"];
4462
+ let valueScaleDomainOverride = {};
4463
+ if (data.some((s) => s.options?.type === "comparison")) {
4464
+ if (!(!horizontal ? options.yScale?.domain : options.xScale?.domain)) {
4465
+ const allValues = [];
4466
+ data.forEach((series) => {
4467
+ series.data.forEach((d) => {
4468
+ const enhanced = d;
4469
+ const v = enhanced.visualValue !== void 0 ? enhanced.visualValue : d.value;
4470
+ if (typeof v === "number" && Number.isFinite(v)) allValues.push(v);
4471
+ });
4472
+ });
4473
+ if (allValues.length > 0) valueScaleDomainOverride = { domain: [Math.min(...allValues), Math.max(...allValues)] };
4474
+ }
4475
+ }
4411
4476
  const xScale = {
4412
4477
  ...baseXScale,
4413
- ...options.xScale || {}
4478
+ ...options.xScale || {},
4479
+ ...horizontal ? valueScaleDomainOverride : {}
4414
4480
  };
4415
4481
  const yScale = {
4416
4482
  ...baseYScale,
4417
- ...options.yScale || {}
4483
+ ...options.yScale || {},
4484
+ ...!horizontal ? valueScaleDomainOverride : {}
4418
4485
  };
4419
4486
  const providedToolTipLabelFormatter = horizontal ? options.axis?.y?.tickFormat : options.axis?.x?.tickFormat;
4420
4487
  const { labelOverflow: xLabelOverflow, ...xAxisOptions } = options.axis?.x || {};
@@ -4449,10 +4516,143 @@ function useBarChartOptions(data, horizontal, options = {}) {
4449
4516
  }, [
4450
4517
  defaultOptions,
4451
4518
  options,
4452
- horizontal
4519
+ horizontal,
4520
+ data
4453
4521
  ]);
4454
4522
  }
4455
4523
  //#endregion
4524
+ //#region src/charts/bar-chart/private/comparison-bars-geometry.ts
4525
+ /**
4526
+ * Output position of a value scale's baseline: zero if in-domain, else the
4527
+ * nearest range edge. Mirrors visx's getScaleBaseline so comparison shadows
4528
+ * sit on the same baseline as primary bars.
4529
+ *
4530
+ * @param {ValueScale} scale - The continuous value scale.
4531
+ * @return {number} The baseline output position in pixels.
4532
+ */
4533
+ function getValueScaleBaseline(scale) {
4534
+ const [a, b] = scale.range().map((r) => Number(r) || 0);
4535
+ const isDescending = b < a;
4536
+ const maybeZero = scale(0);
4537
+ const [minOutput, maxOutput] = isDescending ? [b, a] : [a, b];
4538
+ if (isDescending) return Number.isFinite(maybeZero) ? Math.min(Math.max(minOutput, maybeZero), maxOutput) : maxOutput;
4539
+ return Number.isFinite(maybeZero) ? Math.min(Math.max(maybeZero, minOutput), maxOutput) : minOutput;
4540
+ }
4541
+ /**
4542
+ * Compute the rect for a comparison "shadow" bar, centered on the paired
4543
+ * primary bar slot and scaled by `widthFactor`.
4544
+ *
4545
+ * @param {object} params - Geometry inputs.
4546
+ * @param {boolean} params.horizontal - True for a horizontal bar chart, false for vertical.
4547
+ * @param {number} params.bandPosition - bandScale(category): start px of the category band.
4548
+ * @param {number} params.slotOffset - groupScale(primaryKey): offset of the primary slot within the band.
4549
+ * @param {number} params.slotThickness - groupScale.bandwidth(): primary bar thickness in px.
4550
+ * @param {number} params.valuePosition - valueScale(value): output px for the bar's data value.
4551
+ * @param {number} params.baseline - getValueScaleBaseline(valueScale): zero-line output px.
4552
+ * @param {number} params.widthFactor - Shadow thickness multiplier, e.g. 1.5 for 150% width.
4553
+ * @return {ComparisonRect} The {x, y, width, height} of the shadow rect.
4554
+ */
4555
+ function computeComparisonRect(params) {
4556
+ const { horizontal, bandPosition, slotOffset, slotThickness, valuePosition, baseline, widthFactor } = params;
4557
+ const slotStart = bandPosition + slotOffset;
4558
+ const shadowThickness = slotThickness * widthFactor;
4559
+ const shadowStart = slotStart + slotThickness / 2 - shadowThickness / 2;
4560
+ const valueStart = Math.min(valuePosition, baseline);
4561
+ const valueLength = Math.abs(baseline - valuePosition);
4562
+ if (horizontal) return {
4563
+ x: valueStart,
4564
+ y: shadowStart,
4565
+ width: valueLength,
4566
+ height: shadowThickness
4567
+ };
4568
+ return {
4569
+ x: shadowStart,
4570
+ y: valueStart,
4571
+ width: shadowThickness,
4572
+ height: valueLength
4573
+ };
4574
+ }
4575
+ /**
4576
+ * Fraction of each per-series step left as a gap between bars within a single tick.
4577
+ * Larger = more space between adjacent series; the shadow spans `1 - COMPARISON_INNER_GAP` of the step.
4578
+ */
4579
+ const COMPARISON_INNER_GAP = .1;
4580
+ /**
4581
+ * Upper clamp on the computed group padding, so bars can never collapse to zero width
4582
+ * even at very large `widthFactor` values.
4583
+ */
4584
+ const MAX_GROUP_PADDING = .9;
4585
+ /**
4586
+ * Factor applied to the category band's `paddingInner` in comparison mode to tighten the
4587
+ * gap between ticks. `0.75` = a 25% reduction of the tick-gap padding.
4588
+ */
4589
+ const COMPARISON_TICK_GAP_FACTOR = .75;
4590
+ //#endregion
4591
+ //#region src/charts/bar-chart/private/comparison-bars.tsx
4592
+ const ComparisonBars = ({ comparisonEntries, primaryKeys, groupPadding, horizontal, xAccessor, yAccessor, getElementStyles, resolveFill }) => {
4593
+ const context = useContext(DataContext);
4594
+ const xScale = context?.xScale;
4595
+ const yScale = context?.yScale;
4596
+ if (!xScale || !yScale || primaryKeys.length === 0) return null;
4597
+ const bandScale = horizontal ? yScale : xScale;
4598
+ const valueScale = horizontal ? xScale : yScale;
4599
+ const bandwidth = bandScale.bandwidth ? bandScale.bandwidth() : 0;
4600
+ if (!bandwidth) return null;
4601
+ const groupScale = scaleBand({
4602
+ domain: primaryKeys,
4603
+ range: [0, bandwidth],
4604
+ padding: groupPadding
4605
+ });
4606
+ const slotThickness = groupScale.bandwidth();
4607
+ const baseline = getValueScaleBaseline(valueScale);
4608
+ const bandAccessor = horizontal ? yAccessor : xAccessor;
4609
+ const valueAccessor = horizontal ? xAccessor : yAccessor;
4610
+ const rects = [];
4611
+ comparisonEntries.forEach((entry) => {
4612
+ const { series, index, primaryKey } = entry;
4613
+ const slotOffset = groupScale(primaryKey);
4614
+ if (slotOffset == null || !Number.isFinite(slotOffset)) return;
4615
+ const { barStyles } = getElementStyles({
4616
+ data: series,
4617
+ index
4618
+ });
4619
+ const opacity = barStyles?.opacity ?? .5;
4620
+ const widthFactor = barStyles?.widthFactor ?? 1.5;
4621
+ const fill = resolveFill(entry);
4622
+ series.data.forEach((datum, i) => {
4623
+ const bandPosition = Number(bandScale(bandAccessor(datum)));
4624
+ const valuePosition = Number(valueScale(Number(valueAccessor(datum))));
4625
+ if (!Number.isFinite(bandPosition) || !Number.isFinite(valuePosition)) {
4626
+ 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.`);
4627
+ return;
4628
+ }
4629
+ const rect = computeComparisonRect({
4630
+ horizontal,
4631
+ bandPosition,
4632
+ slotOffset,
4633
+ slotThickness,
4634
+ valuePosition,
4635
+ baseline,
4636
+ widthFactor
4637
+ });
4638
+ rects.push(/* @__PURE__ */ jsx("rect", {
4639
+ x: rect.x,
4640
+ y: rect.y,
4641
+ width: rect.width,
4642
+ height: rect.height,
4643
+ fill,
4644
+ opacity
4645
+ }, `${index}-${i}`));
4646
+ });
4647
+ });
4648
+ if (rects.length === 0) return null;
4649
+ return /* @__PURE__ */ jsx("g", {
4650
+ className: "bar-chart__comparison-bars",
4651
+ pointerEvents: "none",
4652
+ children: rects
4653
+ });
4654
+ };
4655
+ //#endregion
4456
4656
  //#region src/charts/bar-chart/bar-chart.tsx
4457
4657
  const validateData$2 = (data) => {
4458
4658
  if (!data?.length) return "No data available";
@@ -4481,13 +4681,14 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4481
4681
  }, [height]);
4482
4682
  const [selectedIndex, setSelectedIndex] = useState(void 0);
4483
4683
  const [isNavigating, setIsNavigating] = useState(false);
4684
+ const primarySeriesForNav = dataWithVisibleZeros.filter((s) => s.options?.type !== "comparison");
4484
4685
  const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
4485
4686
  selectedIndex,
4486
4687
  setSelectedIndex,
4487
4688
  isNavigating,
4488
4689
  setIsNavigating,
4489
4690
  chartRef,
4490
- totalPoints: Math.max(0, ...data.map((series) => series.data?.length || 0)) * data.length
4691
+ totalPoints: Math.max(0, ...primarySeriesForNav.map((s) => s.data?.length || 0)) * primarySeriesForNav.length
4491
4692
  });
4492
4693
  const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4493
4694
  const seriesWithVisibility = useMemo(() => {
@@ -4510,6 +4711,69 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4510
4711
  const allSeriesHidden = useMemo(() => {
4511
4712
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
4512
4713
  }, [seriesWithVisibility]);
4714
+ const primaryEntries = useMemo(() => seriesWithVisibility.filter(({ isVisible, series }) => isVisible && series.options?.type !== "comparison"), [seriesWithVisibility]);
4715
+ const primaryKeys = useMemo(() => primaryEntries.map(({ series }) => series.label), [primaryEntries]);
4716
+ const comparisonEntries = useMemo(() => {
4717
+ const primaryByGroup = new Map(primaryEntries.map(({ series, index }) => [series.group, {
4718
+ label: series.label,
4719
+ index
4720
+ }]));
4721
+ const entries = [];
4722
+ seriesWithVisibility.forEach(({ series, index, isVisible }) => {
4723
+ if (!isVisible || series.options?.type !== "comparison") return;
4724
+ const primary = primaryByGroup.get(series.group) ?? (primaryEntries.length === 1 ? {
4725
+ label: primaryEntries[0].series.label,
4726
+ index: primaryEntries[0].index
4727
+ } : void 0);
4728
+ if (!primary || !primaryKeys.includes(primary.label)) return;
4729
+ entries.push({
4730
+ series,
4731
+ index,
4732
+ primaryKey: primary.label,
4733
+ primaryIndex: primary.index
4734
+ });
4735
+ });
4736
+ return entries;
4737
+ }, [
4738
+ seriesWithVisibility,
4739
+ primaryEntries,
4740
+ primaryKeys
4741
+ ]);
4742
+ const comparisonWidthFactor = useMemo(() => {
4743
+ if (comparisonEntries.length === 0) return void 0;
4744
+ return getElementStyles({
4745
+ data: comparisonEntries[0].series,
4746
+ index: comparisonEntries[0].index
4747
+ }).barStyles?.widthFactor ?? 1.5;
4748
+ }, [comparisonEntries, getElementStyles]);
4749
+ const groupPadding = useMemo(() => {
4750
+ const basePadding = chartOptions.barGroup.padding;
4751
+ if (!comparisonWidthFactor || comparisonWidthFactor <= 1) return basePadding;
4752
+ const p = 1 - (1 - COMPARISON_INNER_GAP) / comparisonWidthFactor;
4753
+ return Math.min(Math.max(p, basePadding), MAX_GROUP_PADDING);
4754
+ }, [chartOptions.barGroup.padding, comparisonWidthFactor]);
4755
+ const { xScale, yScale } = useMemo(() => {
4756
+ if (comparisonEntries.length === 0) return {
4757
+ xScale: chartOptions.xScale,
4758
+ yScale: chartOptions.yScale
4759
+ };
4760
+ const tighten = (scale) => ({
4761
+ ...scale,
4762
+ paddingInner: (scale.paddingInner ?? .1) * COMPARISON_TICK_GAP_FACTOR
4763
+ });
4764
+ return horizontal ? {
4765
+ xScale: chartOptions.xScale,
4766
+ yScale: tighten(chartOptions.yScale)
4767
+ } : {
4768
+ xScale: tighten(chartOptions.xScale),
4769
+ yScale: chartOptions.yScale
4770
+ };
4771
+ }, [
4772
+ comparisonEntries.length,
4773
+ chartOptions.xScale,
4774
+ chartOptions.yScale,
4775
+ horizontal
4776
+ ]);
4513
4777
  const getBarBackground = useCallback((index) => () => withPatterns ? `url(#${getPatternId(chartId, index)})` : getElementStyles({
4514
4778
  data: dataSorted[index],
4515
4779
  index
@@ -4519,26 +4783,70 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4519
4783
  dataSorted,
4520
4784
  chartId
4521
4785
  ]);
4786
+ const resolveComparisonFill = useCallback((entry) => withPatterns ? `url(#${getPatternId(chartId, entry.primaryIndex)})` : getElementStyles({
4787
+ data: entry.series,
4788
+ index: entry.index
4789
+ }).color, [
4790
+ withPatterns,
4791
+ chartId,
4792
+ getElementStyles
4793
+ ]);
4522
4794
  const renderDefaultTooltip = useCallback(({ tooltipData }) => {
4523
4795
  const nearestDatum = tooltipData?.nearestDatum?.datum;
4524
4796
  if (!nearestDatum) return null;
4797
+ const primaryKey = tooltipData?.nearestDatum?.key;
4798
+ const categoryLabel = chartOptions.tooltip.labelFormatter(nearestDatum.label || (nearestDatum.date ? nearestDatum.date.getTime() : 0), 0, []);
4799
+ const comparisonEntry = comparisonEntries.find((entry) => entry.primaryKey === primaryKey);
4800
+ const comparisonDatum = comparisonEntry?.series.data.find((point) => {
4801
+ const p = point;
4802
+ return nearestDatum.label != null ? p.label === nearestDatum.label : !!nearestDatum.date && !!p.date && p.date.getTime() === nearestDatum.date.getTime();
4803
+ });
4804
+ if (comparisonEntry && comparisonDatum && comparisonDatum.value != null) return /* @__PURE__ */ jsxs("div", {
4805
+ className: bar_chart_module_default["bar-chart__tooltip"],
4806
+ children: [
4807
+ /* @__PURE__ */ jsx("div", {
4808
+ className: bar_chart_module_default["bar-chart__tooltip-header"],
4809
+ children: categoryLabel
4810
+ }),
4811
+ /* @__PURE__ */ jsxs("div", {
4812
+ className: bar_chart_module_default["bar-chart__tooltip-row"],
4813
+ children: [/* @__PURE__ */ jsxs("span", {
4814
+ className: bar_chart_module_default["bar-chart__tooltip-label"],
4815
+ children: [primaryKey, ":"]
4816
+ }), /* @__PURE__ */ jsx("span", {
4817
+ className: bar_chart_module_default["bar-chart__tooltip-value"],
4818
+ children: formatNumber(nearestDatum.value)
4819
+ })]
4820
+ }),
4821
+ /* @__PURE__ */ jsxs("div", {
4822
+ className: bar_chart_module_default["bar-chart__tooltip-row"],
4823
+ children: [/* @__PURE__ */ jsxs("span", {
4824
+ className: bar_chart_module_default["bar-chart__tooltip-label"],
4825
+ children: [comparisonEntry.series.label, ":"]
4826
+ }), /* @__PURE__ */ jsx("span", {
4827
+ className: bar_chart_module_default["bar-chart__tooltip-value"],
4828
+ children: formatNumber(comparisonDatum.value)
4829
+ })]
4830
+ })
4831
+ ]
4832
+ });
4525
4833
  return /* @__PURE__ */ jsxs("div", {
4526
4834
  className: bar_chart_module_default["bar-chart__tooltip"],
4527
4835
  children: [/* @__PURE__ */ jsx("div", {
4528
4836
  className: bar_chart_module_default["bar-chart__tooltip-header"],
4529
- children: tooltipData?.nearestDatum?.key
4837
+ children: primaryKey
4530
4838
  }), /* @__PURE__ */ jsxs("div", {
4531
4839
  className: bar_chart_module_default["bar-chart__tooltip-row"],
4532
4840
  children: [/* @__PURE__ */ jsxs("span", {
4533
4841
  className: bar_chart_module_default["bar-chart__tooltip-label"],
4534
- children: [chartOptions.tooltip.labelFormatter(nearestDatum.label || (nearestDatum.date ? nearestDatum.date.getTime() : 0), 0, []), ":"]
4842
+ children: [categoryLabel, ":"]
4535
4843
  }), /* @__PURE__ */ jsx("span", {
4536
4844
  className: bar_chart_module_default["bar-chart__tooltip-value"],
4537
4845
  children: formatNumber(nearestDatum.value)
4538
4846
  })]
4539
4847
  })]
4540
4848
  });
4541
- }, [chartOptions.tooltip]);
4849
+ }, [chartOptions.tooltip, comparisonEntries]);
4542
4850
  const renderPattern = useCallback((index, color) => {
4543
4851
  const patternType = index % 4;
4544
4852
  const id = getPatternId(chartId, index);
@@ -4575,8 +4883,10 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4575
4883
  }
4576
4884
  }, [chartId]);
4577
4885
  const createPatternBorderStyle = useCallback((index, color) => {
4886
+ const patternId = getPatternId(chartId, index);
4578
4887
  return `
4579
- .visx-bar[fill="url(#${getPatternId(chartId, index)})"] {
4888
+ .visx-bar[fill="url(#${patternId})"],
4889
+ .bar-chart__comparison-bars rect[fill="url(#${patternId})"] {
4580
4890
  stroke: ${color};
4581
4891
  stroke-width: 1;
4582
4892
  }
@@ -4584,11 +4894,13 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4584
4894
  }, [chartId]);
4585
4895
  const createKeyboardHighlightStyle = useCallback(() => {
4586
4896
  if (selectedIndex === void 0) return "";
4587
- const maxDataPoints = Math.max(...data.map((s) => s.data.length));
4588
- const dataPointIndex = Math.floor(selectedIndex / data.length);
4589
- const seriesIndex = selectedIndex % data.length;
4590
- if (dataPointIndex >= maxDataPoints || seriesIndex >= data.length) return "";
4591
- if (dataPointIndex >= data[seriesIndex].data.length) return "";
4897
+ const primaryCount = primaryEntries.length;
4898
+ const maxDataPoints = Math.max(...primaryEntries.map((e) => e.series.data.length));
4899
+ const dataPointIndex = Math.floor(selectedIndex / primaryCount);
4900
+ const seriesIndex = selectedIndex % primaryCount;
4901
+ if (dataPointIndex >= maxDataPoints || seriesIndex >= primaryCount) return "";
4902
+ const seriesData = primaryEntries[seriesIndex]?.series;
4903
+ if (!seriesData || dataPointIndex >= seriesData.data.length) return "";
4592
4904
  return `
4593
4905
  .bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${seriesIndex * maxDataPoints + dataPointIndex + 1}) {
4594
4906
  stroke: #005fcc;
@@ -4597,7 +4909,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4597
4909
  `;
4598
4910
  }, [
4599
4911
  selectedIndex,
4600
- data,
4912
+ primaryEntries,
4601
4913
  chartId
4602
4914
  ]);
4603
4915
  const error = validateData$2(dataSorted);
@@ -4670,8 +4982,8 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4670
4982
  ...defaultMargin,
4671
4983
  ...margin
4672
4984
  },
4673
- xScale: chartOptions.xScale,
4674
- yScale: chartOptions.yScale,
4985
+ xScale,
4986
+ yScale,
4675
4987
  horizontal,
4676
4988
  pointerEventsDataKey: "nearest",
4677
4989
  children: [
@@ -4695,18 +5007,25 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4695
5007
  height: chartHeight,
4696
5008
  children: __("All series are hidden. Click legend items to show data.", "jetpack-charts")
4697
5009
  }) : null,
5010
+ /* @__PURE__ */ jsx(ComparisonBars, {
5011
+ comparisonEntries,
5012
+ primaryKeys,
5013
+ groupPadding,
5014
+ horizontal,
5015
+ xAccessor: chartOptions.accessors.xAccessor,
5016
+ yAccessor: chartOptions.accessors.yAccessor,
5017
+ getElementStyles,
5018
+ resolveFill: resolveComparisonFill
5019
+ }),
4698
5020
  /* @__PURE__ */ jsx(BarGroup, {
4699
- padding: chartOptions.barGroup.padding,
4700
- children: seriesWithVisibility.map(({ series: seriesData, index, isVisible }) => {
4701
- if (!isVisible) return null;
4702
- return /* @__PURE__ */ jsx(BarSeries, {
4703
- dataKey: seriesData?.label,
4704
- data: seriesData.data,
4705
- yAccessor: chartOptions.accessors.yAccessor,
4706
- xAccessor: chartOptions.accessors.xAccessor,
4707
- colorAccessor: getBarBackground(index)
4708
- }, seriesData?.label);
4709
- })
5021
+ padding: groupPadding,
5022
+ children: primaryEntries.map(({ series: seriesData, index }) => /* @__PURE__ */ jsx(BarSeries, {
5023
+ dataKey: seriesData?.label,
5024
+ data: seriesData.data,
5025
+ yAccessor: chartOptions.accessors.yAccessor,
5026
+ xAccessor: chartOptions.accessors.xAccessor,
5027
+ colorAccessor: getBarBackground(index)
5028
+ }, seriesData?.label))
4710
5029
  }),
4711
5030
  /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.x }),
4712
5031
  /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.y }),
@@ -5319,9 +5638,7 @@ const DEFAULT_BACKGROUND_COLOR = "#ffffff";
5319
5638
  */
5320
5639
  const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", renderPlaceholder }) => {
5321
5640
  const { getElementStyles, theme: { geoChart: { featureFillColor }, backgroundColor } } = useGlobalChartsContext();
5322
- const loadingPlaceholder = /* @__PURE__ */ jsx(Stack, {
5323
- align: "center",
5324
- justify: "center",
5641
+ const loadingPlaceholder = /* @__PURE__ */ jsx(Center, {
5325
5642
  className: clsx("geo-chart", geo_chart_module_default.container, className),
5326
5643
  style: {
5327
5644
  width,
@@ -5379,9 +5696,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
5379
5696
  defaultFillColorHex,
5380
5697
  sanitizedData.hasHtmlTooltips
5381
5698
  ]);
5382
- return /* @__PURE__ */ jsx(Stack, {
5383
- align: "center",
5384
- justify: "center",
5699
+ return /* @__PURE__ */ jsx(Center, {
5385
5700
  className: clsx("geo-chart", geo_chart_module_default.container, className),
5386
5701
  style: {
5387
5702
  width,
@@ -8053,7 +8368,6 @@ function RadialWipeAnimation({ id, radius, innerRadius = 0, durationMs = 1e3, wi
8053
8368
  //#region src/charts/pie-chart/pie-chart.module.scss
8054
8369
  var pie_chart_module_default = {
8055
8370
  "pie-chart": "a8ccharts-gnszbG-pie-chart",
8056
- "pie-chart__centering": "a8ccharts-gnszbG-pie-chart__centering",
8057
8371
  "pie-chart--responsive": "a8ccharts-gnszbG-pie-chart--responsive"
8058
8372
  };
8059
8373
  //#endregion
@@ -8218,11 +8532,8 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
8218
8532
  const innerRadius = thickness === 0 ? 0 : outerRadius * (1 - thickness);
8219
8533
  const maxCornerRadius = (outerRadius - innerRadius) / 2;
8220
8534
  const cornerRadius = cornerScale ? Math.min(cornerScale * outerRadius, maxCornerRadius) : 0;
8221
- return /* @__PURE__ */ jsx(Stack, {
8535
+ return /* @__PURE__ */ jsx(Center, {
8222
8536
  ref: containerRef,
8223
- align: "center",
8224
- justify: "center",
8225
- className: pie_chart_module_default["pie-chart__centering"],
8226
8537
  children: /* @__PURE__ */ jsxs("svg", {
8227
8538
  viewBox: `0 0 ${width} ${height}`,
8228
8539
  preserveAspectRatio: "xMidYMid meet",
@@ -8335,7 +8646,6 @@ var pie_semi_circle_chart_module_default = {
8335
8646
  "label": "a8ccharts-YtTOxW-label",
8336
8647
  "note": "a8ccharts-YtTOxW-note",
8337
8648
  "pie-semi-circle-chart": "a8ccharts-YtTOxW-pie-semi-circle-chart",
8338
- "pie-semi-circle-chart__centering": "a8ccharts-YtTOxW-pie-semi-circle-chart__centering",
8339
8649
  "pie-semi-circle-chart--responsive": "a8ccharts-YtTOxW-pie-semi-circle-chart--responsive"
8340
8650
  };
8341
8651
  //#endregion
@@ -8515,11 +8825,8 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
8515
8825
  const height = width / 2;
8516
8826
  const radius = height;
8517
8827
  const innerRadius = radius * (1 - thickness);
8518
- return /* @__PURE__ */ jsx(Stack, {
8828
+ return /* @__PURE__ */ jsx(Center, {
8519
8829
  ref: containerRef,
8520
- align: "center",
8521
- justify: "center",
8522
- className: pie_semi_circle_chart_module_default["pie-semi-circle-chart__centering"],
8523
8830
  children: /* @__PURE__ */ jsxs("svg", {
8524
8831
  width,
8525
8832
  height,