Skip to content

domainpad: pixel spacing inside axis domains - #7965

Open
SharadhNaidu wants to merge 13 commits into
plotly:mainfrom
SharadhNaidu:domainpad
Open

SharadhNaidu wants to merge 13 commits into
plotly:mainfrom
SharadhNaidu:domainpad

Conversation

@SharadhNaidu

@SharadhNaidu SharadhNaidu commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

adds domainpad to cartesian axes: space in pixels inside the edges of domain. closes #7835, and gives plotly/plotly.py#5606 the attribute that it needs.

why

domain is a fraction of the plot area, so the gap between stacked subplots changes with the figure size. a subplot title has a fixed height in pixels. on a short figure, the title overlaps the subplot above. plotly.py cannot fix this, because make_subplots does not know the rendered height.

this follows approach a from #7835 (comment):

yaxis2: {domain: [0.548, 0.665], domainpad: {top: 26}},
annotations: [{text: 'Walking', xref: 'x2 domain', x: 0.5, xanchor: 'center', yref: 'y2 domain', y: 1, yanchor: 'bottom', showarrow: false}]
before, titles overlap after, titles in reserved space

how

  • ax.setScale() takes the padding from _offset and _length. ax.domain keeps the user value.
  • anything that reads _offset and _length follows the padding with no change: the plot area, axis lines, gridlines, clip paths, dragboxes, and <axis> domain references.
  • x axes coerce left and right. y axes coerce top and bottom. overlaying axes use the padding of the axis under them.
  • if the padding does not fit, the two pads shrink by the same factor, and the plot area keeps at least 2px. doAutoMargin uses the same method for margins.
  • with scaleanchor and constrain: 'domain', updateDomain scales only the unpadded part of the span. thus the scale ratio stays exact.

these places build the plot rect from domain, so they now add the padding:

  • scattergl viewport
  • splom cells and the regl grid
  • rangeslider width and position
  • hover spikes with spikemode: 'across', which now stop at the plot area and do not cross a title

with no domainpad, all output is the same as before, to the last bit.

scope

cartesian axes only. polar, ternary, geo, map, smith and the domain traces convert their domains in their own code. plotly/plotly.py#5606 needs only cartesian axes. paper-referenced items, such as the legend, the rangeselector and xref: 'paper' annotations, do not move.

tests

  • axes: axis domainpad covers defaults, coercion, each side, the clamp, overlaying axes, constraints, the rangeslider, many axis types, and the no-op case
  • hover_spikeline: 'across' spikes stop at the padded plot area. this test fails without the fix.
  • scattergl: the regl viewport matches the padded plot area
  • splom: the cells and the regl grid match the padded plot areas
  • image mock axes_domainpad. the baseline must come from the baselines-default-diff ci artifact.

questions

  1. domain is no longer the only input to the plot rect. the alternative is to fold the padding into ax.domain at draw time. that change would leave scattergl and splom alone, but fullLayout would then show padded domain values. do you prefer that approach?
  2. x and y axes share layout_attributes.js, so the schema shows all four keys on the two axis letters. each axis coerces only its two keys. do you prefer separate attribute objects?

`domain` is a plot fraction, so the gap it leaves between subplots grows and
shrinks with the figure while a subplot title does not. domainpad reserves a
fixed number of pixels inside the domain edges instead, resolved at draw time.

Applied in ax.setScale to _offset and _length rather than to ax.domain, so the
domain keeps meaning what the user typed and anything referenced to
`<axis> domain` follows the padded area without new drawing code.

Along with it:

- scattergl, splom and the rangeslider rebuilt the plot rect from `domain` and
  so did not see the pad; they now read _offset and _length
- pads that do not fit are backed off together, since setScale already throws
  on a negative length
- updateDomain scales the padded length rather than the raw span, so a
  scaleanchor ratio stays exact when the constraint shrinks a padded axis

