Skip to content

[charts] Stabilize WebGL bar rendering at sub-pixel widths#22678

Merged
JCQuintas merged 4 commits into
mui:masterfrom
JCQuintas:feat/webgl-bar-fractional-pixel-fixes
Jun 5, 2026
Merged

[charts] Stabilize WebGL bar rendering at sub-pixel widths#22678
JCQuintas merged 4 commits into
mui:masterfrom
JCQuintas:feat/webgl-bar-fractional-pixel-fixes

Conversation

@JCQuintas

Copy link
Copy Markdown
Member

Follow-up to #22607. Splits the sub-pixel rendering fixes out of the range-bar PR so the renderer changes can land on their own.

Problem

The WebGL bar renderer emits one rect per bar at the SVG-coord positions. At aggressive zoom-out the bars become sub-pixel:

  • Multiple bars share a pixel column. The rasterizer drops quads whose centers happen to fall between two pixel centers, producing visible moire stripes.
  • The natural `barGapRatio` gap between bands shrinks below a pixel. The rasterizer leaves background showing through, producing dark hairlines that flicker as the user zooms.
  • The fractional half-size oscillates by a fraction of a pixel on each zoom step, so adjacent bars visibly accordion against each other.

Fix

Three knobs in `useWebGLBarLikePlotData`:

  • `GAP_FILL_THRESHOLD_PX = 1` -- when the natural gap drops below a pixel, expand each bar's band-axis half-size out to half the pitch so adjacent quads meet exactly.
  • `MIN_BAND_HALF_SIZE_PX = 0.5` -- floor on the rendered band-axis half-size so quads can't fall entirely between two pixel centers.
  • `Math.round` on the probed bar size so the rendered half-size stays stable across zoom steps.

The pitch is derived from the first two visible bars in the series and divided by their `dataIndex` gap, so a null bar between the probes doesn't inflate the pitch to two bandwidths.

`hidden` is series-level (mirrored onto every bar by `useBarPlotData`), so the per-bar hidden check moves to a single peek up front.

Adds three knobs to the WebGL bar data hook so the renderer survives
dense data and aggressive zoom-out without producing the moire, dark
hairlines, and accordion oscillation the per-bar emission used to
ship at sub-pixel widths:

- GAP_FILL_THRESHOLD_PX: when the natural barGapRatio gap drops below
  one pixel, expand each bar's band-axis half-size to half the pitch
  so adjacent quads meet exactly. Kills the dark hairlines the
  rasterizer leaves when pixel centers land in the gap.
- MIN_BAND_HALF_SIZE_PX: floor on the rendered band-axis half-size so
  quads can't fall between two pixel centers and get culled by the
  rasterizer at extreme zoom-out.
- Math.round on the probed bar size so the rendered half-size stays
  stable across zoom steps instead of oscillating around its target.

The pitch is derived from the first two visible bars in the series
and divided by their dataIndex gap so a null bar between the probes
doesn't inflate the pitch to two bandwidths.

Hidden state is series-level (mirrored onto every bar by
useBarPlotData) so the per-bar hidden check moves to a single peek
that skips the whole series.
@JCQuintas
JCQuintas requested a review from alexfauquette as a code owner June 3, 2026 14:19
@JCQuintas JCQuintas added plan: Premium Impact at least one Premium user. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. scope: charts Changes related to the charts. labels Jun 3, 2026
@JCQuintas JCQuintas self-assigned this Jun 3, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Jun 3, 2026

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-22678--material-ui-x.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 🔺+380B(+0.06%) 🔺+165B(+0.09%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes

Performance

Total duration: 1,867.43 ms +129.23 ms(+7.4%) | Renders: 67 (+0) | Paint: 2,671.71 ms +161.53 ms(+6.4%)

No significant changes — details


Check out the code infra dashboard for more information about this PR.

Comment on lines +236 to +247
// Consider the natural barGapRatio gap to be zero when it's below the
// threshold. The bar's rendered band-axis half-size is padded out to
// half the pitch so adjacent quads meet exactly, killing the dark
// hairlines the rasterizer leaves where pixel centers land in the gap.
// The MIN_BAND_HALF_SIZE_PX floor keeps very thin bars from being
// dropped by the rasterizer at extreme zoom-out.
const gap = pitch - barSize;
const fillGap = gap > 0 && gap < GAP_FILL_THRESHOLD_PX;
let bandHalfRender = fillGap ? pitch * 0.5 : barSize * 0.5;
if (bandHalfRender < MIN_BAND_HALF_SIZE_PX) {
bandHalfRender = MIN_BAND_HALF_SIZE_PX;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Took me some time to get where the issue was

When you zoom, there is a small range where the renderer osciliate between "fill gap" and "do not fill gap"

It's not a bug, it's because the rounding of the bar width create discontinuities
Image

A solution can be to pick the linear lower bound of the gap which is const gapLowerBound = pitch - rawBarSize - 0.5;

Yes it's just removing 0.5 😅

Image

In this screen shot, $p$ is ration barWidth/pitch

By the way what you call pitch is a step in d3

Comment on lines +82 to +83
// Floor to keep thin quads from falling between pixel centers and getting
// culled at extreme zoom-out.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To clarify this 0.5 does not come from no-where. It comes from x-0.5<round(x)<x+0.5

If we used floor or ceil to approximate the bar size we should pick 0, or -1

@JCQuintas
JCQuintas enabled auto-merge (squash) June 5, 2026 07:23
@JCQuintas
JCQuintas merged commit c6de633 into mui:master Jun 5, 2026
21 checks passed
@JCQuintas
JCQuintas deleted the feat/webgl-bar-fractional-pixel-fixes branch June 5, 2026 07:37
mbrookes pushed a commit to mbrookes/mui-x that referenced this pull request Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plan: Premium Impact at least one Premium user. scope: charts Changes related to the charts. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants