@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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getStringWidth as getStringWidth$1 } from "./visx/text/index.js";
2
2
  import { formatNumber, formatNumberCompact } from "@automattic/number-formatters";
3
3
  import { AnimatedAreaSeries, AnimatedAreaStack, AreaSeries, Axis, BarGroup, BarSeries, DataContext, Grid, Tooltip, TooltipContext, XYChart, buildChartTheme } from "@visx/xychart";
4
- import { __ } from "@wordpress/i18n";
4
+ import { __, sprintf } from "@wordpress/i18n";
5
5
  import clsx from "clsx";
6
6
  import * as React from "react";
7
7
  import { Children, Fragment, Fragment as Fragment$2, createContext, createContext as createContext$1, createElement, forwardRef, forwardRef as forwardRef$1, forwardRef as forwardRef$2, isValidElement, memo, useCallback, useCallback as useCallback$1, useContext, useContext as useContext$1, useEffect, useEffect as useEffect$1, useId, useImperativeHandle, useLayoutEffect, useMemo, useMemo as useMemo$1, useRef, useRef as useRef$1, useState, useState as useState$1 } from "react";
@@ -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(0, ...allValues), Math.max(0, ...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,144 @@ 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
+ "aria-hidden": "true",
4653
+ children: rects
4654
+ });
4655
+ };
4656
+ //#endregion
4456
4657
  //#region src/charts/bar-chart/bar-chart.tsx
4457
4658
  const validateData$2 = (data) => {
4458
4659
  if (!data?.length) return "No data available";
@@ -4460,6 +4661,10 @@ const validateData$2 = (data) => {
4460
4661
  return null;
4461
4662
  };
4462
4663
  const getPatternId = (chartId, index) => `bar-pattern-${chartId}-${index}`;
4664
+ const renderTooltipRow = (label, value) => /* @__PURE__ */ jsx("div", {
4665
+ className: bar_chart_module_default["bar-chart__tooltip-row"],
4666
+ children: sprintf(__("%1$s: %2$s", "jetpack-charts"), label, value)
4667
+ });
4463
4668
  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" }) => {
4464
4669
  const legendInteractive = legend.interactive ?? false;
4465
4670
  const horizontal = orientation === "horizontal";
@@ -4481,13 +4686,14 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4481
4686
  }, [height]);
4482
4687
  const [selectedIndex, setSelectedIndex] = useState(void 0);
4483
4688
  const [isNavigating, setIsNavigating] = useState(false);
4689
+ const primarySeriesForNav = dataWithVisibleZeros.filter((s) => s.options?.type !== "comparison");
4484
4690
  const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
4485
4691
  selectedIndex,
4486
4692
  setSelectedIndex,
4487
4693
  isNavigating,
4488
4694
  setIsNavigating,
4489
4695
  chartRef,
4490
- totalPoints: Math.max(0, ...data.map((series) => series.data?.length || 0)) * data.length
4696
+ totalPoints: Math.max(0, ...primarySeriesForNav.map((s) => s.data?.length || 0)) * primarySeriesForNav.length
4491
4697
  });
4492
4698
  const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4493
