@automattic/charts 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -2
  3. package/dist/index.cjs +380 -60
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +12 -7
  6. package/dist/index.d.cts +119 -30
  7. package/dist/index.d.ts +122 -33
  8. package/dist/index.js +380 -60
  9. package/dist/index.js.map +1 -1
  10. package/package.json +22 -21
  11. package/src/charts/area-chart/types.ts +1 -1
  12. package/src/charts/bar-chart/bar-chart.tsx +197 -43
  13. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +70 -0
  14. package/src/charts/bar-chart/private/comparison-bars.tsx +154 -0
  15. package/src/charts/bar-chart/private/comparison-constants.ts +33 -0
  16. package/src/charts/bar-chart/private/index.ts +9 -0
  17. package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +47 -0
  18. package/src/charts/bar-chart/private/test/comparison-bars.test.tsx +183 -0
  19. package/src/charts/bar-chart/private/use-bar-chart-options.ts +46 -5
  20. package/src/charts/bar-chart/test/bar-chart.test.tsx +191 -1
  21. package/src/charts/geo-chart/geo-chart.tsx +8 -10
  22. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +27 -14
  23. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +7 -4
  24. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +51 -4
  25. package/src/charts/line-chart/line-chart.tsx +1 -1
  26. package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +18 -2
  27. package/src/charts/line-chart/private/line-chart-annotation.tsx +1 -1
  28. package/src/charts/line-chart/types.ts +1 -1
  29. package/src/charts/pie-chart/pie-chart.module.scss +0 -4
  30. package/src/charts/pie-chart/pie-chart.tsx +3 -8
  31. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +0 -5
  32. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +5 -10
  33. package/src/charts/private/center/center.module.scss +4 -0
  34. package/src/charts/private/center/center.tsx +33 -0
  35. package/src/charts/private/center/index.ts +2 -0
  36. package/src/charts/private/center/test/center.test.tsx +60 -0
  37. package/src/charts/private/chart-layout/chart-layout.tsx +1 -2
  38. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +0 -2
  39. package/src/charts/private/svg-empty-state/svg-empty-state.tsx +2 -4
  40. package/src/charts/private/x-zoom.tsx +2 -2
  41. package/src/components/legend/hooks/use-chart-legend-items.ts +6 -2
  42. package/src/components/legend/legend.tsx +1 -2
  43. package/src/components/legend/utils/label-transform-factory.ts +1 -1
  44. package/src/components/tooltip/accessible-tooltip.tsx +2 -6
  45. package/src/index.ts +6 -5
  46. package/src/providers/chart-context/global-charts-provider.tsx +2 -0
  47. package/src/providers/chart-context/test/chart-context.test.tsx +13 -1
  48. package/src/providers/chart-context/themes.ts +9 -1
  49. package/src/providers/chart-context/types.ts +9 -2
  50. package/src/types.ts +120 -11
  51. package/src/utils/get-styles.ts +36 -14
  52. package/src/utils/index.ts +6 -1
  53. package/src/utils/test/get-styles.test.ts +47 -1
  54. package/src/visx/types.ts +30 -1
package/dist/index.js CHANGED
@@ -17,7 +17,6 @@ import { LinearGradient } from "@visx/gradient";
17
17
  import { curveCatmullRom, curveLinear, curveMonotoneX } from "@visx/curve";
18
18
  import { useParentSize } from "@visx/responsive";
19
19
  import { Annotation, CircleSubject, Connector, HtmlLabel, Label, LineSubject } from "@visx/annotation";
20
- import { Icon, chevronRight, close } from "@wordpress/icons";
21
20
  import { PatternCircles, PatternHexagons, PatternLines, PatternWaves } from "@visx/pattern";
22
21
  import { useTooltip, useTooltipInPortal } from "@visx/tooltip";
23
22
  import { Chart } from "react-google-charts";
@@ -25,6 +24,7 @@ import DOMPurify from "dompurify";
25
24
  import { __assign } from "tslib";
26
25
  import _extends from "@babel/runtime/helpers/esm/extends";
27
26
  import "@babel/runtime/helpers/extends";
27
+ import { Icon, chevronRight } from "@wordpress/icons";
28
28
  import { Pie } from "@visx/shape";
29
29
  //#region \0rolldown/runtime.js
30
30
  var __create = Object.create;
