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. 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/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..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,21 +1,27 @@ .pie-semi-circle-chart { - display: flex; - flex-direction: column; - text-align: center; - gap: 20px; + // Fill parent when no explicit width/height provided + &--responsive { + height: 100%; + width: 100%; + } - &--legend-top { - flex-direction: column-reverse; + // 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 { - 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; } } 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..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 @@ -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'; @@ -52,10 +53,13 @@ const renderDefaultPieSemiCircleTooltip = ( { }; const PAD_ANGLE = 0.03; // Padding between segments +const DEFAULT_WIDTH = 400; 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 +161,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 +184,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 >(); @@ -295,10 +302,17 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { const prefersReducedMotion = usePrefersReducedMotion(); + const effectiveWidth = propWidth || DEFAULT_WIDTH; + if ( ! isValid ) { + const errorWidth = propHeight + ? Math.min( propWidth || propHeight * 2, propHeight * 2 ) + : effectiveWidth; + const errorHeight = errorWidth / 2; + return (
- + { message } @@ -307,12 +321,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 : effectiveWidth; + const availableHeight = + 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; - // 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 +347,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 +492,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..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` | `400` | Width of the chart in pixels (height is automatically half of 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 | @@ -28,6 +29,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.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: 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..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 @@ -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', @@ -51,14 +59,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', + width: 600, + height: 300, + }, + parameters: { + docs: { + description: { + story: + 'Semi-circle pie chart with fixed pixel dimensions. The chart will maintain a 2:1 width-to-height ratio within the provided dimensions.', + }, + }, + }, }; export const Animation: Story = { @@ -91,51 +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', }, argTypes: { legendInteractive: { @@ -155,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: { @@ -191,9 +196,6 @@ export const InteractiveLegend: Story = { export const CustomLegendPositioning: Story = { args: { - containerWidth: '600px', - containerHeight: '350px', - resize: 'none', thickness: 0.4, data: [ { @@ -234,20 +236,6 @@ export const CustomLegendPositioning: Story = { }, }; -const responsiveArgs = { ...Default.args, resize: 'both' as const }; -delete responsiveArgs.width; -export const Responsiveness: Story = { - args: responsiveArgs, - parameters: { - docs: { - description: { - story: - 'Semi-circle pie chart with responsive behavior. Uses width prop for unified width/height handling.', - }, - }, - }, -}; - export const ErrorStates: Story = { render: () => (
@@ -281,14 +269,14 @@ export const ErrorStates: Story = {

Single Data Point

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

With Custom SVG Elements

With Custom Legend and HTML Content

= args => = Template.bind( {} ); diff --git a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx index 7f7cc8ef9f04..eb8519f5db54 100644 --- a/projects/js-packages/charts/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +++ b/projects/js-packages/charts/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx @@ -3,6 +3,15 @@ import userEvent from '@testing-library/user-event'; import { GlobalChartsProvider } from '../../../providers'; import PieSemiCircleChart from '../pie-semi-circle-chart'; +// Mock useParentSize so the responsive wrapper returns predictable dimensions in tests +jest.mock( '@visx/responsive', () => ( { + 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(); 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. */