4699
  const seriesWithVisibility = useMemo(() => {
@@ -4510,6 +4716,70 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4510
4716
  const allSeriesHidden = useMemo(() => {
4511
4717
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
4512
4718
  }, [seriesWithVisibility]);
4719
+ const primaryEntries = useMemo(() => seriesWithVisibility.filter(({ isVisible, series }) => isVisible && series.options?.type !== "comparison"), [seriesWithVisibility]);
4720
+ const primaryKeys = useMemo(() => primaryEntries.map(({ series }) => series.label), [primaryEntries]);
4721
+ const primarySeries = useMemo(() => primaryEntries.map(({ series }) => series), [primaryEntries]);
4722
+ const comparisonEntries = useMemo(() => {
4723
+ const primaryByGroup = new Map(primaryEntries.map(({ series, index }) => [series.group, {
4724
+ label: series.label,
4725
+ index
4726
+ }]));
4727
+ const entries = [];
4728
+ seriesWithVisibility.forEach(({ series, index, isVisible }) => {
4729
+ if (!isVisible || series.options?.type !== "comparison") return;
4730
+ const primary = primaryByGroup.get(series.group) ?? (primaryEntries.length === 1 ? {
4731
+ label: primaryEntries[0].series.label,
4732
+ index: primaryEntries[0].index
4733
+ } : void 0);
4734
+ if (!primary || !primaryKeys.includes(primary.label)) return;
4735
+ entries.push({
4736
+ series,
4737
+ index,
4738
+ primaryKey: primary.label,
4739
+ primaryIndex: primary.index
4740
+ });
4741
+ });
4742
+ return entries;
4743
+ }, [
4744
+ seriesWithVisibility,
4745
+ primaryEntries,
4746
+ primaryKeys
4747
+ ]);
4748
+ const comparisonWidthFactor = useMemo(() => {
4749
+ if (comparisonEntries.length === 0) return void 0;
4750
+ return getElementStyles({
4751
+ data: comparisonEntries[0].series,
4752
+ index: comparisonEntries[0].index
4753
+ }).barStyles?.widthFactor ?? 1.5;
4754
+ }, [comparisonEntries, getElementStyles]);
4755
+ const groupPadding = useMemo(() => {
4756
+ const basePadding = chartOptions.barGroup.padding;
4757
+ if (!comparisonWidthFactor || comparisonWidthFactor <= 1) return basePadding;
4758
+ const p = 1 - (1 - COMPARISON_INNER_GAP) / comparisonWidthFactor;
4759
+ return Math.min(Math.max(p, basePadding), MAX_GROUP_PADDING);
4760
+ }, [chartOptions.barGroup.padding, comparisonWidthFactor]);
4761
+ const { xScale, yScale } = useMemo(() => {
4762
+ if (comparisonEntries.length === 0) return {
4763
+ xScale: chartOptions.xScale,
4764
+ yScale: chartOptions.yScale
4765
+ };
4766
+ const tighten = (scale) => ({
4767
+ ...scale,
4768
+ paddingInner: (scale.paddingInner ?? .1) * COMPARISON_TICK_GAP_FACTOR
4769
+ });
4770
+ return horizontal ? {
4771
+ xScale: chartOptions.xScale,
4772
+ yScale: tighten(chartOptions.yScale)
4773
+ } : {
4774
+ xScale: tighten(chartOptions.xScale),
4775
+ yScale: chartOptions.yScale
4776
+ };
4777
+ }, [
4778
+ comparisonEntries.length,
4779
+ chartOptions.xScale,
4780
+ chartOptions.yScale,
4781
+ horizontal
4782
+ ]);
4513
4783
  const getBarBackground = useCallback((index) => () => withPatterns ? `url(#${getPatternId(chartId, index)})` : getElementStyles({
4514
4784
  data: dataSorted[index],
4515
4785
  index
@@ -4519,26 +4789,43 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4519
4789
  dataSorted,
4520
4790
  chartId
4521
4791
  ]);
4792
+ const resolveComparisonFill = useCallback((entry) => withPatterns ? `url(#${getPatternId(chartId, entry.primaryIndex)})` : getElementStyles({
4793
+ data: entry.series,
4794
+ index: entry.index
4795
+ }).color, [
4796
+ withPatterns,
4797
+ chartId,
4798
+ getElementStyles
4799
+ ]);
4522
4800
  const renderDefaultTooltip = useCallback(({ tooltipData }) => {
4523
4801
  const nearestDatum = tooltipData?.nearestDatum?.datum;
4524
4802
  if (!nearestDatum) return null;
4803
+ const primaryKey = tooltipData?.nearestDatum?.key;
4804
+ const categoryLabel = chartOptions.tooltip.labelFormatter(nearestDatum.label || (nearestDatum.date ? nearestDatum.date.getTime() : 0), 0, []);
4805
+ const comparisonEntry = comparisonEntries.find((entry) => entry.primaryKey === primaryKey);
4806
+ const comparisonDatum = comparisonEntry?.series.data.find((point) => {
4807
+ const p = point;
4808
+ return nearestDatum.label != null ? p.label === nearestDatum.label : !!nearestDatum.date && !!p.date && p.date.getTime() === nearestDatum.date.getTime();
4809
+ });
4810
+ if (comparisonEntry && comparisonDatum && comparisonDatum.value != null) return /* @__PURE__ */ jsxs("div", {
4811
+ className: bar_chart_module_default["bar-chart__tooltip"],
4812
+ children: [
4813
+ /* @__PURE__ */ jsx("div", {
4814
+ className: bar_chart_module_default["bar-chart__tooltip-header"],
4815
+ children: categoryLabel
4816
+ }),
4817
+ renderTooltipRow(primaryKey, formatNumber(nearestDatum.value)),
4818
+ renderTooltipRow(comparisonEntry.series.label, formatNumber(comparisonDatum.value))
4819
+ ]
4820
+ });
4525
4821
  return /* @__PURE__ */ jsxs("div", {
4526
4822
  className: bar_chart_module_default["bar-chart__tooltip"],
4527
4823
  children: [/* @__PURE__ */ jsx("div", {
4528
4824
  className: bar_chart_module_default["bar-chart__tooltip-header"],
4529
- children: tooltipData?.nearestDatum?.key
4530
- }), /* @__PURE__ */ jsxs("div", {
4531
- className: bar_chart_module_default["bar-chart__tooltip-row"],
4532
- children: [/* @__PURE__ */ jsxs("span", {
4533
- className: bar_chart_module_default["bar-chart__tooltip-label"],
4534
- children: [chartOptions.tooltip.labelFormatter(nearestDatum.label || (nearestDatum.date ? nearestDatum.date.getTime() : 0), 0, []), ":"]
4535
- }), /* @__PURE__ */ jsx("span", {
4536
- className: bar_chart_module_default["bar-chart__tooltip-value"],
4537
- children: formatNumber(nearestDatum.value)
4538
- })]
4539
- })]
4825
+ children: primaryKey
4826
+ }), renderTooltipRow(categoryLabel, formatNumber(nearestDatum.value))]
4540
4827
  });
4541
- }, [chartOptions.tooltip]);
4828
+ }, [chartOptions.tooltip, comparisonEntries]);
4542
4829
  const renderPattern = useCallback((index, color) => {
4543
4830
  const patternType = index % 4;
4544
4831
  const id = getPatternId(chartId, index);
@@ -4575,8 +4862,10 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4575
4862
  }
4576
4863
  }, [chartId]);
