Skip to content

feat(chart): add responsive prop - #31

Open
rick-hup wants to merge 2 commits into
mainfrom
sour-quiet
Open

feat(chart): add responsive prop#31
rick-hup wants to merge 2 commits into
mainfrom
sour-quiet

Conversation

@rick-hup

@rick-hup rick-hup commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

What

Two related changes:

1. New responsive prop (aligns with Recharts 3.3+)

Recharts did not remove ResponsiveContainer; it added a responsive prop on the chart itself as a more flexible, CSS-driven alternative. This PR ports that:

  • Adds responsive?: boolean (default false) to all charts via the shared generateCategoricalChart factory.
  • When set, the chart's own root element fills its parent (width/height: 100%) and measures itself with a ResizeObserver — no ResponsiveContainer wrapper needed.
  • Measured pixel size flows through the existing ReportMainChartProps → setChartSize → redux chain, so downstream layout/selectors are untouched.
  • ResponsiveContainer is kept for backward compatibility.
<LineChart responsive :style="{ height: '300px' }" :data="data">
  <Line data-key="value" />
</LineChart>

Non-responsive charts keep their exact previous behavior (early bail on invalid size); responsive charts render the wrapper first so it can be measured, then gate the inner surface on a valid measured size.

Notes

  • aspect in responsive mode is delegated to CSS aspectRatio (no custom calc).
  • Parent needs a definite height (same caveat as Recharts / ResponsiveContainer).

Testing

  • New responsive-prop.spec.tsx (5 cases: 100% sizing, gated-until-measured, renders at measured size, updates on ResizeObserver, non-responsive regression).
  • Full suite: 62 files / 716 tests pass.
  • pnpm --filter vccs build passes; eslint clean on new/edited files.
  • Added Examples/ResponsiveProp story and a responsive section in the chart-size guide.

Summary by CodeRabbit

  • New Features

    • Added responsive sizing support for Vue charts, allowing charts to adapt to their container size.
    • Added examples showing fixed height, parent-sized, and aspect-ratio layouts.
    • Applied the new responsive behavior to Sankey, Sunburst, and Treemap charts.
  • Documentation

    • Updated the chart sizing guide with clearer sizing options and SSR/client-side usage notes.
  • Tests

    • Added coverage for responsive resizing and fixed-size rendering behavior.

…tsWrapper

Follows Recharts 3.3+ by adding a `responsive` prop to chart components: the
chart root fills its parent via CSS and self-measures with a ResizeObserver,
so no ResponsiveContainer wrapper is needed. Non-responsive behavior is
unchanged. Also renames the misnamed RechartsWrapper (React library name) to
ChartsWrapper, matching the ChartsSurface convention.

- generateCategoricalChart: new `responsive` prop, measured size drives layout
- ChartsWrapper: `responsive`/`onResize` props, 100% CSS sizing + ResizeObserver
- update Sankey/Sunburst/Treemap consumers to ChartsWrapper
- add responsive-prop tests and Examples/ResponsiveProp story
- document the `responsive` prop in the chart-size guide
@rick-hup rick-hup changed the title feat(chart): add responsive prop; rename RechartsWrapper to ChartsWrapper feat(chart): add responsive prop Jul 2, 2026
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rick-hup, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57531bfd-a33e-4c27-977a-e1509a8759dd

📥 Commits

Reviewing files that changed from the base of the PR and between 7ce723b and d664c01.

📒 Files selected for processing (2)
  • packages/vue/src/chart/ChartsWrapper.tsx
  • packages/vue/src/chart/__tests__/responsive-prop.spec.tsx
📝 Walkthrough

Walkthrough

Introduces a responsive prop for Vue chart components that measures the wrapper element via ResizeObserver and updates chart dimensions dynamically. RechartsWrapper is renamed to ChartsWrapper throughout chart components. Adds tests, a Storybook story, and documentation for the new responsive sizing behavior.

Changes

Responsive Chart Sizing