@@ -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
@@ -709,7 +727,7 @@ const defaultTheme = {
709
727
  leaderboardChart: {
710
728
  rowGap: 12,
711
729
  columnGap: 4,
712
- labelSpacing: 1.5,
730
+ labelSpacing: "xs",
713
731
  deltaColors: [
714
732
  "#FF8C8F",
715
733
  "#757575",
@@ -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
  })
@@ -3059,6 +3106,19 @@ const LineChartAnnotationsOverlay = ({ children }) => {
3059
3106
  })
3060
3107
  });
3061
3108
  };
3109
+ const CloseIcon = () => /* @__PURE__ */ jsx("svg", {
3110
+ width: "16",
3111
+ height: "16",
3112
+ viewBox: "0 0 24 24",
3113
+ fill: "none",
3114
+ stroke: "currentColor",
3115
+ strokeWidth: "2",
3116
+ strokeLinecap: "round",
3117
+ strokeLinejoin: "round",
3118
+ "aria-hidden": "true",
3119
+ focusable: "false",
3120
+ children: /* @__PURE__ */ jsx("path", { d: "M6 6l12 12M18 6L6 18" })
3121
+ });
3062
3122
  const LineChartAnnotationLabelWithPopover = ({ title, subtitle, renderLabel, renderLabelPopover }) => {
3063
3123
  const popoverId = useId();
3064
3124
  const buttonRef = useRef(null);
@@ -3120,10 +3180,7 @@ const LineChartAnnotationLabelWithPopover = ({ title, subtitle, renderLabel, ren
3120
3180
  popovertargetaction: "hide",
3121
3181
  className: line_chart_module_default["line-chart__annotation-label-popover-close-button"],
3122
3182
  "aria-label": __("Close", "jetpack-charts"),
3123
- children: /* @__PURE__ */ jsx(Icon, {
3124
- icon: close,
3125
- size: 16
3126
- })
3183
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
3127
3184
  })]
3128
3185
  })
3129
3186
  })]
@@ -4331,6 +4388,10 @@ const TruncatedXTickComponent = createTruncatedTickComponent("x");
4331
4388
  const TruncatedYTickComponent = createTruncatedTickComponent("y");
4332
4389
  //#endregion
