From 21cef3ad17ffee87314da213bd74bedce318c786 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:15:19 +1300 Subject: [PATCH 01/10] Enhance chart components by adding 'gap' prop for spacing between elements. Update BarChart, LineChart, and PieChart interfaces to include the new prop, and reflect this change in their respective documentation. Remove redundant gap prop definitions from the interfaces. --- .../js-packages/charts/src/charts/bar-chart/bar-chart.tsx | 7 ------- .../charts/src/charts/bar-chart/stories/index.api.mdx | 1 + .../charts/src/charts/line-chart/stories/index.api.mdx | 1 + .../js-packages/charts/src/charts/line-chart/types.ts | 7 ------- .../js-packages/charts/src/charts/pie-chart/pie-chart.tsx | 8 -------- projects/js-packages/charts/src/types.ts | 8 ++++++++ 6 files changed, 10 insertions(+), 22 deletions(-) diff --git a/projects/js-packages/charts/src/charts/bar-chart/bar-chart.tsx b/projects/js-packages/charts/src/charts/bar-chart/bar-chart.tsx index 80ba480c0b1c..75802258c0f6 100644 --- a/projects/js-packages/charts/src/charts/bar-chart/bar-chart.tsx +++ b/projects/js-packages/charts/src/charts/bar-chart/bar-chart.tsx @@ -32,7 +32,6 @@ import { useBarChartOptions } from './private'; import type { BaseChartProps, DataPointDate, SeriesData, Optional } from '../../types'; import type { ResponsiveConfig } from '../private/with-responsive'; import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip'; -import type { GapSize } from '@wordpress/theme'; import type { FC, ReactNode, ComponentType } from 'react'; export interface BarChartProps extends BaseChartProps< SeriesData[] > { @@ -42,12 +41,6 @@ export interface BarChartProps extends BaseChartProps< SeriesData[] > { showZeroValues?: boolean; legendInteractive?: boolean; children?: ReactNode; - /** - * Gap between chart elements (SVG, legend, children). - * Uses WordPress design system tokens. - * @default 'md' - */ - gap?: GapSize; } // Base props type with optional responsive properties diff --git a/projects/js-packages/charts/src/charts/bar-chart/stories/index.api.mdx b/projects/js-packages/charts/src/charts/bar-chart/stories/index.api.mdx index e2c797aefc57..556eb3479f51 100644 --- a/projects/js-packages/charts/src/charts/bar-chart/stories/index.api.mdx +++ b/projects/js-packages/charts/src/charts/bar-chart/stories/index.api.mdx @@ -32,6 +32,7 @@ Main chart component with responsive behavior by default. | `options` | `ChartOptions` | `{}` | Advanced axis and scale configuration | | `className` | `string` | - | Additional CSS class name | | `children` | `ReactNode` | - | Child components (e.g., ``) | +| `gap` | `GapSize` | `'md'` | Gap between chart elements (SVG, legend, children). Uses WordPress design system tokens | ## BarChart.Legend diff --git a/projects/js-packages/charts/src/charts/line-chart/stories/index.api.mdx b/projects/js-packages/charts/src/charts/line-chart/stories/index.api.mdx index 0ee42ccab3c2..add6e12bf21e 100644 --- a/projects/js-packages/charts/src/charts/line-chart/stories/index.api.mdx +++ b/projects/js-packages/charts/src/charts/line-chart/stories/index.api.mdx @@ -38,6 +38,7 @@ Main chart component with responsive behavior by default. | `onPointerMove` | `(event: EventHandlerParams) => void?` | - | Pointer move event handler | | `onPointerOut` | `(event: PointerEvent) => void?` | - | Pointer out event handler | | `children` | `ReactNode?` | - | Child components (e.g., annotations) | +| `gap` | `GapSize` | `'md'` | Gap between chart elements (SVG, legend, children). Uses WordPress design system tokens | ## LineChart.AnnotationsOverlay diff --git a/projects/js-packages/charts/src/charts/line-chart/types.ts b/projects/js-packages/charts/src/charts/line-chart/types.ts index 6ca0d12ca1b3..0d5d5629bce2 100644 --- a/projects/js-packages/charts/src/charts/line-chart/types.ts +++ b/projects/js-packages/charts/src/charts/line-chart/types.ts @@ -7,7 +7,6 @@ import type { } from '../../types'; import type { GlyphProps } from '@visx/xychart'; import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip'; -import type { GapSize } from '@wordpress/theme'; import type { ReactNode, SVGProps, FC } from 'react'; export type LineChartAnnotationProps = { @@ -44,12 +43,6 @@ export interface LineChartProps extends BaseChartProps< SeriesData[] > { }; legendInteractive?: boolean; children?: ReactNode; - /** - * Gap between chart elements (SVG, legend, children). - * Uses WordPress design system tokens. - * @default 'md' - */ - gap?: GapSize; } export type TooltipDatum = { diff --git a/projects/js-packages/charts/src/charts/pie-chart/pie-chart.tsx b/projects/js-packages/charts/src/charts/pie-chart/pie-chart.tsx index ccca4e30e199..6282a8496786 100644 --- a/projects/js-packages/charts/src/charts/pie-chart/pie-chart.tsx +++ b/projects/js-packages/charts/src/charts/pie-chart/pie-chart.tsx @@ -26,7 +26,6 @@ import styles from './pie-chart.module.scss'; import type { LegendValueDisplay } from '../../components/legend'; import type { BaseChartProps, DataPointPercentage, Optional } from '../../types'; import type { ChartComponentWithComposition } from '../private/chart-composition'; -import type { GapSize } from '@wordpress/theme'; import type { SVGProps, MouseEvent, ReactNode, FC } from 'react'; /** @@ -121,13 +120,6 @@ export interface PieChartProps extends BaseChartProps< DataPointPercentage[] > { * When provided, replaces the default BaseTooltip with custom content. */ renderTooltip?: ( params: PieChartRenderTooltipParams ) => ReactNode; - - /** - * Gap between chart elements (SVG, legend, children). - * Uses WordPress design system tokens. - * @default 'md' - */ - gap?: GapSize; } // Base props type with optional responsive properties diff --git a/projects/js-packages/charts/src/types.ts b/projects/js-packages/charts/src/types.ts index 781f064b1341..e59c352c1c44 100644 --- a/projects/js-packages/charts/src/types.ts +++ b/projects/js-packages/charts/src/types.ts @@ -7,6 +7,7 @@ import type { LegendShape } from '@visx/legend/lib/types'; import type { ScaleInput, ScaleType } from '@visx/scale'; import type { TextProps } from '@visx/text/lib/Text'; import type { EventHandlerParams, GlyphProps, GridStyles, LineStyles } from '@visx/xychart'; +import type { GapSize } from '@wordpress/theme'; import type { CSSProperties, PointerEvent, ReactNode } from 'react'; import type { GoogleDataTableColumn, GoogleDataTableRow } from 'react-google-charts'; @@ -459,6 +460,13 @@ export type BaseChartProps< T = DataPoint | DataPointDate | LeaderboardEntry > = */ animation?: boolean; + /** + * Gap between chart elements (SVG, legend, children). + * Uses WordPress design system tokens. + * @default 'md' + */ + gap?: GapSize; + /** * More options for the chart. */ From 56dda779e027b3ec67f7300d04638d0a74818a1d Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:16:38 +1300 Subject: [PATCH 02/10] fix(charts): Make PieSemiCircleChart responsive by default The chart previously hardcoded a 400px width and derived height by subtracting legend height from the chart area. This caused incorrect sizing when the container had height constraints or the legend position changed. The chart now fills its parent container and measures the available SVG wrapper area to maintain a 2:1 width-to-height ratio, constrained by both available width and height. Explicit width/height props override the responsive behavior. - Replace fixed width default with responsive container measurement - Use @wordpress/ui Stack for gap spacing between chart elements - Add configurable gap prop using design system tokens - Wrap SVG in a flex measurement div for accurate sizing Co-authored-by: Cursor --- .../pie-semi-circle-chart.module.scss | 19 +- .../pie-semi-circle-chart.tsx | 260 ++++++++++-------- .../stories/index.api.mdx | 3 +- .../stories/index.stories.tsx | 60 +++- .../test/pie-semi-circle-chart.test.tsx | 52 +++- 5 files changed, 251 insertions(+), 143 deletions(-) diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss index 47d9df637406..e3b9d718cd2c 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss @@ -2,10 +2,23 @@ display: flex; flex-direction: column; text-align: center; - gap: 20px; + overflow: hidden; - &--legend-top { - flex-direction: column-reverse; + // Fill parent when no explicit width/height provided + &--responsive { + height: 100%; + width: 100%; + } + + // Flex wrapper that fills remaining Stack space and measures the SVG area + &__svg-wrapper { + flex: 1; + min-height: 0; // Required for flex shrinking + min-width: 0; // Required for flex shrinking + width: 100%; + display: flex; + align-items: center; + justify-content: center; } .label { diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx index 46c9d62764a2..f1709900082a 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx @@ -3,6 +3,7 @@ import { Pie } from '@visx/shape'; import { Text } from '@visx/text'; import { useTooltip, useTooltipInPortal } from '@visx/tooltip'; import { __ } from '@wordpress/i18n'; +import { Stack } from '@wordpress/ui'; import clsx from 'clsx'; import { useCallback, useContext, useMemo } from 'react'; import { Legend, useChartLegendItems } from '../../components/legend'; @@ -55,7 +56,9 @@ const PAD_ANGLE = 0.03; // Padding between segments export interface PieSemiCircleChartProps extends BaseChartProps< DataPointPercentage[] > { /** - * Width of the chart in pixels; height would be half of this value calculated automatically. + * Explicit width of the chart container in pixels. + * When omitted, the chart fills its parent container's width. + * The chart always maintains a 2:1 width-to-height ratio, constrained by available space. */ width?: number; @@ -157,7 +160,8 @@ const validateData = ( data: DataPointPercentage[] ) => { const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { data, chartId: providedChartId, - width = 400, + width: propWidth, + height: propHeight, thickness = 0.4, clockwise = true, withTooltips = false, @@ -179,9 +183,11 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { tooltipOffsetX = 0, tooltipOffsetY = -15, renderTooltip = renderDefaultPieSemiCircleTooltip, + gap = 'md', } ) => { const chartId = useChartId( providedChartId ); - const [ legendRef, , legendHeight ] = useElementSize< HTMLDivElement >(); + // Measure the SVG wrapper to calculate constrained dimensions + const [ svgWrapperRef, svgWrapperWidth, svgWrapperHeight ] = useElementSize< HTMLDivElement >(); const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = useTooltip< DataPointPercentage >(); @@ -298,7 +304,11 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { if ( ! isValid ) { return (
- + { message } @@ -307,12 +317,16 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { ); } - // Calculate chart dimensions - // TODO: we might want to accept height as a prop in the future, because the height of container might not always be enough. + // Calculate chart dimensions maintaining the 2:1 width-to-height ratio. + // Use measured SVG wrapper dimensions to respect height constraints, falling back + // to explicit props during initial render before measurement is available. + const availableWidth = svgWrapperWidth > 0 ? svgWrapperWidth : propWidth || 400; + const availableHeight = + svgWrapperHeight > 0 ? svgWrapperHeight : propHeight || ( propWidth || 400 ) / 2; + // Constrain width so that height (= width / 2) never exceeds the available height + const width = Math.min( availableWidth, availableHeight * 2 ); const height = width / 2; - // The chart only takes the height minus the legend height. - const chartHeight = height - ( showLegend && legendPosition === 'top' ? legendHeight : 0 ); - const radius = Math.min( width / 2, chartHeight ); + const radius = height; // For a semi-circle, radius equals the SVG height const innerRadius = radius * ( 1 - thickness ); // Map data with index for color assignment @@ -329,119 +343,144 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { const startAngle = clockwise ? -Math.PI / 2 : Math.PI / 2; const endAngle = clockwise ? Math.PI / 2 : -Math.PI / 2; + const legendElement = showLegend && ( + + ); + return ( -
- - - - - - { /* Main chart group centered horizontally and positioned at bottom */ } - + - { allSegmentsHidden ? ( - - { __( - 'All segments are hidden. Click legend items to show data.', - 'jetpack-charts' - ) } - - ) : ( - <> - { /* Pie chart */ } - - data={ dataWithIndex } - pieValue={ accessors.value } - outerRadius={ radius } - innerRadius={ innerRadius } - cornerRadius={ 3 } - padAngle={ PAD_ANGLE } - startAngle={ startAngle } - endAngle={ endAngle } - pieSort={ accessors.sort } + + + + + { /* Main chart group centered horizontally and positioned at bottom */ } + + { allSegmentsHidden ? ( + - { pie => { - return pie.arcs.map( arc => ( - - - - ) ); - } } - - - { /* Label and note text */ } - - - { label } - - + ) : ( + <> + { /* Pie chart */ } + + data={ dataWithIndex } + pieValue={ accessors.value } + outerRadius={ radius } + innerRadius={ innerRadius } + cornerRadius={ 3 } + padAngle={ PAD_ANGLE } + startAngle={ startAngle } + endAngle={ endAngle } + pieSort={ accessors.sort } > - { note } - - - - { /* Render SVG children from composition API */ } - { ! allSegmentsHidden && svgChildren } - - ) } - - + { pie => { + return pie.arcs.map( arc => ( + + + + ) ); + } } + + + { /* Label and note text */ } + + + { label } + + + { note } + + + + { /* Render SVG children from composition API */ } + { ! allSegmentsHidden && svgChildren } + + ) } + + +
+ + { legendPosition !== 'top' && legendElement } { withTooltips && tooltipOpen && tooltipData && ( @@ -449,27 +488,12 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { ) } - { showLegend && ( - - ) } - { /* Render HTML children from composition API */ } { htmlChildren } { /* Render any other children that aren't compound components */ } { otherChildren } -
+ ); }; diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.api.mdx b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.api.mdx index d8ab0a8e0373..d37ec710bd01 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.api.mdx +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.api.mdx @@ -13,7 +13,7 @@ Main component for rendering semi-circular pie charts. | Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | `data` | `DataPointPercentage[]` | - | **Required.** Array of data points to display | -| `width` | `number` | `400` | Width of the chart in pixels (height is automatically half of width) | +| `width` | `number` | responsive | Width of the chart in pixels. When omitted, fills parent width (height is automatically half of width) | | `thickness` | `number` | `0.4` | Thickness of the pie segments (0-1, where 1 is full thickness) | | `clockwise` | `boolean` | `true` | Direction of segment rendering | | `label` | `string` | - | Text displayed above the chart | @@ -28,6 +28,7 @@ Main component for rendering semi-circular pie charts. | `legendPosition` | `'top' \| 'bottom'` | `'bottom'` | Legend position (where the legend appears) | | `legendShape` | `'circle' \| 'rect' \| 'line'` | `'circle'` | Shape of legend indicators | | `className` | `string` | - | Additional CSS class for the container | +| `gap` | `GapSize` | `'md'` | Gap between chart elements (SVG, legend, children). Uses WordPress design system tokens | | `chartId` | `string` | - | Optional unique identifier (auto-generated if not provided) | ## PieSemiCircleChartRenderTooltipParams Type diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.stories.tsx b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.stories.tsx index 165fb0a3250f..3b95175729e3 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.stories.tsx +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.stories.tsx @@ -51,14 +51,48 @@ type Story = StoryObj< StoryArgs >; export const Default: Story = { args: { ...sharedThemeArgs, - containerWidth: '600px', - resize: 'none', thickness: 0.4, data, label: 'OS', note: 'Windows +10%', clockwise: true, }, + parameters: { + docs: { + description: { + story: + 'Responsive semi-circle pie chart. Resize the dashed container to see the chart adapt while maintaining a 2:1 width-to-height ratio.', + }, + }, + }, +}; + +export const FixedDimensions: Story = { + render: args => ( + + ), + args: { + ...Default.args, + resize: 'none', + containerWidth: '600px', + containerHeight: '350px', + width: 400, + }, + parameters: { + docs: { + description: { + story: + 'Semi-circle pie chart with fixed pixel dimensions. Use `PieSemiCircleChartUnresponsive` when you need precise control over the chart size.', + }, + }, + }, }; export const Animation: Story = { @@ -102,7 +136,6 @@ export const WithCompositionLegend: Story = {