Layer / File(s) Summary
ChartsWrapper rename and responsive measurement
packages/vue/src/chart/ChartsWrapper.tsx
Renames RechartsWrapper to ChartsWrapper, adds a responsive prop, tracks wrapperEl, measures size via getBoundingClientRect/ResizeObserver, calls onResize, and conditionally applies 100%/fixed style.
generateCategoricalChart responsive integration
packages/vue/src/chart/generateCategoricalChart.tsx
Adds responsive prop to CategoricalProps, introduces responsiveSize/handleResize, computes effectiveWidth/effectiveHeight, adjusts invalid-size bailout logic, and gates rendering of chart subcomponents and ChartsWrapper wiring on valid size.
ChartsWrapper rename propagation
packages/vue/src/chart/Sankey.tsx, packages/vue/src/chart/SunburstChart.tsx, packages/vue/src/chart/Treemap.tsx, packages/vue/src/chart/__tests__/Treemap.spec.tsx
Updates imports and render output in Sankey, SunburstChart, and Treemap to use ChartsWrapper instead of RechartsWrapper, plus a matching test comment update.
Tests, story, and docs for responsive prop
packages/vue/src/chart/__tests__/responsive-prop.spec.tsx, packages/vue/src/chart/__stories__/ResponsiveProp.stories.tsx, docs/content/2.guides/05.chart-size.md
Adds a Vitest suite mocking ResizeObserver/bounding rects to verify responsive sizing and surface gating, a Storybook example, and documentation covering the responsive prop, ResponsiveContainer alternative, and SSR/ResizeObserver notes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ChartComponent
  participant ChartsWrapper
  participant ResizeObserver

  User->>ChartComponent: mount chart with responsive prop
  ChartComponent->>ChartsWrapper: render(responsive=true, onResize=handleResize)
  ChartsWrapper->>ChartsWrapper: measure via getBoundingClientRect
  ChartsWrapper->>ResizeObserver: observe(wrapperEl)
  ChartsWrapper->>ChartComponent: onResize(width, height)
  ChartComponent->>ChartComponent: update effectiveWidth/effectiveHeight
  ChartComponent-->>User: render Surface when size valid
  ResizeObserver-->>ChartsWrapper: size change detected
  ChartsWrapper->>ChartComponent: onResize(newWidth, newHeight)
  ChartComponent-->>User: re-render with updated dimensions
Loading

Possibly related PRs

  • unovue/vue-charts#18: Adds the initial chart-size documentation guide that this PR extends with the responsive prop and ResizeObserver/SSR notes.
  • unovue/vue-charts#27: Implements the Sankey/SunburstChart components that this PR updates to use ChartsWrapper instead of RechartsWrapper.
  • unovue/vue-charts#30: Introduces array-based style merging on the wrapper component that this PR builds on for responsive/fixed sizing styles.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a chart responsive prop.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sour-quiet

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying vue-charts with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7ce723b
Status: ✅  Deploy successful!
Preview URL: https://79765609.vue-charts.pages.dev
Branch Preview URL: https://sour-quiet.vue-charts.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/content/2.guides/05.chart-size.md (1)

20-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a live ::chart-demo:: for the new responsive section.

The responsive prop is presented as the recommended approach, but only code snippets are shown here, while the "alternative" ResponsiveContainer section below still includes a live ::chart-demo{}:: embed. Adding a live demo would keep the docs consistent and better showcase the recommended path.

As per coding guidelines, "Use MDC syntax ::chart-demo{src=\"...\"}:: to embed live demos in documentation."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/content/2.guides/05.chart-size.md` around lines 20 - 47, Add a live
chart demo to the new responsive sizing section so the recommended `responsive`
prop is showcased with the same MDC `::chart-demo{src="..."}::` pattern used
elsewhere in the docs. Update the markdown near the `responsive` heading to
include a demo embed alongside the existing examples, keeping it consistent with
the `ResponsiveContainer` section below.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/vue/src/chart/ChartsWrapper.tsx`:
- Around line 58-73: The ResizeObserver setup in ChartsWrapper is tied to a
one-time onMounted check, so changes to props.responsive after mount are
ignored. Move the observer lifecycle logic into a watch on props.responsive (and
wrapperEl if needed) so it starts when responsive becomes true and disconnects
when it becomes false, keeping the existing onMounted/onUnmounted cleanup
behavior aligned with the reactive prop.
- Around line 62-67: The initial measurement in ChartsWrapper.tsx is using
getBoundingClientRect() while the ResizeObserver callback reads contentRect, so
the first onResize value can differ from later updates when padding or borders
are present. Update the resize logic in the same flow that creates
resizeObserver so both measurements use the same box model, preferably by
observing wrapperEl.value with box: 'border-box' or by changing the initial read
to match contentRect.

---

Nitpick comments:
In `@docs/content/2.guides/05.chart-size.md`:
- Around line 20-47: Add a live chart demo to the new responsive sizing section
so the recommended `responsive` prop is showcased with the same MDC
`::chart-demo{src="..."}::` pattern used elsewhere in the docs. Update the
markdown near the `responsive` heading to include a demo embed alongside the
existing examples, keeping it consistent with the `ResponsiveContainer` section
below.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 634e7cda-2190-45ce-9391-cf0e4ab1b2e5

📥 Commits

Reviewing files that changed from the base of the PR and between ecf3d26 and 7ce723b.

📒 Files selected for processing (9)
  • docs/content/2.guides/05.chart-size.md
  • packages/vue/src/chart/ChartsWrapper.tsx
  • packages/vue/src/chart/Sankey.tsx
  • packages/vue/src/chart/SunburstChart.tsx
  • packages/vue/src/chart/Treemap.tsx
  • packages/vue/src/chart/__stories__/ResponsiveProp.stories.tsx
  • packages/vue/src/chart/__tests__/Treemap.spec.tsx
  • packages/vue/src/chart/__tests__/responsive-prop.spec.tsx
  • packages/vue/src/chart/generateCategoricalChart.tsx