4577
4864
  const createPatternBorderStyle = useCallback((index, color) => {
4865
+ const patternId = getPatternId(chartId, index);
4578
4866
  return `
4579
- .visx-bar[fill="url(#${getPatternId(chartId, index)})"] {
4867
+ .visx-bar[fill="url(#${patternId})"],
4868
+ .bar-chart__comparison-bars rect[fill="url(#${patternId})"] {
4580
4869
  stroke: ${color};
4581
4870
  stroke-width: 1;
4582
4871
  }
@@ -4584,11 +4873,13 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4584
4873
  }, [chartId]);
4585
4874
  const createKeyboardHighlightStyle = useCallback(() => {
4586
4875
  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 "";
4876
+ const primaryCount = primaryEntries.length;
4877
+ const maxDataPoints = Math.max(...primaryEntries.map((e) => e.series.data.length));
4878
+ const dataPointIndex = Math.floor(selectedIndex / primaryCount);
4879
+ const seriesIndex = selectedIndex % primaryCount;
4880
+ if (dataPointIndex >= maxDataPoints || seriesIndex >= primaryCount) return "";
4881
+ const seriesData = primaryEntries[seriesIndex]?.series;
4882
+ if (!seriesData || dataPointIndex >= seriesData.data.length) return "";
4592
4883
  return `
4593
4884
  .bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${seriesIndex * maxDataPoints + dataPointIndex + 1}) {
4594
4885
  stroke: #005fcc;
@@ -4597,7 +4888,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4597
4888
  `;
4598
4889
  }, [
4599
4890
  selectedIndex,
4600
- data,
4891
+ primaryEntries,
4601
4892
  chartId
4602
4893
  ]);
4603
4894
  const error = validateData$2(dataSorted);
@@ -4670,8 +4961,8 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4670
4961
  ...defaultMargin,
4671
4962
  ...margin
4672
4963
  },
4673
- xScale: chartOptions.xScale,
4674
- yScale: chartOptions.yScale,
4964
+ xScale,
4965
+ yScale,
4675
4966
  horizontal,
4676
4967
  pointerEventsDataKey: "nearest",
4677
4968
  children: [
@@ -4695,18 +4986,25 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4695
4986
  height: chartHeight,
4696
4987
  children: __("All series are hidden. Click legend items to show data.", "jetpack-charts")
4697
4988
  }) : null,
4989
+ /* @__PURE__ */ jsx(ComparisonBars, {
4990
+ comparisonEntries,
4991
+ primaryKeys,
4992
+ groupPadding,
4993
+ horizontal,
4994
+ xAccessor: chartOptions.accessors.xAccessor,
4995
+ yAccessor: chartOptions.accessors.yAccessor,
4996
+ getElementStyles,
4997
+ resolveFill: resolveComparisonFill
4998
+ }),
4698
4999
  /* @__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
- })
5000
+ padding: groupPadding,
5001
+ children: primaryEntries.map(({ series: seriesData, index }) => /* @__PURE__ */ jsx(BarSeries, {
5002
+ dataKey: seriesData?.label,
5003
+ data: seriesData.data,
5004
+ yAccessor: chartOptions.accessors.yAccessor,
5005
+ xAccessor: chartOptions.accessors.xAccessor,
5006
+ colorAccessor: getBarBackground(index)
5007
+ }, seriesData?.label))
4710
5008
  }),
4711
5009
  /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.x }),
4712
5010
  /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.y }),
@@ -4718,7 +5016,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4718
5016
  selectedIndex,
4719
5017
  tooltipRef,
4720
5018
  keyboardFocusedClassName: bar_chart_module_default["bar-chart__tooltip--keyboard-focused"],
4721
- series: data,
5019
+ series: primarySeries,
4722
5020
  mode: "individual"
4723
5021
  })
4724
5022
  ]
@@ -5319,9 +5617,7 @@ const DEFAULT_BACKGROUND_COLOR = "#ffffff";
5319
5617
  */
5320
5618
  const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", renderPlaceholder }) => {
5321
5619
  const { getElementStyles, theme: { geoChart: { featureFillColor }, backgroundColor } } = useGlobalChartsContext();
5322
- const loadingPlaceholder = /* @__PURE__ */ jsx(Stack, {
5323
- align: "center",
5324
- justify: "center",
5620
+ const loadingPlaceholder = /* @__PURE__ */ jsx(Center, {
5325
5621
  className: clsx("geo-chart", geo_chart_module_default.container, className),
5326
5622
  style: {
5327
5623
  width,
@@ -5379,9 +5675,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
5379
5675
  defaultFillColorHex,
5380
5676
  sanitizedData.hasHtmlTooltips
5381
5677
  ]);
5382
- return /* @__PURE__ */ jsx(Stack, {
5383
- align: "center",
5384
- justify: "center",
5678
+ return /* @__PURE__ */ jsx(Center, {
5385
5679
  className: clsx("geo-chart", geo_chart_module_default.container, className),
5386
5680
  style: {
5387
5681
  width,
@@ -8053,7 +8347,6 @@ function RadialWipeAnimation({ id, radius, innerRadius = 0, durationMs = 1e3, wi
8053
8347
  //#region src/charts/pie-chart/pie-chart.module.scss
8054
8348
  var pie_chart_module_default = {
8055
8349
  "pie-chart": "a8ccharts-gnszbG-pie-chart",
8056
- "pie-chart__centering": "a8ccharts-gnszbG-pie-chart__centering",
8057
8350
  "pie-chart--responsive": "a8ccharts-gnszbG-pie-chart--responsive"
8058
8351
  };
8059
8352
  //#endregion
@@ -8218,11 +8511,8 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
8218
8511
  const innerRadius = thickness === 0 ? 0 : outerRadius * (1 - thickness);
8219
8512
  const maxCornerRadius = (outerRadius - innerRadius) / 2;
8220
8513
  const cornerRadius = cornerScale ? Math.min(cornerScale * outerRadius, maxCornerRadius) : 0;
8221
- return /* @__PURE__ */ jsx(Stack, {
8514
+ return /* @__PURE__ */ jsx(Center, {
8222
8515
  ref: containerRef,
8223
- align: "center",
8224
- justify: "center",
8225
- className: pie_chart_module_default["pie-chart__centering"],
8226
8516
  children: /* @__PURE__ */ jsxs("svg", {
8227
8517
  viewBox: `0 0 ${width} ${height}`,
8228
8518
  preserveAspectRatio: "xMidYMid meet",
@@ -8335,7 +8625,6 @@ var pie_semi_circle_chart_module_default = {
8335
8625
  "label": "a8ccharts-YtTOxW-label",
8336
8626
  "note": "a8ccharts-YtTOxW-note",
8337
8627
  "pie-semi-circle-chart": "a8ccharts-YtTOxW-pie-semi-circle-chart",
8338
- "pie-semi-circle-chart__centering": "a8ccharts-YtTOxW-pie-semi-circle-chart__centering",
8339
8628
  "pie-semi-circle-chart--responsive": "a8ccharts-YtTOxW-pie-semi-circle-chart--responsive"
8340
8629
  };
8341
8630
  //#endregion
@@ -8515,11 +8804,8 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
8515
8804
  const height = width / 2;
8516
8805
  const radius = height;
8517
8806
  const innerRadius = radius * (1 - thickness);
8518
- return /* @__PURE__ */ jsx(Stack, {
8807
+ return /* @__PURE__ */ jsx(Center, {
8519
8808
  ref: containerRef,
8520
- align: "center",
8521
- justify: "center",
8522
- className: pie_semi_circle_chart_module_default["pie-semi-circle-chart__centering"],
8523
8809
  children: /* @__PURE__ */ jsxs("svg", {
8524
8810
  width,
8525
8811
  height,