Traditional Props-based Legend

Composition API with Legend Component

- + ( { + useParentSize: jest.fn( () => ( { + parentRef: { current: null }, + width: 400, + height: 200, + } ) ), +} ) ); + // Mock data for testing const mockData = [ { @@ -167,15 +176,15 @@ describe( 'PieSemiCircleChart', () => { expect( thinPathD ).not.toBe( thickPathD ); } ); - it( 'renders with correct dimensions', () => { - const width = 400; - render( ); + it( 'renders with correct dimensions from measured container', () => { + // Mock returns width:400, height:200 — chart should render at 400×200 (2:1 ratio) + render( ); const svg = screen.getByTestId( 'pie-chart-svg' ); - expect( svg ).toHaveAttribute( 'width', width.toString() ); - expect( svg ).toHaveAttribute( 'height', ( width / 2 ).toString() ); - expect( svg ).toHaveAttribute( 'viewBox', `0 0 ${ width } ${ width / 2 }` ); + expect( svg ).toHaveAttribute( 'width', '400' ); + expect( svg ).toHaveAttribute( 'height', '200' ); + expect( svg ).toHaveAttribute( 'viewBox', '0 0 400 200' ); } ); describe( 'Data Validation', () => { @@ -216,6 +225,37 @@ describe( 'PieSemiCircleChart', () => { } ); } ); + describe( 'Responsive wrapper', () => { + it( 'fills parent container (height:100%) by default', () => { + render( ); + const wrapper = screen.getByTestId( 'responsive-wrapper' ); + expect( wrapper ).toHaveStyle( { height: '100%' } ); + } ); + + it( 'constrains chart to 2:1 ratio from measured dimensions', () => { + // Mock returns width:400, height:200, so chart renders at 400×200 (2:1 ratio) + render( ); + const svg = screen.getByTestId( 'pie-chart-svg' ); + expect( svg ).toHaveAttribute( 'width', '400' ); + expect( svg ).toHaveAttribute( 'height', '200' ); + } ); + + it( 'constrains chart width when container height is shorter than 2:1 ratio', () => { + // If parent height is 100px, chart should be at most 200×100 (not 400×200) + const { useParentSize } = jest.requireMock( '@visx/responsive' ); + useParentSize.mockReturnValueOnce( { + parentRef: { current: null }, + width: 400, + height: 100, + } ); + render( ); + const svg = screen.getByTestId( 'pie-chart-svg' ); + // chartWidth = min(400, 100*2) = 200, chartHeight = 100 + expect( svg ).toHaveAttribute( 'width', '200' ); + expect( svg ).toHaveAttribute( 'height', '100' ); + } ); + } ); + describe( 'Interactive Legend', () => { test( 'filters segments when interactive legend is enabled and segment is toggled', async () => { const user = userEvent.setup(); From 3ce083e1a3dbe5a367459b87a8787aed34663112 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:27:43 +1300 Subject: [PATCH 03/10] changelog: add changelog entry for CHARTS-169 PieSemiCircleChart responsive fix Co-Authored-By: Claude Sonnet 4.6 --- ...hart-height-and-size-calculation-for-pie-semi-circle-chart | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/js-packages/charts/changelog/charts-169-fix-chart-height-and-size-calculation-for-pie-semi-circle-chart diff --git a/projects/js-packages/charts/changelog/charts-169-fix-chart-height-and-size-calculation-for-pie-semi-circle-chart b/projects/js-packages/charts/changelog/charts-169-fix-chart-height-and-size-calculation-for-pie-semi-circle-chart new file mode 100644 index 000000000000..9ca16111a8d0 --- /dev/null +++ b/projects/js-packages/charts/changelog/charts-169-fix-chart-height-and-size-calculation-for-pie-semi-circle-chart @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Charts: fix PieSemiCircleChart height and size calculations to be responsive by default, maintaining 2:1 width-to-height ratio. From 155464c4247d194c67487dbcb6f1bb47ab4dc381 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:33:13 +1300 Subject: [PATCH 04/10] Apply suggestion from @Copilot Correct docs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/charts/pie-semi-circle-chart/stories/index.api.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.api.mdx b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.api.mdx index d37ec710bd01..b92018851132 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.api.mdx +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.api.mdx @@ -13,7 +13,7 @@ Main component for rendering semi-circular pie charts. | Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | `data` | `DataPointPercentage[]` | - | **Required.** Array of data points to display | -| `width` | `number` | responsive | Width of the chart in pixels. When omitted, fills parent width (height is automatically half of width) | +| `width` | `number` | responsive | Width of the chart in pixels. When omitted, the chart will try to fill the parent width, but may reduce its width based on available height to maintain the 2:1 aspect ratio (height is automatically half of the final width) | | `thickness` | `number` | `0.4` | Thickness of the pie segments (0-1, where 1 is full thickness) | | `clockwise` | `boolean` | `true` | Direction of segment rendering | | `label` | `string` | - | Text displayed above the chart | From 0e6341b217a85945b883e9532c656eab709209e8 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:03:04 +1300 Subject: [PATCH 05/10] fix(charts): Constrain error-state SVG to respect height The invalid-data render path hardcoded width and ignored propHeight, so the error SVG could overflow a height-constrained container. Apply the same 2:1 aspect-ratio constraint used by the valid chart. Co-authored-by: Cursor --- .../pie-semi-circle-chart/pie-semi-circle-chart.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx index f1709900082a..836c58a6fa57 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx @@ -302,13 +302,12 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { const prefersReducedMotion = usePrefersReducedMotion(); if ( ! isValid ) { + const errorWidth = Math.min( propWidth || 400, ( propHeight || ( propWidth || 400 ) / 2 ) * 2 ); + const errorHeight = errorWidth / 2; + return (
- + { message } From 34abe99bf71e3e073f9050502ad4083ad0d4df13 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:04:11 +1300 Subject: [PATCH 06/10] refactor(charts): Update semi-circle stories for responsive default Stories were still using fixed widths and container overrides from the old sizing model. Now that the chart is responsive by default, most stories no longer need explicit width/containerWidth props. - Add height range control to Storybook argTypes - Remove redundant Responsiveness story (all stories are responsive) - Simplify composition and interactive legend examples - Use height prop instead of width where a size constraint is needed Co-authored-by: Cursor --- .../stories/index.stories.tsx | 125 ++++++------------ .../stories/tooltip.stories.tsx | 3 - 2 files changed, 40 insertions(+), 88 deletions(-) diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.stories.tsx b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.stories.tsx index 3b95175729e3..144063c13d36 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.stories.tsx +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.stories.tsx @@ -10,7 +10,7 @@ import { partialOsUsageData as data, themeArgTypes, } from '../../../stories'; -import { PieSemiCircleChart, PieSemiCircleChartUnresponsive } from '../index'; +import { PieSemiCircleChart } from '../index'; import type { Meta, StoryObj } from '@storybook/react'; type StoryArgs = ChartStoryArgs< React.ComponentProps< typeof PieSemiCircleChart > >; @@ -34,6 +34,14 @@ const meta: Meta< StoryArgs > = { step: 10, }, }, + height: { + control: { + type: 'range', + min: 100, + max: 1000, + step: 10, + }, + }, thickness: { control: { type: 'range', @@ -69,27 +77,27 @@ export const Default: Story = { export const FixedDimensions: Story = { render: args => ( - ), args: { ...Default.args, resize: 'none', - containerWidth: '600px', - containerHeight: '350px', - width: 400, + width: 600, + height: 300, }, parameters: { docs: { description: { story: - 'Semi-circle pie chart with fixed pixel dimensions. Use `PieSemiCircleChartUnresponsive` when you need precise control over the chart size.', + 'Semi-circle pie chart with fixed pixel dimensions. The chart will maintain a 2:1 width-to-height ratio within the provided dimensions.', }, }, }, @@ -125,46 +133,18 @@ export const WithLegend: Story = { export const WithCompositionLegend: Story = { render: args => ( -
-
-

Traditional Props-based Legend

- -
-
-

Composition API with Legend Component

- - - -
-
+ + + ), args: { data, - containerWidth: '900px', - containerHeight: '400px', }, argTypes: { legendInteractive: { @@ -184,29 +164,25 @@ export const WithCompositionLegend: Story = { export const InteractiveLegend: Story = { render: args => ( -
-

Interactive Semi-Circle Chart

+

Click legend items to show/hide segments. Percentages adjust automatically.

- -
+
), args: { data, - width: 400, }, parameters: { docs: { @@ -220,8 +196,6 @@ export const InteractiveLegend: Story = { export const CustomLegendPositioning: Story = { args: { - containerWidth: '600px', - containerHeight: '400px', thickness: 0.4, data: [ { @@ -262,22 +236,6 @@ export const CustomLegendPositioning: Story = { }, }; -export const Responsiveness: Story = { - args: { - ...Default.args, - containerWidth: '800px', - containerHeight: '500px', - }, - parameters: { - docs: { - description: { - story: - 'Demonstrates responsive resizing. The chart automatically maintains a 2:1 width-to-height ratio as the container is resized. Drag the bottom-right corner of the dashed container to resize.', - }, - }, - }, -}; - export const ErrorStates: Story = { render: () => (
@@ -311,15 +269,12 @@ export const ErrorStates: Story = {

Single Data Point

), - args: { - containerHeight: '600px', - }, parameters: { docs: { description: { @@ -347,7 +302,7 @@ export const CompositionAPI: Story = {

With Custom SVG Elements

With Custom Legend and HTML Content

= args => = Template.bind( {} ); From f2fd220336240f65ea7193d375d8641e021a7194 Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:16:06 +1300 Subject: [PATCH 07/10] style(charts): Remove redundant SCSS from semi-circle chart The flex/column properties are now handled by the Stack component, and margin declarations have no effect on SVG elements. Co-authored-by: Cursor --- .../pie-semi-circle-chart.module.scss | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss index e3b9d718cd2c..de49033a51ec 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss @@ -1,9 +1,4 @@ .pie-semi-circle-chart { - display: flex; - flex-direction: column; - text-align: center; - overflow: hidden; - // Fill parent when no explicit width/height provided &--responsive { height: 100%; @@ -22,13 +17,11 @@ } .label { - margin-bottom: 0; // Add space between label and pie chart - font-weight: 600; // Make label more prominent than note - font-size: 16px; // Set explicit font size + font-weight: 600; + font-size: 16px; } .note { - margin-top: 0; // Add space between pie chart and note - font-size: 14px; // Slightly smaller text for hierarchy + font-size: 14px; } } From 32000d47d995b260e844d1578cd87312ee474abc Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:53:29 +1300 Subject: [PATCH 08/10] refactor(charts): Extract DEFAULT_WIDTH and fix error-state sizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The error-state SVG ignored an explicit height prop, always defaulting to 400px width regardless. When only propHeight was provided (e.g. 400), the error rendered at 400×200 instead of the expected 800×400. Extracts the repeated `propWidth || 400` fallback into a DEFAULT_WIDTH constant and effectiveWidth variable, then reworks the error-state calculation so an explicit height correctly derives width via the 2:1 aspect ratio. Co-authored-by: Cursor --- .../pie-semi-circle-chart/pie-semi-circle-chart.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx index 836c58a6fa57..a262cdf03906 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx @@ -53,6 +53,7 @@ const renderDefaultPieSemiCircleTooltip = ( { }; const PAD_ANGLE = 0.03; // Padding between segments +const DEFAULT_WIDTH = 400; export interface PieSemiCircleChartProps extends BaseChartProps< DataPointPercentage[] > { /** @@ -301,8 +302,12 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { const prefersReducedMotion = usePrefersReducedMotion(); + const effectiveWidth = propWidth || DEFAULT_WIDTH; + if ( ! isValid ) { - const errorWidth = Math.min( propWidth || 400, ( propHeight || ( propWidth || 400 ) / 2 ) * 2 ); + const errorWidth = propHeight + ? Math.min( propWidth || propHeight * 2, propHeight * 2 ) + : effectiveWidth; const errorHeight = errorWidth / 2; return ( @@ -319,9 +324,9 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { // Calculate chart dimensions maintaining the 2:1 width-to-height ratio. // Use measured SVG wrapper dimensions to respect height constraints, falling back // to explicit props during initial render before measurement is available. - const availableWidth = svgWrapperWidth > 0 ? svgWrapperWidth : propWidth || 400; + const availableWidth = svgWrapperWidth > 0 ? svgWrapperWidth : effectiveWidth; const availableHeight = - svgWrapperHeight > 0 ? svgWrapperHeight : propHeight || ( propWidth || 400 ) / 2; + svgWrapperHeight > 0 ? svgWrapperHeight : propHeight || effectiveWidth / 2; // Constrain width so that height (= width / 2) never exceeds the available height const width = Math.min( availableWidth, availableHeight * 2 ); const height = width / 2; From 3b536a79de6626d0dbd227c5df928787553a42bb Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:13:08 +1300 Subject: [PATCH 09/10] docs(charts): Update semi-circle docs for responsive default The docs described responsive as opt-in ("omit width to be responsive") when the chart is now responsive by default. This was inconsistent with line, bar, and pie chart docs which all frame it as "fills parent container, use width/height to constrain." Rewrites the responsive section to match the shared pattern, fixes Source blocks to match their Canvas stories, adds the missing height prop to the API reference, and fixes a broken Responsiveness story reference. Co-authored-by: Cursor --- .../stories/index.api.mdx | 3 +- .../stories/index.docs.mdx | 341 +++++++++--------- 2 files changed, 166 insertions(+), 178 deletions(-) diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.api.mdx b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.api.mdx index b92018851132..26082d51b976 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.api.mdx +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.api.mdx @@ -13,7 +13,8 @@ Main component for rendering semi-circular pie charts. | Prop | Type | Default | Description | | ---- | ---- | ------- | ----------- | | `data` | `DataPointPercentage[]` | - | **Required.** Array of data points to display | -| `width` | `number` | responsive | Width of the chart in pixels. When omitted, the chart will try to fill the parent width, but may reduce its width based on available height to maintain the 2:1 aspect ratio (height is automatically half of the final width) | +| `width` | `number` | responsive | Width constraint in pixels. By default the chart fills its parent container. When provided, constrains the chart to this width while maintaining the 2:1 aspect ratio | +| `height` | `number` | responsive | Height constraint in pixels. By default the chart fills its parent container. When provided, the chart will reduce its width if needed so that height (= width / 2) does not exceed this value | | `thickness` | `number` | `0.4` | Thickness of the pie segments (0-1, where 1 is full thickness) | | `clockwise` | `boolean` | `true` | Direction of segment rendering | | `label` | `string` | - | Text displayed above the chart | diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.docs.mdx b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.docs.mdx index 50c5d90d2ad1..a6811fe5f363 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.docs.mdx +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.docs.mdx @@ -17,36 +17,33 @@ The PieSemiCircleChart component renders data as segments in a semi-circular arc language="jsx" code={ `import { PieSemiCircleChart } from '@automattic/charts'; -const data = [ - { - label: 'MacOS', - value: 30000, - valueDisplay: '30K', - percentage: 30, - }, - { - label: 'Linux', - value: 22000, - valueDisplay: '22K', - percentage: 22, - }, - { - label: 'Windows', - value: 48000, - valueDisplay: '48K', - percentage: 48, - }, -]; - -` } + const data = [ + { + label: 'MacOS', + value: 30000, + valueDisplay: '30K', + percentage: 5, + }, + { + label: 'Linux', + value: 22000, + valueDisplay: '22K', + percentage: 1, + }, + { + label: 'Windows', + value: 80000, + valueDisplay: '80K', + percentage: 2, + }, + ]; + + ` } /> The chart automatically validates data to ensure positive values and meaningful percentages, displaying error states for invalid data configurations. @@ -65,23 +62,12 @@ The simplest implementation requires only data with proper percentage values. Un ` } + code={ `` } /> ### Required Props @@ -103,12 +89,11 @@ Add `withTooltips` to enable hover interactions that display detailed informatio ` } + data={ data } + withTooltips={ true } + label="OS" + note="Windows +10%" + />` } /> ### Counter-Clockwise Direction @@ -118,11 +103,11 @@ Use the `clockwise` prop to control the rendering direction of segments: ` } + data={ data } + width={ 600 } + clockwise={ false } + label="Counter-clockwise Rendering" + />` } /> ### Different Thickness Values @@ -132,42 +117,44 @@ Adjust the visual weight of the chart using the `thickness` prop: + -// Thick ring (thickness: 0.8) -` } + // Thick ring (thickness: 0.8) + ` } /> ## Responsive Behavior -### Responsive Width - -The chart can be made responsive by omitting the `width` prop, allowing it to adapt to its container: +By default, charts **fill their parent container's dimensions** while maintaining a 2:1 width-to-height aspect ratio. The parent must have an explicit height: - + ` } + code={ `// Fill parent container (default) - parent needs explicit height +
+ +
+ + // Fixed dimensions - chart constrains to 2:1 ratio within these bounds + ` } /> -The responsive behavior maintains the 2:1 aspect ratio (width:height) and scales proportionally. +The chart always maintains a 2:1 width-to-height ratio. When both `width` and `height` are provided, the chart constrains to whichever dimension is more restrictive. Use `width` and/or `height` to constrain the chart to specific pixel dimensions. + + + +For more details on responsive behavior, see the [Responsive Design section](./?path=/docs/js-packages-charts-library-introduction--docs#responsive-design) in the introduction. ## Styling and Customization @@ -178,25 +165,25 @@ Segments automatically use theme colors, but you can override individual segment ` } + { + label: 'Primary', + value: 60, + percentage: 60, + color: '#3366CC', // Custom blue + }, + { + label: 'Secondary', + value: 40, + percentage: 40, + color: '#DC3912', // Custom red + }, + ]; + + ` } /> ### Label and Note Styling @@ -209,11 +196,11 @@ The chart includes built-in styling for labels and notes with appropriate typogr ` } + data={ data } + width={ 600 } + label="Primary Heading" + note="Secondary information or context" + />` } /> ## Theming Integration @@ -243,9 +230,9 @@ The Pie Semi Circle Chart component supports an optional entry animation that cr language="jsx" code={ `` } /> @@ -267,20 +254,20 @@ Control legend placement using alignment properties: + -// Vertical legend on the right -` } + // Vertical legend on the right + ` } /> ### Legend Shape Options @@ -321,18 +308,18 @@ The chart gracefully handles single data points, rendering a complete semi-circl ` } + { + label: 'Complete', + value: 100, + percentage: 100, + }, + ]; + + ` } /> ## Advanced Features @@ -344,25 +331,25 @@ Use the `valueDisplay` property to show formatted values in tooltips and legends ` } + { + label: 'Users', + value: 15000, + valueDisplay: '15K users', // Custom formatted display + percentage: 60, + }, + { + label: 'Revenue', + value: 10000, + valueDisplay: '$10K', // Currency formatting + percentage: 40, + }, + ]; + + ` } /> ### Chart Integration @@ -382,16 +369,16 @@ Semi-circle charts use the same data format as full pie charts, making migration + -// Semi-circle chart (uses width instead of size) -` } + // Semi-circle chart (uses width instead of size) + ` } /> ### Key Differences from Full Pie Charts @@ -408,19 +395,19 @@ Unlike full pie charts that require percentages to sum to exactly 100: From fc20a39ca822e8de92fcc770925fc3bea67f891b Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:26:05 +1300 Subject: [PATCH 10/10] feat(charts): Add containerHeight argument to ErrorStates story --- .../src/charts/pie-semi-circle-chart/stories/index.stories.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.stories.tsx b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.stories.tsx index 144063c13d36..c69847edee9c 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.stories.tsx +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/stories/index.stories.tsx @@ -275,6 +275,9 @@ export const ErrorStates: Story = {
), + args: { + containerHeight: 600, + }, parameters: { docs: { description: {