Comment on lines +58 to +73
onMounted(() => {
if (!props.responsive || !wrapperEl.value) {
return
}
const { width, height } = wrapperEl.value.getBoundingClientRect()
props.onResize?.(width, height)
resizeObserver = new ResizeObserver((entries) => {
const { width: w, height: h } = entries[0].contentRect
props.onResize?.(w, h)
})
resizeObserver.observe(wrapperEl.value)
})
onUnmounted(() => {
resizeObserver?.disconnect()
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

responsive prop is only checked once at mount, not reactive.

The ResizeObserver setup/teardown is gated by props.responsive inside onMounted's closure, which runs a single time. If a consumer toggles responsive after the component mounts, the inline style will correctly switch (it's computed in the render function), but the observer will never start (if it was initially false) or never stop (if it was initially true), leaving the chart's measured size permanently stale. Guidelines recommend mapping effects with reactive dependencies to watch, not a one-shot lifecycle hook.

♻️ Suggested fix using `watch`
-    let resizeObserver: ResizeObserver | null = null
-    onMounted(() => {
-      if (!props.responsive || !wrapperEl.value) {
-        return
-      }
-      const { width, height } = wrapperEl.value.getBoundingClientRect()
-      props.onResize?.(width, height)
-      resizeObserver = new ResizeObserver((entries) => {
-        const { width: w, height: h } = entries[0].contentRect
-        props.onResize?.(w, h)
-      })
-      resizeObserver.observe(wrapperEl.value)
-    })
-    onUnmounted(() => {
-      resizeObserver?.disconnect()
-    })
+    let resizeObserver: ResizeObserver | null = null
+    function startObserving() {
+      if (!wrapperEl.value) return
+      const { width, height } = wrapperEl.value.getBoundingClientRect()
+      props.onResize?.(width, height)
+      resizeObserver = new ResizeObserver((entries) => {
+        const { width: w, height: h } = entries[0].contentRect
+        props.onResize?.(w, h)
+      })
+      resizeObserver.observe(wrapperEl.value)
+    }
+    function stopObserving() {
+      resizeObserver?.disconnect()
+      resizeObserver = null
+    }
+    watch(() => props.responsive, (responsive) => {
+      stopObserving()
+      if (responsive) startObserving()
+    }, { immediate: true })
+    onUnmounted(stopObserving)

As per coding guidelines, packages/vue/src/**/*.{ts,tsx} should "map React useState/useEffect to Vue ref/watch" when porting Recharts patterns; a prop-dependent effect like this should be reactive.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue/src/chart/ChartsWrapper.tsx` around lines 58 - 73, The
ResizeObserver setup in ChartsWrapper is tied to a one-time onMounted check, so
changes to props.responsive after mount are ignored. Move the observer lifecycle
logic into a watch on props.responsive (and wrapperEl if needed) so it starts
when responsive becomes true and disconnects when it becomes false, keeping the
existing onMounted/onUnmounted cleanup behavior aligned with the reactive prop.

Source: Coding guidelines

Comment on lines +62 to +67
const { width, height } = wrapperEl.value.getBoundingClientRect()
props.onResize?.(width, height)
resizeObserver = new ResizeObserver((entries) => {
const { width: w, height: h } = entries[0].contentRect
props.onResize?.(w, h)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does ResizeObserver.observe support a {box: 'border-box'} option to get border-box measurements directly?

💡 Result:

Yes, the ResizeObserver.observe method supports the box option, which allows you to specify whether the observer should track changes to the element's content-box (the default) or its border-box [1][2][3]. You can use it by passing an options object as the second argument to the observe method [2][3]: resizeObserver.observe(element, { box: 'border-box' }); When this option is set, the observer monitors the size of the element's border box as defined in CSS [1][3]. This option specifically determines which box the observer monitors for layout changes [3]. Note that the ResizeObserver API also provides additional options such as 'device-pixel-content-box' for tracking sizes in device pixels [2][4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the target file with line numbers
sed -n '1,220p' packages/vue/src/chart/ChartsWrapper.tsx | cat -n

Repository: unovue/vue-charts

Length of output: 7935


Align the resize observer with the initial measurement. getBoundingClientRect() returns the border box, but ResizeObserver defaults to contentRect; if props.style adds padding or a border, the first onResize value will differ from later updates. Use resizeObserver.observe(wrapperEl.value, { box: 'border-box' }) or make both paths measure the same box.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vue/src/chart/ChartsWrapper.tsx` around lines 62 - 67, The initial
measurement in ChartsWrapper.tsx is using getBoundingClientRect() while the
ResizeObserver callback reads contentRect, so the first onResize value can
differ from later updates when padding or borders are present. Update the resize
logic in the same flow that creates resizeObserver so both measurements use the
same box model, preferably by observing wrapperEl.value with box: 'border-box'
or by changing the initial read to match contentRect.

Replace the hand-rolled ResizeObserver + onMounted/onUnmounted lifecycle with
@vueuse/core's useResizeObserver, which owns the observer lifecycle and auto-
disconnects on unmount. Keep the initial getBoundingClientRect to avoid a
first-frame flash. Test targets the active (latest) observer instance since
useResizeObserver re-creates its observer when the target ref resolves.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant