True-to-scale rendering¶
Scanpath Studio reproduces the experiment's stimulus true to scale — the word boxes, fixations, and reading text keep the same physical proportions the reader actually saw, at any display size. This page explains the model so you can reason about font sizes, the px-vs-pt question, and why the spatial plot takes a special render path. It's a reference for contributors and power users; nothing here is required for day-to-day use.
Data space vs. screen space¶
Everything the eye-tracker recorded lives in monitor pixels — the data
space. A word's bounding box, a fixation's (x, y), the stimulus image: all are
positioned in the same coordinate system the experiment screen used (e.g. a
1920×1080 monitor).
The on-screen plot is a separate screen space. The figure is drawn at a fixed,
equal-aspect size (Plotly scaleratio=1, so one data unit is the same number of
screen pixels on both axes), capped to a research-display-friendly box
(plots._DISPLAY_MAX_WIDTH / _DISPLAY_MAX_HEIGHT), then the whole block is
CSS-scaled uniformly to fill the column. The bridge between the two spaces is a
single number:
display_scale = screen pixels per data unit
= min(fitted_w / x_span, fitted_h / y_span) # plots._display_scale
Because the aspect ratio is locked, x and y share this scale; the min keeps
rounding from ever pushing text or markers past their boxes.
Sizing the reading text¶
The experiment's font is a fixed size in monitor pixels, so to stay true-to-scale
the label font is computed in data pixels and multiplied by display_scale
into screen pixels (plots._word_label_font_px). Two modes feed the data-pixel
size:
-
Scale text to boxes (default). One line of text is budgeted to fill
1 / line_spacingof the line pitch — the median line-to-line distance measured from the data (plots._line_pitch), not the raw box height. The two differ for corpora whose AOI boxes hug the glyph (e.g. MultiplEYE): there the line slot is much taller than a box, and the pitch is the right budget. For OneStop the boxes tile the lines (pitch == height) and the defaultline_spacing = 3(one blank line above and below) reduces to the familiarheight / 3. The size is then capped so the longest word still fits its box width (plots._width_fit_font); the smaller of the height- and width-fits wins, so text never collides or overflows. -
Manual font. When box geometry is missing or you turn scaling off, the font control's value is treated as the real monitor font size and scaled the same way.
Script-aware width fitting¶
Word boxes hug their text, so a box's width equals the sum of its glyph advances.
plots._width_fit_font recovers the em (≈ the font size) as box_width / Σ
advances, where each glyph advances 1 em for full-width CJK and a smaller
latin_advance em for half-width Latin. Doing this per word — not with one global
aspect — keeps a CJK word boxed next to a half-width Latin URL from being shrunk
to the narrow run. For an all-Latin corpus it reduces exactly to
(box_width / n_chars) / aspect.
Datasets that ship their real typeface¶
Inferring the font from geometry is a good default, but a dataset that knows its
own typography can hand it over directly. MultiplEYE reads FONT_SIZE + FONT
from the stimulus config and stamps stimulus_font_px / stimulus_font_family
onto the frames; the app then snaps the font controls to the exact size and a
CJK-capable family (and turns scale-to-boxes off) on a source switch, so the
labels match the stimulus precisely rather than relying on inference.
Pixels, not points¶
Font-size controls are in pixels (px), not points (pt)
Every font-size control in the app — the base label size, order-number size, colour-bar tick size — is in pixels. Stimulus typography, however, is usually specified in points. The two are not interchangeable: a point is a physical unit (1 pt = 1/72 inch), a pixel is a screen unit, and the conversion depends on the display's DPI:
So "18 pt" is only "18 px" on a 72-DPI display; on a 96-DPI monitor it is
24 px. If you're trying to reproduce an original stimulus font exactly, convert
through the experiment's DPI — or, better, use a dataset that stamps its real
stimulus_font_px (see above) and let the app snap to it. Folding the
experimental-setup values (screen resolution, viewing distance, DPI, stimulus
font pt) into the display settings so this conversion is automatic is tracked
as a roadmap item.
Why the spatial plot needs a special render path¶
The true-to-scale text only holds if the figure is embedded at its exact pixel
size. Rendering the spatial plots with st.plotly_chart pins the chart width
to the column while keeping the layout height, which re-lays-out Plotly to an
unknown scale and leaves the px-sized word labels mismatched to the boxes. So the
single-trial, animation, and comparison plots all go through
tabs._render_true_scale_chart, which embeds the figure's own HTML at its exact
size and CSS-scales the whole block uniformly to the column width. Static image
export bumps the raster scale to stay crisp; SVG/PDF are vector and need no bump.
If you add a new spatial figure, keep it on _render_true_scale_chart — never
st.plotly_chart — or the reading text will drift from the boxes.