Refs plotly#7835
Removed attribution from the commit message.
@SharadhNaidu SharadhNaidu changed the title domainpad: pixel spacing inside axis domains (FIX)domainpad: pixel spacing inside axis domains Aug 14, 2026
@SharadhNaidu SharadhNaidu changed the title (FIX)domainpad: pixel spacing inside axis domains domainpad: pixel spacing inside axis domains Aug 14, 2026
The three places that build the plot rect from `domain` were changed to read
_offset and _length instead. That round trips through pixel space, and the
result is not exactly the value it started from: splom cell domains came back
about one ulp out, which moved rasterised markers enough to fail the
splom_*-nodiag image baselines even though no padding was involved.

Add the resolved padding to the original expressions instead, and expose it as
ax._padStart / ax._padEnd for those callers. Adding a literal zero is exact, so
an unpadded plot now produces the same numbers it always did.

Also list domainpad in swapAxisGroup's noSwapAttrs, next to domain: its keys are
named for screen edges, so swapping x and y would need them remapped.
@SharadhNaidu
SharadhNaidu marked this pull request as ready for review August 19, 2026 02:13
@robertclaus
robertclaus requested a review from emilykl August 19, 2026 19:22
@SharadhNaidu

Copy link
Copy Markdown
Contributor Author

@emilykl apologies if i am violating any principles by directly tagging you , but could we close this issue ?

@robertclaus
robertclaus requested review from KoolADE85 and removed request for emilykl September 15, 2026 20:10
@robertclaus robertclaus assigned KoolADE85 and unassigned emilykl Sep 15, 2026
@SharadhNaidu

Copy link
Copy Markdown
Contributor Author

any updates on this PR ?

@camdecoster

Copy link
Copy Markdown
Contributor

@emilykl apologies if i am violating any principles by directly tagging you , but could we close this issue ?

Sorry that this got dropped. What issue are you asking to close?

@SharadhNaidu

Copy link
Copy Markdown
Contributor Author

oh my bad i meant the PR and the issue i am talking about is #7835 .

@camdecoster

Copy link
Copy Markdown
Contributor

You want to close the PR?

@SharadhNaidu

Copy link
Copy Markdown
Contributor Author

@camdecoster

no,sorry for the confusion , I don't want to close it. I meant i would like the PR to be reviewed so that , once it's merged, it closes #7835.

I'm also pushing a few updates today the regenerated TypeScript types that the generated types drift check is flagging, a fix so spikemode 'across' spikes stop at the padded plot area , tests for the scattergl and splom code paths , and an image mock. I'll update the description when that's in. the API questions at the bottom are still the main thing it would help if i get a input on.

@camdecoster

Copy link
Copy Markdown
Contributor

Understood. I'll check with my team and follow up with you.

@SharadhNaidu

Copy link
Copy Markdown
Contributor Author

thanks mate!

updateDomain multiplied the domain by (newSpan / inputSpan). Without padding,
that ratio is 1 / factor, but the multiply can change the last bit of the
result, so a plot with no domainpad could move by a tiny amount.

Divide by `factor` as main does, and change the divisor only when the axis
has padding. Without domainpad, the result is now the same as on main.
… and types

The comments and attribute descriptions now follow the Simplified Technical
English rules in AGENTS.md. Lines that this PR touches now use const, let and
arrow functions. There is no change in behavior.

The regenerated schema.d.ts and plot-schema.json fix the
generated-types-drift check, which failed because the types did not include
domainpad.
Spikes with spikemode 'across' got their span from the counter axis domains,
so with domainpad they crossed the reserved space, for example a subplot
title. Get the span from the domain minus the padding of each counter axis.

Without domainpad the span is the same as before. A free axis still extends
the span to its position. The new test fails without this fix.
scattergl and splom build their regl viewports from `domain`, so they add the
padding in their own code. These tests make sure that the viewport, the splom
cells and the regl grid match the padded plot area.
The mock shows four subplots with different domainpad sides, so a change in
the padded plot area shows in the image tests. The baseline image must come
from the baselines-default-diff CI artifact.
An earlier commit removed the "with thanks to" credit by mistake. The draftlog
convention includes it.
# Conflicts:
#	test/jasmine/tests/hover_spikeline_test.js
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.

[FEATURE]: pixel-based spacing for subplot domains (so titles can't overlap the plot above)

4 participants