Summary
A y scale with side: 'right' reserves no gutter when the chart's container is
dir="rtl". The axis draws at the right edge, the plot keeps the full width
underneath it, and the tick labels are painted across the plot with the grid
lines running through them.
Reproduced on main at 258ed39 (0.16.0), with a new vanilla example, listed
below. Also present in 0.15.0, the release side shipped in.
Repro
examples/rtl-axis-side/src/main.ts: one definition, mounted twice, once in an
ltr container and once in an rtl one. Nothing else differs.
import { defineChart, lineY } from '@tanstack/charts'
import { mountChart } from '@tanstack/charts/dom'
import { scaleLinear } from '@tanstack/charts/scales/linear'
import { scalePoint } from '@tanstack/charts/scales/point'
const data = [
{ month: 'Jan', revenue: 128000 },
{ month: 'Feb', revenue: 194500 },
{ month: 'Mar', revenue: 151250 },
{ month: 'Apr', revenue: 233900 },
]
const definition = defineChart({
marks: [lineY(data, { x: 'month', y: 'revenue' })],
scales: {
x: { scale: () => scalePoint<string>().padding(0.4) },
y: {
scale: scaleLinear,
nice: true,
grid: true,
side: 'right',
axis: { ticks: { format: (v: number) => v.toLocaleString('en-US') } },
},
},
})
for (const dir of ['ltr', 'rtl'] as const) {
const caption = document.createElement('p')
caption.textContent = `dir="${dir}", y scale side: "right"`
const host = document.createElement('div')
host.dir = dir
host.style.cssText = 'width:640px;outline:1px solid #ccc'
document.body.append(caption, host)
mountChart(host, { definition, height: 260, ariaLabel: `revenue ${dir}` })
}
Measured in the resulting SVGs, coordinates relative to each SVG's own left
edge, both 640px wide:
| container |
plot x-range |
y tick labels |
rendered text-anchor |
axis x |
ltr |
4 to 593 |
601 to 636 |
start |
600.67 |
rtl |
4 to 628 |
601 to 636 |
start |
636.42 |
The LTR row is the control: a 47px gutter, labels clear of the plot. The RTL row
reserves nothing, and the plot runs 27px under its own labels.
Where it comes from
createTickLabelCandidates in packages/charts-core/src/scene.ts:
const defaultAnchor =
guide.channel === 'y'
? positiveSide
? 'start'
: 'end'
: ...
That relation holds only while the container's inline direction is LTR. The
anchor reaches the DOM as SVG text-anchor (svg-renderer.ts), which resolves
against inline base direction, so under dir="rtl" a right-side axis labelled
start paints leftward, into the plot rather than away from it.
Everything downstream is then faithful to that. The measurement mirrors with it,
as it should: the same string measures actualBoundingBoxLeft = -1.2, Right = 35.3 under ltr and 34.5 / -0.4 under rtl. includeBoundsMargin
sees a box on the plot side of the axis line, so margin.right stays at the
inset, chart.width grows, the axis slides further right, and the layout
converges on the overlap above.
So the fix looks like it belongs at the anchor default rather than in the
margin pass: under an RTL container a right axis wants end and a left axis
wants start, the mirror of the current relation.
Scope
Confirmed for a y scale with side: 'right' in an RTL container. Not tested:
side: 'top', or an x axis with a rotated label, both of which pick their
anchor from the same physical-side reasoning and may share the defect.
Note on framing
This is about the physical side that shipped in 0.15.0 behaving differently in
the two directions, not about logical start / end semantics or automatic RTL
mirroring. Physical right should reserve the right gutter whatever the
container's direction is.
Happy to send the one-line patch plus the example above as a PR if that reading
is right.
Summary
A y scale with
side: 'right'reserves no gutter when the chart's container isdir="rtl". The axis draws at the right edge, the plot keeps the full widthunderneath it, and the tick labels are painted across the plot with the grid
lines running through them.
Reproduced on
mainat 258ed39 (0.16.0), with a new vanilla example, listedbelow. Also present in 0.15.0, the release
sideshipped in.Repro
examples/rtl-axis-side/src/main.ts: one definition, mounted twice, once in anltrcontainer and once in anrtlone. Nothing else differs.Measured in the resulting SVGs, coordinates relative to each SVG's own left
edge, both 640px wide:
text-anchorxltrstartrtlstartThe LTR row is the control: a 47px gutter, labels clear of the plot. The RTL row
reserves nothing, and the plot runs 27px under its own labels.
Where it comes from
createTickLabelCandidatesinpackages/charts-core/src/scene.ts:That relation holds only while the container's inline direction is LTR. The
anchor reaches the DOM as SVG
text-anchor(svg-renderer.ts), which resolvesagainst inline base direction, so under
dir="rtl"a right-side axis labelledstartpaints leftward, into the plot rather than away from it.Everything downstream is then faithful to that. The measurement mirrors with it,
as it should: the same string measures
actualBoundingBoxLeft = -1.2, Right = 35.3underltrand34.5 / -0.4underrtl.includeBoundsMarginsees a box on the plot side of the axis line, so
margin.rightstays at theinset,
chart.widthgrows, the axis slides further right, and the layoutconverges on the overlap above.
So the fix looks like it belongs at the anchor default rather than in the
margin pass: under an RTL container a
rightaxis wantsendand aleftaxiswants
start, the mirror of the current relation.Scope
Confirmed for a y scale with
side: 'right'in an RTL container. Not tested:side: 'top', or an x axis with a rotated label, both of which pick theiranchor from the same physical-side reasoning and may share the defect.
Note on framing
This is about the physical
sidethat shipped in 0.15.0 behaving differently inthe two directions, not about logical
start/endsemantics or automatic RTLmirroring. Physical
rightshould reserve the right gutter whatever thecontainer's direction is.
Happy to send the one-line patch plus the example above as a PR if that reading
is right.