4333
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;
4334
4395
  const formatDateTick = (timestamp) => {
4335
4396
  return new Date(timestamp).toLocaleDateString(void 0, {
4336
4397
  month: "short",
@@ -4358,8 +4419,8 @@ function useBarChartOptions(data, horizontal, options = {}) {
4358
4419
  const defaultOptions = useMemo(() => {
4359
4420
  const bandScale = {
4360
4421
  type: "band",
4361
- padding: .2,
4362
- paddingInner: .1
4422
+ padding: BASE_BAND_PADDING,
4423
+ paddingInner: BASE_BAND_PADDING_INNER
4363
4424
  };
4364
4425
  const linearScale = {
4365
4426
  type: "linear",
@@ -4398,13 +4459,29 @@ function useBarChartOptions(data, horizontal, options = {}) {
4398
4459
  }, [data]);
4399
4460
  return useMemo(() => {
4400
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
+ }
4401
4476
  const xScale = {
4402
4477
  ...baseXScale,
4403
- ...options.xScale || {}
4478
+ ...options.xScale || {},
4479
+ ...horizontal ? valueScaleDomainOverride : {}
4404
4480
  };
4405
4481
  const yScale = {
4406
4482
  ...baseYScale,
4407
- ...options.yScale || {}
4483
+ ...options.yScale || {},
4484
+ ...!horizontal ? valueScaleDomainOverride : {}
4408
4485
  };
4409
4486
  const providedToolTipLabelFormatter = horizontal ? options.axis?.y?.tickFormat : options.axis?.x?.tickFormat;
4410
4487
  const { labelOverflow: xLabelOverflow, ...xAxisOptions } = options.axis?.x || {};
@@ -4439,10 +4516,143 @@ function useBarChartOptions(data, horizontal, options = {}) {
4439
4516
  }, [
4440
4517
  defaultOptions,
4441
4518
  options,
4442
- horizontal
4519
+ horizontal,
4520
+ data
4443
4521
  ]);
4444
4522
  }
4445
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
4446
4656
  //#region src/charts/bar-chart/bar-chart.tsx
4447
4657
  const validateData$2 = (data) => {
4448
4658
  if (!data?.length) return "No data available";
@@ -4471,13 +4681,14 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4471
4681
  }, [height]);
4472
4682
  const [selectedIndex, setSelectedIndex] = useState(void 0);
4473
4683
  const [isNavigating, setIsNavigating] = useState(false);
4684
+ const primarySeriesForNav = dataWithVisibleZeros.filter((s) => s.options?.type !== "comparison");
4474
4685
  const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation({
4475
4686
  selectedIndex,
4476
4687
  setSelectedIndex,
4477
4688
  isNavigating,
4478
4689
  setIsNavigating,
4479
4690
  chartRef,
4480
- 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
4481
4692
  });
4482
4693
  const { getElementStyles, isSeriesVisible } = useGlobalChartsContext();
4483
4694
  const seriesWithVisibility = useMemo(() => {
@@ -4500,6 +4711,69 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4500
4711
  const allSeriesHidden = useMemo(() => {
4501
4712
  return seriesWithVisibility.every(({ isVisible }) => !isVisible);
4502
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
+ ]);
4503
4777
  const getBarBackground = useCallback((index) => () => withPatterns ? `url(#${getPatternId(chartId, index)})` : getElementStyles({
4504
4778
  data: dataSorted[index],
4505
4779
  index
@@ -4509,26 +4783,70 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4509
4783
  dataSorted,
4510
4784
  chartId
4511
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
+ ]);
4512
4794
  const renderDefaultTooltip = useCallback(({ tooltipData }) => {
4513
4795
  const nearestDatum = tooltipData?.nearestDatum?.datum;
4514
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
+ });
4515
4833
  return /* @__PURE__ */ jsxs("div", {
4516
4834
  className: bar_chart_module_default["bar-chart__tooltip"],
4517
4835
  children: [/* @__PURE__ */ jsx("div", {
4518
4836
  className: bar_chart_module_default["bar-chart__tooltip-header"],
4519
- children: tooltipData?.nearestDatum?.key
4837
+ children: primaryKey
4520
4838
  }), /* @__PURE__ */ jsxs("div", {
4521
4839
  className: bar_chart_module_default["bar-chart__tooltip-row"],
4522
4840
  children: [/* @__PURE__ */ jsxs("span", {
4523
4841
  className: bar_chart_module_default["bar-chart__tooltip-label"],
4524
- children: [chartOptions.tooltip.labelFormatter(nearestDatum.label || (nearestDatum.date ? nearestDatum.date.getTime() : 0), 0, []), ":"]
4842
+ children: [categoryLabel, ":"]
4525
4843
  }), /* @__PURE__ */ jsx("span", {
4526
4844
  className: bar_chart_module_default["bar-chart__tooltip-value"],
4527
4845
  children: formatNumber(nearestDatum.value)
4528
4846
  })]
4529
4847
  })]
4530
4848
  });
4531
- }, [chartOptions.tooltip]);
4849
+ }, [chartOptions.tooltip, comparisonEntries]);
4532
4850
  const renderPattern = useCallback((index, color) => {
4533
4851
  const patternType = index % 4;
4534
4852
  const id = getPatternId(chartId, index);
@@ -4565,8 +4883,10 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4565
4883
  }
4566
4884
  }, [chartId]);
4567
4885
  const createPatternBorderStyle = useCallback((index, color) => {
4886
+ const patternId = getPatternId(chartId, index);
4568
4887
  return `
4569
- .visx-bar[fill="url(#${getPatternId(chartId, index)})"] {
4888
+ .visx-bar[fill="url(#${patternId})"],
4889
+ .bar-chart__comparison-bars rect[fill="url(#${patternId})"] {
4570
4890
  stroke: ${color};
4571
4891
  stroke-width: 1;
4572
4892
  }
@@ -4574,11 +4894,13 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4574
4894
  }, [chartId]);
4575
4895
  const createKeyboardHighlightStyle = useCallback(() => {
4576
4896
  if (selectedIndex === void 0) return "";
4577
- const maxDataPoints = Math.max(...data.map((s) => s.data.length));
4578
- const dataPointIndex = Math.floor(selectedIndex / data.length);
4579
- const seriesIndex = selectedIndex % data.length;
4580
- if (dataPointIndex >= maxDataPoints || seriesIndex >= data.length) return "";
4581
- 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 "";
4582
4904
  return `
4583
4905
  .bar-chart[data-chart-id="bar-chart-${chartId}"] .visx-bar-group .visx-bar:nth-child(${seriesIndex * maxDataPoints + dataPointIndex + 1}) {
4584
4906
  stroke: #005fcc;
@@ -4587,7 +4909,7 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4587
4909
  `;
4588
4910
  }, [
4589
4911
  selectedIndex,
4590
- data,
4912
+ primaryEntries,
4591
4913
  chartId
4592
4914
  ]);
4593
4915
  const error = validateData$2(dataSorted);
@@ -4660,8 +4982,8 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4660
4982
  ...defaultMargin,
4661
4983
  ...margin
4662
4984
  },
4663
- xScale: chartOptions.xScale,
4664
- yScale: chartOptions.yScale,
4985
+ xScale,
4986
+ yScale,
4665
4987
  horizontal,
4666
4988
  pointerEventsDataKey: "nearest",
4667
4989
  children: [
@@ -4685,18 +5007,25 @@ const BarChartInternal = ({ data, chartId: providedChartId, width, height, class
4685
5007
  height: chartHeight,
4686
5008
  children: __("All series are hidden. Click legend items to show data.", "jetpack-charts")
4687
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
+ }),
4688
5020
  /* @__PURE__ */ jsx(BarGroup, {
4689
- padding: chartOptions.barGroup.padding,
4690
- children: seriesWithVisibility.map(({ series: seriesData, index, isVisible }) => {
4691
- if (!isVisible) return null;
4692
- return /* @__PURE__ */ jsx(BarSeries, {
4693
- dataKey: seriesData?.label,
4694
- data: seriesData.data,
4695
- yAccessor: chartOptions.accessors.yAccessor,
4696
- xAccessor: chartOptions.accessors.xAccessor,
4697
- colorAccessor: getBarBackground(index)
4698
- }, seriesData?.label);
4699
- })
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))
4700
5029
  }),
4701
5030
  /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.x }),
4702
5031
  /* @__PURE__ */ jsx(Axis, { ...chartOptions.axis.y }),
@@ -5309,9 +5638,7 @@ const DEFAULT_BACKGROUND_COLOR = "#ffffff";
5309
5638
  */
5310
5639
  const GeoChartInternal = ({ className, data, width, height, region = "world", resolution = "countries", renderPlaceholder }) => {
5311
5640
  const { getElementStyles, theme: { geoChart: { featureFillColor }, backgroundColor } } = useGlobalChartsContext();
5312
- const loadingPlaceholder = /* @__PURE__ */ jsx(Stack, {
5313
- align: "center",
5314
- justify: "center",
5641
+ const loadingPlaceholder = /* @__PURE__ */ jsx(Center, {
5315
5642
  className: clsx("geo-chart", geo_chart_module_default.container, className),
5316
5643
  style: {
5317
5644
  width,
@@ -5369,9 +5696,7 @@ const GeoChartInternal = ({ className, data, width, height, region = "world", re
5369
5696
  defaultFillColorHex,
5370
5697
  sanitizedData.hasHtmlTooltips
5371
5698
  ]);
5372
- return /* @__PURE__ */ jsx(Stack, {
5373
- align: "center",
5374
- justify: "center",
5699
+ return /* @__PURE__ */ jsx(Center, {
5375
5700
  className: clsx("geo-chart", geo_chart_module_default.container, className),
5376
5701
  style: {
5377
5702
  width,
@@ -7763,13 +8088,15 @@ const defaultDeltaFormatter = (value) => {
7763
8088
  };
7764
8089
  /**
7765
8090
  * Build a bar's width. A hover-inset CSS variable (0 by default) is subtracted
7766
- * so interactive rows can pull the bar's right edge back by a fixed pixel amount
7767
- * on hover instead of a percentage scale keeping the bar↔value gap constant.
8091
+ * on hover, scaled by the bar's share so the pull-back is proportional to its
8092
+ * length: the full-length (100%) bar the one that reaches the value pulls
8093
+ * back the whole inset to keep its gap with the value, while shorter bars pull
8094
+ * back proportionally less, down to ~0 for a very short bar.
7768
8095
  *
7769
8096
  * @param share - The bar's share of the row width, as a percentage.
7770
8097
  * @return A CSS width value.
7771
8098
  */
7772
- const getBarWidth = (share) => `calc(${share}% - var(--a8c--charts--leaderboard--bar--hover-inset, 0px))`;
8099
+ const getBarWidth = (share) => `calc(${share}% - var(--a8c--charts--leaderboard--bar--hover-inset, 0px) * ${share} / 100)`;
7773
8100
  const BarLabel = ({ label }) => /* @__PURE__ */ jsx(Fragment$1, { children: typeof label === "string" ? /* @__PURE__ */ jsx(Text$1, {
7774
8101
  className: leaderboard_chart_module_default.label,
7775
8102
  children: label
@@ -7978,6 +8305,7 @@ const LeaderboardChartInternal = ({ data, chartId: providedChartId, width: propW
7978
8305
  type: "button",
7979
8306
  className: leaderboard_chart_module_default.interactiveRow,
7980
8307
  onClick: entry.onClick,
8308
+ "aria-label": entry.ariaLabel,
7981
8309
  children: [rowCells, /* @__PURE__ */ jsx(Icon, {
7982
8310
  className: leaderboard_chart_module_default.chevron,
7983
8311
  icon: chevronRight,
@@ -8040,7 +8368,6 @@ function RadialWipeAnimation({ id, radius, innerRadius = 0, durationMs = 1e3, wi
8040
8368
  //#region src/charts/pie-chart/pie-chart.module.scss
8041
8369
  var pie_chart_module_default = {
8042
8370
  "pie-chart": "a8ccharts-gnszbG-pie-chart",
8043
- "pie-chart__centering": "a8ccharts-gnszbG-pie-chart__centering",
8044
8371
  "pie-chart--responsive": "a8ccharts-gnszbG-pie-chart--responsive"
8045
8372
  };
8046
8373
  //#endregion
@@ -8205,11 +8532,8 @@ const PieChartInternal = ({ data, chartId: providedChartId, withTooltips = false
8205
8532
  const innerRadius = thickness === 0 ? 0 : outerRadius * (1 - thickness);
8206
8533
  const maxCornerRadius = (outerRadius - innerRadius) / 2;
8207
8534
  const cornerRadius = cornerScale ? Math.min(cornerScale * outerRadius, maxCornerRadius) : 0;
8208
- return /* @__PURE__ */ jsx(Stack, {
8535
+ return /* @__PURE__ */ jsx(Center, {
8209
8536
  ref: containerRef,
8210
- align: "center",
8211
- justify: "center",
8212
- className: pie_chart_module_default["pie-chart__centering"],
8213
8537
  children: /* @__PURE__ */ jsxs("svg", {
8214
8538
  viewBox: `0 0 ${width} ${height}`,
8215
8539
  preserveAspectRatio: "xMidYMid meet",
@@ -8322,7 +8646,6 @@ var pie_semi_circle_chart_module_default = {
8322
8646
  "label": "a8ccharts-YtTOxW-label",
8323
8647
  "note": "a8ccharts-YtTOxW-note",
8324
8648
  "pie-semi-circle-chart": "a8ccharts-YtTOxW-pie-semi-circle-chart",
8325
- "pie-semi-circle-chart__centering": "a8ccharts-YtTOxW-pie-semi-circle-chart__centering",
8326
8649
  "pie-semi-circle-chart--responsive": "a8ccharts-YtTOxW-pie-semi-circle-chart--responsive"
8327
8650
  };
8328
8651
  //#endregion
@@ -8502,11 +8825,8 @@ const PieSemiCircleChartInternal = ({ data, chartId: providedChartId, width: pro
8502
8825
  const height = width / 2;
8503
8826
  const radius = height;
8504
8827
  const innerRadius = radius * (1 - thickness);
8505
- return /* @__PURE__ */ jsx(Stack, {
8828
+ return /* @__PURE__ */ jsx(Center, {
8506
8829
  ref: containerRef,
8507
- align: "center",
8508
- justify: "center",
8509
- className: pie_semi_circle_chart_module_default["pie-semi-circle-chart__centering"],
8510
8830
  children: /* @__PURE__ */ jsxs("svg", {
8511
8831
  width,
8512
8832
  height,