Skip to content

[fix] st.metric with chart_data wrapping in horizontal containers#14110

Merged
sfc-gh-lwilby merged 3 commits intodevelopfrom
fix/gh-13785-metric-chart-wrapping
Feb 26, 2026
Merged

[fix] st.metric with chart_data wrapping in horizontal containers#14110
sfc-gh-lwilby merged 3 commits intodevelopfrom
fix/gh-13785-metric-chart-wrapping

Conversation

@sfc-gh-lwilby
Copy link
Copy Markdown
Collaborator

@sfc-gh-lwilby sfc-gh-lwilby commented Feb 24, 2026

Summary

Fixes #13785.

Test App.

st.metric elements with chart_data (sparkline charts) placed inside st.container(horizontal=True) could non-deterministically wrap to multiple rows. This was caused by a sizing race condition: the Vega-Lite chart renders asynchronously via useEffect + ResizeObserver, which changes the element's intrinsic size after the initial flex layout, causing a positive feedback loop of reflows and widening.

The fix conditionally applies LARGE_ELEMENT config (14rem min-width, matching other chart elements) when the metric has chart_data. Metrics without charts retain DEFAULT config — zero behavior change for the common case.

Behavior matrix:

Scenario Before After
Metric with chart in horizontal container DEFAULT (no min-width → race condition) LARGE_ELEMENT (14rem min-width → stable)
Metric without chart in horizontal container DEFAULT DEFAULT (unchanged)
Metric in vertical layout No flex applied No flex applied (unchanged)
Metric in st.columns Column sizing Column sizing (unchanged)

Test plan

  • Existing ElementNodeRenderer unit tests pass (4/4)
  • Existing Metric component unit tests pass (58/58)
  • E2E st_metric tests (metrics with charts in columns — unchanged behavior)
  • E2E st_layouts_container_various_elements (metrics without charts in horizontal — unchanged behavior)
  • Visual verification with reproduction app

Metrics with sparkline charts in `st.container(horizontal=True)` could
non-deterministically wrap to multiple rows due to a sizing race between
Vega-Lite async rendering and flex layout recalculation.

Conditionally apply `LARGE_ELEMENT` config (14rem min-width) when the
metric has `chart_data`, matching the behavior of other chart elements.
Metrics without charts retain `DEFAULT` config for zero behavior change.

Co-authored-by: Cursor <[email protected]>

Co-authored-by: lawilby <[email protected]>
@sfc-gh-lwilby sfc-gh-lwilby added the change:bugfix PR contains bug fix implementation label Feb 24, 2026
@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Feb 24, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 24, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-14110/streamlit-1.54.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-14110.streamlit.app (☁️ Deploy here if not accessible)

@sfc-gh-lwilby sfc-gh-lwilby added the impact:users PR changes affect end users label Feb 24, 2026
@sfc-gh-lwilby sfc-gh-lwilby changed the title Fix st.metric with chart_data wrapping in horizontal containers [fix] st.metric with chart_data wrapping in horizontal containers Feb 24, 2026
@sfc-gh-lwilby sfc-gh-lwilby marked this pull request as ready for review February 25, 2026 16:14
Copilot AI review requested due to automatic review settings February 25, 2026 16:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a frontend layout instability where st.metric with chart_data (sparkline) could wrap unpredictably inside st.container(horizontal=True) by ensuring metrics with charts use a larger minimum flex width.

Changes:

  • Detects when a metric has non-empty chartData.
  • Uses ElementContainerConfig.LARGE_ELEMENT for metrics with charts (keeps DEFAULT otherwise).
  • Refactors the metric case to avoid repeated proto casts.

Comment on lines +455 to +466
case "metric": {
const metricProto = node.element.metric as MetricProto
const hasChart =
metricProto.chartData && metricProto.chartData.length > 0
return (
<ElementContainer
node={node}
config={ElementContainerConfig.DEFAULT}
config={
hasChart
? ElementContainerConfig.LARGE_ELEMENT
: ElementContainerConfig.DEFAULT
}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

This change adds new layout behavior (switching to LARGE_ELEMENT for metrics with chartData) but there doesn’t appear to be unit/integration coverage asserting the container styles for metric-with-chart in a horizontal layout. Please add a test (similar to the Markdown auto-width integration tests) that renders a metric node with non-empty chartData under a horizontal FlexContext and asserts the resulting min-width / flex-basis behavior stays stable.

Copilot generated this review using guidance from repository custom instructions.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added three tests in ElementNodeRenderer.test.tsx verifying the conditional config: metric with chartDataLARGE_ELEMENT, empty chartDataDEFAULT, no chartDataDEFAULT. Uses vi.mock to wrap ElementContainer and assert the config prop directly.

sfc-gh-lwilby and others added 2 commits February 25, 2026 17:33
Verify that metrics with chartData use LARGE_ELEMENT config and
metrics without chartData use DEFAULT config.

Co-authored-by: Cursor <[email protected]>

Co-authored-by: lawilby <[email protected]>
Replace `any` types with proper `ElementContainerProps` and `Element`
types, and replace non-null assertions with throw guards.

Co-authored-by: Cursor <[email protected]>

Co-authored-by: lawilby <[email protected]>
@lukasmasuch lukasmasuch added the ai-review If applied to PR or issue will run AI review workflow label Feb 25, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Feb 25, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Consolidated Code Review

PR: [fix] st.metric with chart_data wrapping in horizontal containers (#14110)

Summary

This PR fixes a non-deterministic layout regression (#13785) where st.metric elements with chart_data (sparkline charts) could wrap unpredictably inside st.container(horizontal=True). The root cause is a sizing race condition: the Vega-Lite chart renders asynchronously via useEffect + ResizeObserver, which changes the element's intrinsic size after the initial flex layout.

The fix is minimal and surgical: in ElementNodeRenderer.tsx, the "metric" case now conditionally uses ElementContainerConfig.LARGE_ELEMENT (14rem min-width) when chartData is present, and retains ElementContainerConfig.DEFAULT for plain metrics. Three new unit tests cover the three key scenarios (chart data present, empty, absent).

Files changed: ElementNodeRenderer.tsx (+11/-6), ElementNodeRenderer.test.tsx (+106/-0)

Code Quality

Both reviewers agree the implementation is clean, minimal, and follows established patterns. The conditional config selection mirrors how other chart-containing elements are sized with LARGE_ELEMENT.

Minor observation (non-blocking): The expression metricProto.chartData && metricProto.chartData.length > 0 could potentially trigger the @typescript-eslint/prefer-optional-chain lint rule (set to "error" in eslint.config.mjs). However, this exact pattern is already used in the existing Metric.tsx component itself (lines 312-313 and 425), so the new code is consistent with the established codebase and unlikely to be flagged by the linter for this specific type.

Test Coverage

Both reviewers agree the three unit tests provide solid coverage of the decision logic:

  • Chart data present ([1, 2, 3, 4, 5]) -> LARGE_ELEMENT config
  • Empty chart data ([]) -> DEFAULT config
  • No chart data (protobuf default) -> DEFAULT config

Both reviewers note no new E2E test was added. They agree this is reasonable given the non-deterministic nature of the race condition bug (which would make E2E tests inherently flaky). One reviewer (gpt-5.3-codex-high) recommends adding a targeted E2E regression test as a non-blocking follow-up; the other (opus-4.6-thinking) accepts the unit-test-only approach as pragmatic.

Minor observation (non-blocking): The tests use getByTestId(...).toBeInTheDocument() which is technically redundant (getBy* already throws if not found; toBeVisible() is preferred). However, this matches the pre-existing test patterns in the same file.

Backwards Compatibility

Both reviewers agree there are no backwards-compatibility concerns:

Scenario Before After
Metric with chart in horizontal container DEFAULT (race condition) LARGE_ELEMENT (14rem min-width, stable)
Metric without chart anywhere DEFAULT DEFAULT (unchanged)
Metric in vertical/column layout No flex applied No flex applied (unchanged)

The LARGE_ELEMENT config only affects behavior in horizontal flex layouts where min-width matters. No API or protocol changes introduced.

Security & Risk

Both reviewers agree there are no security concerns. The change is purely presentational (CSS layout configuration) with no new data handling, network calls, or user input processing. Regression risk is minimal, scoped to a single case block gated by a simple boolean condition.

Accessibility

Both reviewers agree there is no accessibility impact. The change modifies only the CSS container configuration (min-width), not DOM structure, ARIA attributes, or interactive elements.

Reviewer Agreement Summary

Topic gpt-5.3-codex-high opus-4.6-thinking Consensus
Code quality Clean, minimal Clean, follows patterns Agree
Test coverage Good, suggests E2E follow-up Good, E2E impractical for race condition Agree (non-blocking)
Backwards compat No breaking changes No breaking changes Agree
Security No concerns No concerns Agree
Accessibility No regressions No impact Agree
Verdict APPROVED APPROVED APPROVED

No disagreements between reviewers. Both approved with only minor non-blocking observations.

Recommendations

  1. (Non-blocking) Consider adding a targeted E2E regression test for st.metric(..., chart_data=...) inside st.container(horizontal=True) if a reliable, non-flaky approach can be found.
  2. (Non-blocking) The chartData && chartData.length > 0 guard is consistent with existing code in Metric.tsx, but could be simplified to metricProto.chartData.length > 0 if protobuf3 repeated fields guarantee an empty array default.

Verdict

APPROVED: Both reviewers approve unanimously. The fix is well-scoped, low-risk, consistent with existing codebase patterns, and adequately tested. No critical or blocking issues were raised. Ready to merge.

Missing Reviews

All expected models submitted reviews:

  • gpt-5.3-codex-high: Submitted
  • opus-4.6-thinking: Submitted

Consolidated review by opus-4.6-thinking.


📋 Review by `gpt-5.3-codex-high`

Summary

This PR fixes a layout regression where st.metric(..., chart_data=...) could wrap unpredictably inside horizontal containers by assigning charted metrics a large flex/min-width container config.
The implementation in frontend/lib/src/components/core/Block/ElementNodeRenderer.tsx (around lines 455-466) is concise and targeted, and new unit tests in frontend/lib/src/components/core/Block/ElementNodeRenderer.test.tsx (around lines 215-277) verify config selection for chart/no-chart cases.

Code Quality

No blocking code-quality issues found.

  • The renderer change is minimal and localized to the metric switch branch in frontend/lib/src/components/core/Block/ElementNodeRenderer.tsx (around lines 455-470), reducing blast radius.
  • Logic is straightforward (hasChart check) and reuses existing ElementContainerConfig constants, which matches repo patterns.
  • Test updates are readable and focused, though they validate internal config wiring (mocked ElementContainer) rather than end-user layout behavior.

Test Coverage

Coverage is good at the unit level, with a small residual e2e gap.

  • Added unit tests in frontend/lib/src/components/core/Block/ElementNodeRenderer.test.tsx (around lines 215-277) cover:
    • chart data present -> LARGE_ELEMENT
    • empty chart data -> DEFAULT
    • chart data omitted -> DEFAULT
  • Existing e2e metric tests cover chart rendering in columns (e2e_playwright/st_metric.py + e2e_playwright/st_metric_test.py), but there is no dedicated automated e2e assertion for the exact regression scenario: charted metrics inside st.container(horizontal=True).

Backwards Compatibility

No API or protocol changes were introduced, and no breaking changes are apparent.

  • The change is frontend-only and conditional on charted metrics.
  • Metrics without chart_data keep previous behavior.
  • Existing usages of st.metric arguments remain unchanged.

Security & Risk

No security concerns identified.

  • Change does not touch auth, data handling, network boundaries, or unsafe rendering paths.
  • Main risk is visual/layout regression in edge container combinations; current unit tests reduce logic risk, but end-to-end layout race behavior is still only indirectly covered.

Accessibility

No accessibility regressions identified in this diff.

  • No interactive semantics, focus behavior, ARIA attributes, or keyboard handling were changed.

Recommendations

  1. Add a targeted e2e regression check for st.metric(..., chart_data=...) inside st.container(horizontal=True) to directly lock in the bug fix and reduce future layout-race regressions.

Verdict

APPROVED: The fix is well-scoped, low risk, and unit-tested; it is ready to merge, with a non-blocking recommendation to add a focused e2e regression test.


This is an automated AI review by gpt-5.3-codex-high.

📋 Review by `opus-4.6-thinking`

Summary

This PR fixes a non-deterministic wrapping bug (#13785) where st.metric elements with chart_data (sparkline charts) placed inside st.container(horizontal=True) could wrap to multiple rows. The root cause was a sizing race condition: the Vega-Lite chart renders asynchronously via useEffect + ResizeObserver, which changes the element's intrinsic size after the initial flex layout.

The fix is minimal and surgical: in ElementNodeRenderer.tsx, the "metric" case now conditionally uses ElementContainerConfig.LARGE_ELEMENT (14rem min-width) when chartData is present, and retains ElementContainerConfig.DEFAULT for metrics without charts. Three new unit tests cover the three key scenarios (chart data present, empty, absent).

Only two files are changed: ElementNodeRenderer.tsx and ElementNodeRenderer.test.tsx.

Code Quality

The implementation is clean and follows established patterns in the codebase. The conditional config selection mirrors how other chart-containing elements (e.g., "table", "audio", "video") are configured with LARGE_ELEMENT.

Minor observations:

  1. Optional chaining preference (ElementNodeRenderer.tsx, line 457-458): The expression metricProto.chartData && metricProto.chartData.length > 0 uses a && chain for property access. Per the @typescript-eslint/prefer-optional-chain rule documented in frontend/AGENTS.md, this should ideally be written as (metricProto.chartData?.length ?? 0) > 0 or simply metricProto.chartData.length > 0 (since protobuf3 repeated fields default to [], chartData will always be defined as an array). This may or may not trigger the linter depending on exact configuration, but is worth noting for consistency.

  2. Test query pattern (ElementNodeRenderer.test.tsx, lines 230, 250, 270): The tests use expect(screen.getByTestId("stElementContainer")).toBeInTheDocument(), which the AGENTS.md notes is redundant since getBy* already throws if not found. The preferred assertion is toBeVisible(). However, this pattern is consistent with the pre-existing tests in the same file (lines 166, etc.), so this is not a blocking concern.

  3. Implementation-detail testing: The tests mock ElementContainer and inspect the config prop passed to it. While the AGENTS.md recommends testing user behavior over implementation details, this is a pragmatic approach since the actual behavior change (CSS min-width in flex layout) is difficult to assert at the unit test level without real layout measurement. The approach is reasonable for this specific case.

Test Coverage

The three new unit tests provide solid coverage of the decision logic:

  • Chart data present (chartData: [1, 2, 3, 4, 5]): verifies LARGE_ELEMENT config is used
  • Empty chart data (chartData: []): verifies DEFAULT config is used
  • No chart data (default protobuf): verifies DEFAULT config is used

Each test also validates the expected minStretchWidth value, adding depth beyond just config identity checks.

No new E2E test was added. The PR description notes that existing E2E tests (st_metric, st_layouts_container_various_elements) cover related scenarios. Given that the bug is a non-deterministic race condition, it would be inherently flaky to test in E2E. The unit test approach is reasonable, and the author verified the fix visually with a reproduction app.

Backwards Compatibility

The change is fully backwards compatible:

Scenario Before After
Metric with chart in horizontal container DEFAULT (race condition) LARGE_ELEMENT (stable, 14rem min-width)
Metric without chart anywhere DEFAULT DEFAULT (unchanged)
Metric in vertical/column layout No flex applied No flex applied (unchanged)

The LARGE_ELEMENT config only affects behavior in horizontal flex layouts (where min-width matters). In standard vertical layouts or st.columns, the min-width has no practical effect. There is no risk of breaking existing apps.

Security & Risk

No security concerns. The change is purely presentational (CSS layout configuration) with no new data handling, network calls, or user input processing.

Regression risk is minimal — the change is scoped to a single case block in the element renderer, gated by a simple boolean condition derived from existing protobuf data.

Accessibility

No accessibility impact. The change modifies only the CSS container configuration (min-width), not the rendered DOM structure, ARIA attributes, or interactive elements.

Recommendations

  1. Consider simplifying the guard clause: Since protobuf3 repeated fields always default to an empty array, metricProto.chartData will never be undefined or null in practice. The check could be simplified to metricProto.chartData.length > 0. If defensive coding is preferred, use optional chaining: (metricProto.chartData?.length ?? 0) > 0 to align with the project's prefer-optional-chain rule.

Verdict

APPROVED: Clean, minimal, and well-tested fix for a non-deterministic layout race condition. The approach is consistent with existing codebase patterns and carries no backwards-compatibility or security risk.


This is an automated AI review by opus-4.6-thinking.

Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@sfc-gh-lwilby
Copy link
Copy Markdown
Collaborator Author

On the e2e test: I did consider this as well. The layouts area has many possible configurations that could be tested with snapshots. When the feature shipped, I added tests that cover some common patterns, but it's not everything. I think a good next step would be to align the e2e tests with what we are guiding agents to do in the skills. I will follow up on this separately.

@sfc-gh-lwilby sfc-gh-lwilby merged commit 8a591ee into develop Feb 26, 2026
57 of 58 checks passed
@sfc-gh-lwilby sfc-gh-lwilby deleted the fix/gh-13785-metric-chart-wrapping branch February 26, 2026 07:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:bugfix PR contains bug fix implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

st.metric with chart_data wraps unexpectedly in horizontal containers

3 participants