Coverage for scanpath_studio/session_keys.py: 100%
245 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-07 21:10 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-07 21:10 +0000
1"""Session-state keys that carry an **external contract** — a wire format.
3These strings are not local variable names. Each one appears in at least one of
4three places that outlive the running process:
6* the **deep link / Share URL** (``url_state._URL_PRESETS`` on the read side,
7 ``url_state._build_share_query`` on the write side) — links live in other
8 people's bookmarks, papers, issue trackers and embedded review apps;
9* the **settings and setup files** (🔗 Share → File, written by
10 ``tabs._build_studio_config`` and read by ``url_state._restore_plot_config``;
11 ✏️ Edit dataset → Download setup file, written by ``wizard._wizard_setup_config``) —
12 files sit on disk for months;
13* the **pre-widget seeding** both of the above rely on: values are written into
14 ``st.session_state`` *before* the widget exists, so the key is the only thing
15 connecting the stored value to the control it drives.
17**Renaming one of these strings breaks existing share links and saved configs,
18and nothing fails loudly** — the link still opens, the config still restores,
19they just silently drop the setting. That is why they are pinned here and in
20``tests/test_session_key_contract.py``: a rename now fails a test instead of a
21user's old link.
23Scope is deliberately narrow (ENG-6). This module does **not** try to name all
24~270 ``session_state`` keys in the app — a key with no external contract is just
25a local variable and can be renamed freely. Only the contract keys live here.
27Adding a key to the wire format? Add the constant **and** put it in the frozen
28grouping below in the same commit; the contract test tells you so by name.
29Changing the *meaning* or encoding of an existing key is the same kind of break
30as renaming it — take a new key instead.
31"""
33from __future__ import annotations
35from collections.abc import Mapping
36from types import MappingProxyType
38# ---------------------------------------------------------------------------
39# Visualization settings — the `global_*` keys the plot rail's widgets own.
40# Every one of these round-trips through BOTH the share link and the saved
41# config (the recording setup below the divider only since EXP-19).
42# ---------------------------------------------------------------------------
43GLOBAL_SHOW_WORDS = "global_show_words"
44# The ⬚ Word boxes section's style: outline + its opacity, fill + its opacity.
45GLOBAL_WORD_BOX_COLOR = "global_word_box_color"
46GLOBAL_WORD_BOX_LINE_OPACITY = "global_word_box_line_opacity"
47GLOBAL_WORD_BOX_FILL_COLOR = "global_word_box_fill_color"
48GLOBAL_WORD_BOX_FILL_OPACITY = "global_word_box_fill_opacity"
49GLOBAL_SHOW_LABELS = "global_show_labels"
50# UX-128: the 📄 Stimulus section's master switch.
51GLOBAL_SHOW_STIMULUS = "global_show_stimulus"
52GLOBAL_SHOW_FIX = "global_show_fix"
53GLOBAL_SHOW_ORDER = "global_show_order"
54GLOBAL_SHOW_SACCADES = "global_show_saccades"
55GLOBAL_SHOW_SACCADE_ARROWS = "global_show_saccade_arrows"
56GLOBAL_SACCADE_TYPE_LEGEND = "global_saccade_type_legend"
57GLOBAL_FIXATION_SNAP_TO_WORD = "global_fixation_snap_to_word"
58GLOBAL_ANIM_AUTOPLAY = "global_anim_autoplay"
59GLOBAL_SHOW_HEATMAP = "global_show_heatmap"
60GLOBAL_SHOW_RAW_GAZE = "global_show_raw_gaze"
61# UX-86: raw gaze's own style.
62GLOBAL_RAW_GAZE_COLOR = "global_raw_gaze_color"
63GLOBAL_RAW_GAZE_MARKER_SIZE = "global_raw_gaze_marker_size"
64GLOBAL_RAW_GAZE_OPACITY = "global_raw_gaze_opacity"
65# Each colour scale's bar — the fixations' and the heatmap's — has its own
66# switch and style.
67GLOBAL_SHOW_FIXATION_COLORBAR = "global_show_fixation_colorbar"
68GLOBAL_SHOW_HEATMAP_COLORBAR = "global_show_heatmap_colorbar"
69GLOBAL_HOLLOW_FIXATIONS = "global_hollow_fixations"
70GLOBAL_SCALE_TEXT_TO_BOXES = "global_scale_text_to_boxes"
71GLOBAL_COLOR_BY = "global_color_by"
72GLOBAL_HEATMAP_STYLE = "global_heatmap_style"
73# The Interpolated heatmap's blur: automatic, or a fixed σ in px.
74GLOBAL_HEATMAP_SIGMA_AUTO = "global_heatmap_sigma_auto"
75GLOBAL_HEATMAP_SIGMA_PX = "global_heatmap_sigma_px"
76GLOBAL_HEATMAP_NORM = "global_heatmap_norm"
77GLOBAL_HEATMAP_METRIC = "global_heatmap_metric"
78GLOBAL_CRITICAL_SPAN_STYLE = "global_critical_span_style"
79GLOBAL_HIGHLIGHT_COLUMN = "global_highlight_column"
80GLOBAL_X_FIELD = "global_x_field"
81GLOBAL_Y_FIELD = "global_y_field"
82GLOBAL_SACCADE_STYLE = "global_saccade_style"
83GLOBAL_SACCADE_RENDER_MODE = "global_saccade_render_mode"
84# PRE-3 vertical drift correction (ENG-23 put it on the link + the config).
85GLOBAL_ALIGN_ALGORITHM = "global_align_algorithm"
86GLOBAL_ALIGN_CONNECTORS = "global_align_connectors"
87GLOBAL_ILLUSTRATION_LABEL = "global_illustration_label"
88GLOBAL_ILLUSTRATION_TEXT = "global_illustration_text"
89GLOBAL_PREPROC_ENABLED = "global_preproc_enabled"
90GLOBAL_PREPROC_BLINK_ADJACENT = "global_preproc_blink_adjacent"
91GLOBAL_PREPROC_SHORT_POLICY = "global_preproc_short_policy"
92GLOBAL_PREPROC_SHORT_THRESHOLD_MS = "global_preproc_short_threshold_ms"
93GLOBAL_PREPROC_MERGE_DISTANCE_CHARS = "global_preproc_merge_distance_chars"
94GLOBAL_FIXATION_SYMBOL = "global_fixation_symbol"
95GLOBAL_FIXATION_COLOR = "global_fixation_color"
96GLOBAL_PALETTE = "global_palette"
97GLOBAL_FIXATION_COLORSCALE = "global_fixation_colorscale"
98GLOBAL_HEATMAP_COLORSCALE = "global_heatmap_colorscale"
99GLOBAL_SACCADE_COLOR = "global_saccade_color"
100GLOBAL_SACCADE_COLOR_MODE = "global_saccade_color_mode"
101GLOBAL_SACCADE_CLASS_COLOR_FORWARD = "global_saccade_class_color_forward"
102GLOBAL_SACCADE_CLASS_COLOR_SKIP = "global_saccade_class_color_skip"
103GLOBAL_SACCADE_CLASS_COLOR_REFIXATION = "global_saccade_class_color_refixation"
104GLOBAL_SACCADE_CLASS_COLOR_RETURN_SWEEP = "global_saccade_class_color_return_sweep"
105GLOBAL_SACCADE_CLASS_COLOR_REGRESSION = "global_saccade_class_color_regression"
106# VIZ-31: the saccade reading-class *filter* (which classes are drawn at all),
107# a list of class names. Wire format because a "regressions only" figure is
108# exactly the kind of view a link or a saved config exists to reproduce.
109GLOBAL_SACCADE_CLASSES = "global_saccade_classes"
110GLOBAL_ORDER_FONT_COLOR = "global_order_font_color"
111GLOBAL_TEXT_COLOR = "global_text_color"
112GLOBAL_HIGHLIGHT_TEXT_COLOR = "global_highlight_text_color"
113GLOBAL_BG_CHOICE = "global_bg_choice"
114GLOBAL_BG_CUSTOM = "global_bg_custom"
115GLOBAL_FONT_FAMILY = "global_font_family"
116GLOBAL_WORD_HOVER_MEASURE = "global_word_hover_measure"
117GLOBAL_WORD_HOVER_FIELDS = "global_word_hover_fields"
118GLOBAL_FIXATION_HOVER_FIELDS = "global_fixation_hover_fields"
119GLOBAL_ORDER_FONT_SIZE = "global_order_font_size"
120GLOBAL_ANIM_GRID_STEP_MS = "global_anim_grid_step_ms"
121GLOBAL_ANIM_MAX_FRAMES = "global_anim_max_frames"
122GLOBAL_LINE_SPACING = "global_line_spacing"
123GLOBAL_SACCADE_WIDTH = "global_saccade_width"
124GLOBAL_FIXATION_OPACITY = "global_fixation_opacity"
125GLOBAL_STIMULUS_IMAGE_OPACITY = "global_stimulus_image_opacity"
126GLOBAL_STIMULUS_IMAGE_OFFSET_X = "global_stimulus_image_offset_x"
127GLOBAL_STIMULUS_IMAGE_OFFSET_Y = "global_stimulus_image_offset_y"
128GLOBAL_STIMULUS_IMAGE_SCALE = "global_stimulus_image_scale"
129GLOBAL_MARKER_SIZE_RANGE = "global_marker_size_range"
130# The fixed duration scale: curve (or "relative"), its ms bounds, and its key.
131GLOBAL_MARKER_SIZE_SCALE = "global_marker_size_scale"
132GLOBAL_MARKER_DURATION_RANGE = "global_marker_duration_range"
133GLOBAL_DURATION_SIZE_LEGEND = "global_duration_size_legend"
134# 📐 Figure & canvas → Legends: where each legend sits. One key per legend and
135# part, ``global_legend_<kind>_{position,arrangement,size}``, for the kinds in
136# `constants.LEGEND_KINDS` (spelled out here, as everything in this file is).
137LEGEND_KIND_NAMES = ("compare", "saccades", "colors", "size_key")
138LEGEND_STATE_KEYS = frozenset(
139 f"global_legend_{kind}_{part}"
140 for kind in LEGEND_KIND_NAMES
141 for part in ("position", "arrangement", "size")
142)
143GLOBAL_FIXATION_COLOR_RANGE = "global_fixation_color_range"
144GLOBAL_HEATMAP_COLOR_RANGE = "global_heatmap_color_range"
145GLOBAL_SHOW_STIMULUS_IMAGE = "global_show_stimulus_image"
146GLOBAL_FIT_TO_MONITOR = "global_fit_to_monitor"
147# VIZ-34: optional screen-coordinate grid and its automatic/manual interval.
148GLOBAL_SHOW_COORDINATE_GRID = "global_show_coordinate_grid"
149GLOBAL_COORDINATE_GRID_AUTO = "global_coordinate_grid_auto"
150GLOBAL_COORDINATE_GRID_SPACING = "global_coordinate_grid_spacing"
151# EXP-5: title/caption on the figure (moved here from being Export-only).
152GLOBAL_SHOW_TITLE = "global_show_title"
153GLOBAL_SHOW_CAPTION = "global_show_caption"
154GLOBAL_TITLE_PATTERN = "global_title_pattern"
155GLOBAL_CAPTION_PATTERN = "global_caption_pattern"
156# EXP-18: settings that change the figure and used to travel in the saved config
157# only — colour-bar styling, the span border colour, the PRE-2 fixation flags
158# (one mode/threshold/symbol/colour group per category; `oob` and `blink` have no
159# threshold — geometry and blink tests, not durations) — plus Compare's A/B
160# legend and the replay speed, which travelled in neither.
161GLOBAL_FIXATION_COLORBAR_ORIENTATION = "global_fixation_colorbar_orientation"
162GLOBAL_FIXATION_COLORBAR_TICKANGLE = "global_fixation_colorbar_tickangle"
163GLOBAL_FIXATION_COLORBAR_TICKFONT_SIZE = "global_fixation_colorbar_tickfont_size"
164GLOBAL_HEATMAP_COLORBAR_ORIENTATION = "global_heatmap_colorbar_orientation"
165GLOBAL_HEATMAP_COLORBAR_TICKANGLE = "global_heatmap_colorbar_tickangle"
166GLOBAL_HEATMAP_COLORBAR_TICKFONT_SIZE = "global_heatmap_colorbar_tickfont_size"
167GLOBAL_SPAN_BORDER_COLOR = "global_span_border_color"
168GLOBAL_FIXCLASS_SHORT_MODE = "global_fixclass_short_mode"
169GLOBAL_FIXCLASS_SHORT_THRESHOLD_MS = "global_fixclass_short_threshold_ms"
170GLOBAL_FIXCLASS_SHORT_SYMBOL = "global_fixclass_short_symbol"
171GLOBAL_FIXCLASS_SHORT_COLOR = "global_fixclass_short_color"
172GLOBAL_FIXCLASS_LONG_MODE = "global_fixclass_long_mode"
173GLOBAL_FIXCLASS_LONG_THRESHOLD_MS = "global_fixclass_long_threshold_ms"
174GLOBAL_FIXCLASS_LONG_SYMBOL = "global_fixclass_long_symbol"
175GLOBAL_FIXCLASS_LONG_COLOR = "global_fixclass_long_color"
176GLOBAL_FIXCLASS_OOB_MODE = "global_fixclass_oob_mode"
177GLOBAL_FIXCLASS_OOB_SYMBOL = "global_fixclass_oob_symbol"
178GLOBAL_FIXCLASS_OOB_COLOR = "global_fixclass_oob_color"
179GLOBAL_FIXCLASS_BLINK_MODE = "global_fixclass_blink_mode"
180GLOBAL_FIXCLASS_BLINK_SYMBOL = "global_fixclass_blink_symbol"
181GLOBAL_FIXCLASS_BLINK_COLOR = "global_fixclass_blink_color"
182GLOBAL_SHOW_COMPARE_LEGEND = "global_show_compare_legend"
184# --- The recording setup ----------------------------------------------------
185# Saved-config-only until EXP-19 put every one of them on the link too — each
186# emitted only when it differs from what the recipient's own session would
187# resolve for the same source (see `URL_OPTIONAL_PARAMS`).
188GLOBAL_BASE_FONT_SIZE = "global_base_font_size"
189GLOBAL_CANVAS_WIDTH = "global_canvas_width"
190GLOBAL_CANVAS_HEIGHT = "global_canvas_height"
191GLOBAL_MONITOR_WIDTH_MM = "global_monitor_width_mm"
192GLOBAL_VIEWING_DISTANCE_MM = "global_viewing_distance_mm"
193GLOBAL_DISPLAY_DPI = "global_display_dpi"
194GLOBAL_STIMULUS_FONT_PT = "global_stimulus_font_pt"
195GLOBAL_USE_STIMULUS_FONT_PT = "global_use_stimulus_font_pt"
197# --- Export → Current figure's print size (#374, F28) -------------------------
198# Not `global_*`: the size a file is written at is not part of a design. On the
199# link and in the settings file, but only while a width is set.
200EXPORT_FIGURE_WIDTH = "export_figure_width"
201EXPORT_FIGURE_WIDTH_UNIT = "export_figure_width_unit"
202EXPORT_FIGURE_DPI = "export_figure_dpi"
204# --- Trial-picker keys a link / config seeds (utils.select_trial owns them) --
205# `_SELECTION_PREFIXES` in url_state is ("single",); these are that prefix's
206# widget keys, seeded before the picker renders.
207SINGLE_SELECT_TRIAL_MODE = "single_select_trial_mode"
208SINGLE_TRIAL_ID = "single_trial_id"
209#: #374 — one-shot: the trial a link or settings file chose, so the per-dataset
210#: trial memory never takes it for one carried over (`utils.select_trial`).
211SINGLE_TRIAL_CHOSEN = "_single_trial_chosen"
212SINGLE_PARTICIPANT = "single_participant"
213SINGLE_SLIDER = "single_slider"
214SINGLE_ANIMATE = "single_animate"
215#: #373: show the chips above the plot (default on). Hiding keeps the field
216#: selection (`trial_chip_fields`), so showing them again restores the row.
217SINGLE_SHOW_CHIPS = "single_show_chips"
218#: The ⚙ Playback replay speed (EXP-18 put it on the link). Mode-local, like
219#: `single_animate`, which is why it is `single_*` rather than `global_*`.
220SINGLE_PLAYBACK_SPEED = "single_playback_speed"
221#: VIZ-7 — the fixation-index window. Wire format since UX-135 gave it a
222#: `?fix_range=lo,hi` param; the widget itself predates it.
223SINGLE_FIX_RANGE = "single_fix_range"
224#: Turned on by a `?compare=` deep link, exactly as `?tab=animation` turns on
225#: `single_animate` (CMP-8 §7).
226SINGLE_COMPARE_TOGGLE = "single_compare_toggle"
227#: How the two scanpaths are arranged — "Overlay" / "Side by side" / "Stacked".
228#: CMP-8 shipped `compare=` and `cmp_source` but *not* this, so a shared
229#: comparison always reopened as Overlay; CMP-11 put it on the wire.
230SINGLE_COMPARE_LAYOUT = "single_compare_layout"
231#: Which reading supplies an overlay's word boxes + text — "Both" / "A" / "B"
232#: (CMP-11). Two datasets' AOIs coincide only when the text is identical.
233SINGLE_COMPARE_STIMULUS = "single_compare_stimulus"
235# --- Public-OneStop source options that ride the deep link (DATA-3) ---------
236ONESTOP_VARIANT = "onestop_variant"
237ONESTOP_REGIME = "onestop_regime"
238ONESTOP_PARTS = "onestop_parts"
240# --- Infrastructure keys the URL / config paths read or stamp ---------------
241DEEPLINK_PARTICIPANT = "_deeplink_participant"
242GLOBAL_ADVANCED = "global_advanced"
243# The per-trial annotation store; owned by annotations.py (pinned equal to it by
244# the contract test). Schema 2–3 configs carried it; since UX-179 (schema 4) it
245# travels in its own file (🗂️ Data → Annotations) and the recovery cache. Since
246# DATA-48 it holds the selected dataset's store only; the others wait under
247# `annotations.DATASET_STORE_KEY`, which is not wire format (never in a link).
248TRIAL_ANNOTATIONS = "trial_annotations"
249# VIZ-39 — the user's saved design library, one of the keys the on-device
250# recovery cache writes to its manifest, so the designs survive closing the
251# app. Schema 2–3 configs carried it under `design_presets`; since UX-179 it
252# has its own Export / Import on the saved-designs menu. Owned by controls.py
253# (pinned equal to it by the contract test).
254DESIGN_PRESETS = "_design_presets"
255# The column mapping is seeded key-by-key from a setup file's `column_mapping`
256# section (✏️ Edit dataset → Download setup file) and stored the same way in the recovery
257# cache; the prefix is the contract, the suffixes are data-dependent.
258COLUMN_MAPPING_PREFIX = "col_map_"
260# --- Per-scanpath comparison styling (config `compare` list) ----------------
261# Templates, not keys: the list index is substituted at read/write time.
262CMP_FIX_COLOR = "cmp{idx}_fix_color"
263CMP_SACCADE_COLOR = "cmp{idx}_saccade_color"
264CMP_SACCADE_STYLE = "cmp{idx}_saccade_style"
265CMP_SACCADE_WIDTH = "cmp{idx}_saccade_width"
266CMP_MARKER_SIZE_RANGE = "cmp{idx}_marker_size_range"
267CMP_HOLLOW = "cmp{idx}_hollow"
268CMP_OPACITY = "cmp{idx}_opacity"
269# UX-31: the A/B legend label override ("" = the auto "participant · trial").
270CMP_LABEL_PATTERN = "cmp{idx}_label_pattern"
271# The word-box outline override ("" = the scanpath's own fixation colour).
272CMP_BOX_COLOR = "cmp{idx}_box_color"
273# The word-box fill override ("" = the figure's `global_word_box_fill_color`).
274CMP_BOX_FILL_COLOR = "cmp{idx}_box_fill_color"
275# The raw-gaze sample colour override ("" = the scanpath's own fixation colour).
276CMP_RAW_GAZE_COLOR = "cmp{idx}_raw_gaze_color"
277# The heatmap colour-scale override ("" = the figure's `global_heatmap_colorscale`).
278CMP_HEATMAP_COLORSCALE = "cmp{idx}_heatmap_colorscale"
280# --- CMP-24: scanpath B's own filters in Compare ----------------------------
281# A's filters are the rail's ordinary ones (`global_fixclass_*`,
282# `global_saccade_classes`, `single_fix_range`), which is what makes a filter set
283# on one trial still apply once a second is brought in. Only B has keys of its
284# own — B-only on purpose rather than `cmp{idx}_*` templates, since a `cmp0_*`
285# copy would hand A a second, competing filter. B's Highlight marker and colour
286# are A's: what B chooses is *which* fixations, not how a flag is drawn.
287CMP_B_FIXCLASS_SHORT_MODE = "cmp1_fixclass_short_mode"
288CMP_B_FIXCLASS_SHORT_THRESHOLD_MS = "cmp1_fixclass_short_threshold_ms"
289CMP_B_FIXCLASS_LONG_MODE = "cmp1_fixclass_long_mode"
290CMP_B_FIXCLASS_LONG_THRESHOLD_MS = "cmp1_fixclass_long_threshold_ms"
291CMP_B_FIXCLASS_OOB_MODE = "cmp1_fixclass_oob_mode"
292CMP_B_FIXCLASS_BLINK_MODE = "cmp1_fixclass_blink_mode"
293CMP_B_SACCADE_CLASSES = "cmp1_saccade_classes"
294#: B's fixation-index window — VIZ-7's slider, for the second scanpath.
295SINGLE_COMPARE_FIX_RANGE = "single_compare_fix_range"
296#: …on the wire. Optional, like A's `fix_range`: an untouched window is the
297#: trial's own full range, not a setting.
298COMPARE_FIX_RANGE_PARAM = "cmp_b_fix_range"
299#: The B filter keys a saved config's second `compare` entry restores.
300COMPARE_B_FILTER_STATE_KEYS = frozenset(
301 {
302 CMP_B_FIXCLASS_SHORT_MODE,
303 CMP_B_FIXCLASS_SHORT_THRESHOLD_MS,
304 CMP_B_FIXCLASS_LONG_MODE,
305 CMP_B_FIXCLASS_LONG_THRESHOLD_MS,
306 CMP_B_FIXCLASS_OOB_MODE,
307 CMP_B_FIXCLASS_BLINK_MODE,
308 CMP_B_SACCADE_CLASSES,
309 }
310)
312#: EXP-19 — the link's spelling of the same styles: ``cmp_a_<field>`` for the
313#: first scanpath (``cmp0_*``) and ``cmp_b_<field>`` for the second (``cmp1_*``),
314#: ``<field>`` being the saved config's own name for it. The letters rather than
315#: the index because that is what every other surface calls the two scanpaths —
316#: ``style_a`` / ``style_b``, ``--label-a`` / ``--label-b``, ``cmp_stimulus=A|B``.
317COMPARE_STYLE_SIDES = (("a", 0), ("b", 1))
320def _compare_style_params(*fields: str) -> dict[str, str]:
321 """``{"cmp_a_<field>": "cmp0_<field>", "cmp_b_<field>": "cmp1_<field>"}``."""
322 return {
323 f"cmp_{side}_{name}": f"cmp{idx}_{name}"
324 for side, idx in COMPARE_STYLE_SIDES
325 for name in fields
326 }
329#: EXP-19 — where a deep link parks the recording-setup keys it seeded, and the
330#: source it resolved to (`url_state.scope_link_setup`), so that source's own
331#: monitor / typeface snap (`app.seed_canvas_state`) leaves them alone on the
332#: recipient's first run instead of overwriting the sender's values with the
333#: corpus defaults. A one-shot handoff, not a setting.
334LINK_SETUP_STATE_KEY = "_link_setup_keys"
336# ---------------------------------------------------------------------------
337# URL query-parameter names that are NOT viz settings — the selection half of a
338# deep link, plus one legacy alias kept alive for old links.
339# ---------------------------------------------------------------------------
340PARAM_SOURCE = "source"
341PARAM_PARTICIPANT = "participant"
342PARAM_TRIAL = "trial" # slider index (read-only; Share emits `trial_id`)
343PARAM_TRIAL_ID = "trial_id"
344PARAM_TAB = "tab"
345PARAM_ONESTOP_VARIANT = "onestop_variant"
346PARAM_ONESTOP_REGIME = "onestop_regime"
347PARAM_ONESTOP_PARTS = "onestop_parts"
348# Legacy inverse of `show_order`, still parsed so pre-Share links keep working.
349PARAM_HIDE_FIXATION_NUMBERS = "hide_fixation_numbers"
350# The one switch title and caption shared before each got its own; still read
351# (it turns both on or off), never written.
352PARAM_SHOW_TITLE_CAPTION = "show_title_caption"
353# The colour-bar settings the fixations and the heatmap shared before each had
354# its own; still read (each sets both bars), never written.
355PARAM_LEGACY_COLORBAR = (
356 "show_colorbars",
357 "colorbar_orientation",
358 "colorbar_tickangle",
359 "colorbar_tickfont_size",
360)
362# DATA-27 (Task 12): every public corpus on the deep link.
363#
364# `?source=corpus` says the data source is one entry of
365# `app.public_dataset_registry()` — the built-in public corpora **and** each
366# harmonised benchmark corpus the user added, which is a catalogue that varies
367# per machine and so cannot have one `?source=` token each. `?corpus=`
368# names which, by a slug of the entry's *stable identifier* (a prepared corpus'
369# manifest name, a built-in's registry `short`) — never of its display label,
370# which is copy and will be reworded.
371#
372# OneStop's regimes keep tokens of their own (`onestop_<regime>`, DATA-63) and
373# are emitted in preference to this pair, as `onestop_public` was before them.
374PARAM_CORPUS = "corpus"
376# #374 F14: a dataset the user added, by its name. Its files can't travel in a
377# link, so the name is what lets the recipient's app open it when it holds a
378# dataset of that name, and say which dataset is missing when it doesn't —
379# rather than apply the view to whatever else is open. Read in `app.main`, like
380# `corpus`; emitted only for an added dataset.
381PARAM_DATASET = "dataset"
383# Streamlit 1.65 `bind="query-params"` widgets: the widget key IS the URL param,
384# and its value is the option's label verbatim — so neither the key nor the
385# labels (`tabs.CORPUS_SUBTABS`) can be renamed without breaking a bookmarked
386# view — unless the old label stays readable (`tabs.CORPUS_SUBTAB_ALIASES`). Streamlit reads and writes these itself; `_apply_url_preset` and
387# `_build_share_query` never see them.
388CORPUS_SUBTAB = "corpus_subtab"
389URL_BOUND_WIDGET_KEYS = frozenset({CORPUS_SUBTAB})
391# The bundle *directory* is deliberately NOT here and never goes in a link: it is
392# a local filesystem path, so putting it on the wire would leak the sender's
393# directory layout to the recipient (and name a path that means nothing on their
394# machine). A link names the corpus; where the recipient keeps their bundle is
395# theirs to say. `eyegenbench_dir` therefore stays an ordinary session key.
397# Where `?source=` lands. Written by `app.main`'s source dispatch — every
398# `?source=` token has resolved to `data_source_choice` there since long before
399# this file existed, and `?source=corpus` adds `public_dataset_choice` (the
400# corpus behind the picker's collapse of any registry label to
401# `PUBLIC_DATASETS_CHOICE`). Pinned here because a link writes them; they are
402# **not** in `URL_SEEDED_STATE_KEYS`, which is specifically what
403# `url_state._apply_url_preset` seeds.
404DATA_SOURCE_CHOICE = "data_source_choice"
405PUBLIC_DATASET_CHOICE = "public_dataset_choice"
407# DATA-22 §7 surface 2: how the recording setup's three groups came to be known,
408# as `screen:assumed,geom:skipped,text:measured`. Metadata *about* the settings a
409# link already carries — it takes no input and changes no figure, so it stops at
410# the UI / link / saved-config / export surfaces and deliberately never becomes a
411# `render` flag or a builder argument.
412SETUP_PROVENANCE_PARAM = "setup_prov"
413# Where the arriving badge is parked for the UI to read. Not itself a *setting*,
414# so it is not in PLOT_CONFIG_STATE_KEYS — it describes the values beside it.
415SETUP_PROVENANCE_STATE_KEY = "_setup_provenance_arrived"
417# CMP-8 §7: the comparison's *second* scanpath.
418#
419# `compare` is `<participant>:<trial>` — B's real ids, never the `dataset · pid`
420# form the compare frames use internally. It is new to the wire format because
421# compare mode had **no** link representation at all before CMP-8 (Animate had
422# `?tab=animation`; Compare had nothing), and a `cmp_source` that named B's
423# corpus without naming B's trial would restore nothing.
424#
425# `cmp_source` is the corpus B came from, using the same vocabulary as `source`
426# — emitted only when that corpus is in `url_state._SHAREABLE_SOURCES`, since an
427# uploaded dataset lives in session state and cannot travel. Absent means "B is
428# in the same dataset as A", which is every pre-CMP-8 comparison.
429COMPARE_PARAM = "compare"
430COMPARE_SOURCE_PARAM = "cmp_source"
431#: B's screen of a multipart trial — `screen=` is A's. Emitted only beside
432#: `compare=`, and seeded into B's own navigator (`single_compare_screen_id`).
433COMPARE_SCREEN_PARAM = "cmp_screen"
434#: CMP-11 — the compare layout and the overlay's stimulus source. Closed
435#: vocabularies, so a bad value raises and the reader's "Ignored bad URL param"
436#: warning fires rather than the widget wedging on an option it has never heard of.
437COMPARE_LAYOUT_PARAM = "cmp_layout"
438COMPARE_STIMULUS_PARAM = "cmp_stimulus"
439#: Widget key holding the picked comparison dataset (`tabs`/`compare_source`).
440COMPARE_SOURCE_STATE_KEY = "cmp_dataset"
441#: VIZ-40 (UX-135) — VIZ-7's fixation-index window on the wire. Its own constant
442#: because two frozen groupings name it: the encoding group
443#: (`SHARE_INT_RANGE_PARAMS`) and `URL_OPTIONAL_PARAMS`, which is what says
444#: a link may legitimately not carry it.
445FIX_RANGE_PARAM = "fix_range"
446#: Where an arriving `compare=` selection waits until the candidate list exists.
447#: The picker's own key holds a *label* built at render time, so a link can't
448#: seed it directly — the same problem `PENDING_TRIAL_KEY` solves for A.
449PENDING_COMPARE_STATE_KEY = "_pending_compare"
450#: B's own screen navigator, seeded by `cmp_screen=` and by a settings file's
451#: `selection.compare.screen_id` (schema 6).
452SINGLE_COMPARE_SCREEN_ID = "single_compare_screen_id"
454# ---------------------------------------------------------------------------
455# Frozen groupings — `url_key -> session_state key`, one mapping per encoding.
456# The GROUP a param sits in is part of the wire format too: it decides how the
457# value is written into the URL and coerced back out ("1"/"0", str, int, float,
458# "lo,hi"). Moving a param between groups changes the encoding, so the contract
459# test pins the groups separately rather than one flat set.
460# ---------------------------------------------------------------------------
461# bool -> "1"/"0"
462SHARE_TOGGLE_PARAMS: Mapping[str, str] = MappingProxyType(
463 {
464 "show_words": GLOBAL_SHOW_WORDS,
465 "show_labels": GLOBAL_SHOW_LABELS,
466 "show_stimulus": GLOBAL_SHOW_STIMULUS,
467 "show_fixations": GLOBAL_SHOW_FIX,
468 "show_order": GLOBAL_SHOW_ORDER,
469 "show_saccades": GLOBAL_SHOW_SACCADES,
470 "show_saccade_arrows": GLOBAL_SHOW_SACCADE_ARROWS,
471 "saccade_type_legend": GLOBAL_SACCADE_TYPE_LEGEND,
472 "duration_size_legend": GLOBAL_DURATION_SIZE_LEGEND,
473 "snap_fixations": GLOBAL_FIXATION_SNAP_TO_WORD,
474 "align_connectors": GLOBAL_ALIGN_CONNECTORS,
475 "anim_autoplay": GLOBAL_ANIM_AUTOPLAY,
476 "show_heatmap": GLOBAL_SHOW_HEATMAP,
477 "show_raw_gaze": GLOBAL_SHOW_RAW_GAZE,
478 "show_fixation_colorbar": GLOBAL_SHOW_FIXATION_COLORBAR,
479 "show_heatmap_colorbar": GLOBAL_SHOW_HEATMAP_COLORBAR,
480 "heatmap_sigma_auto": GLOBAL_HEATMAP_SIGMA_AUTO,
481 "hollow_fixations": GLOBAL_HOLLOW_FIXATIONS,
482 "scale_text_to_boxes": GLOBAL_SCALE_TEXT_TO_BOXES,
483 "show_title": GLOBAL_SHOW_TITLE,
484 "show_caption": GLOBAL_SHOW_CAPTION,
485 "coordinate_grid": GLOBAL_SHOW_COORDINATE_GRID,
486 "coordinate_grid_auto": GLOBAL_COORDINATE_GRID_AUTO,
487 "preproc_enabled": GLOBAL_PREPROC_ENABLED,
488 "preproc_blink_adjacent": GLOBAL_PREPROC_BLINK_ADJACENT,
489 "show_stimulus_image": GLOBAL_SHOW_STIMULUS_IMAGE,
490 "fit_to_monitor": GLOBAL_FIT_TO_MONITOR,
491 "show_compare_legend": GLOBAL_SHOW_COMPARE_LEGEND,
492 # EXP-19.
493 "use_stimulus_font_pt": GLOBAL_USE_STIMULUS_FONT_PT,
494 **_compare_style_params("hollow"),
495 # #373.
496 "show_chips": SINGLE_SHOW_CHIPS,
497 }
498)
500# string / choice / colour
501SHARE_VALUE_PARAMS: Mapping[str, str] = MappingProxyType(
502 {
503 "export_width_unit": EXPORT_FIGURE_WIDTH_UNIT,
504 "color_by": GLOBAL_COLOR_BY,
505 "heatmap_style": GLOBAL_HEATMAP_STYLE,
506 "heatmap_norm": GLOBAL_HEATMAP_NORM,
507 "heatmap_metric": GLOBAL_HEATMAP_METRIC,
508 "critical_span_style": GLOBAL_CRITICAL_SPAN_STYLE,
509 "highlight_column": GLOBAL_HIGHLIGHT_COLUMN,
510 "x_field": GLOBAL_X_FIELD,
511 "y_field": GLOBAL_Y_FIELD,
512 "saccade_style": GLOBAL_SACCADE_STYLE,
513 "saccade_render_mode": GLOBAL_SACCADE_RENDER_MODE,
514 "marker_size_scale": GLOBAL_MARKER_SIZE_SCALE,
515 "align_algorithm": GLOBAL_ALIGN_ALGORITHM,
516 "fixation_symbol": GLOBAL_FIXATION_SYMBOL,
517 "fixation_color": GLOBAL_FIXATION_COLOR,
518 "palette": GLOBAL_PALETTE,
519 "fixation_colorscale": GLOBAL_FIXATION_COLORSCALE,
520 "heatmap_colorscale": GLOBAL_HEATMAP_COLORSCALE,
521 "saccade_color": GLOBAL_SACCADE_COLOR,
522 "raw_gaze_color": GLOBAL_RAW_GAZE_COLOR,
523 "word_box_color": GLOBAL_WORD_BOX_COLOR,
524 "word_box_fill_color": GLOBAL_WORD_BOX_FILL_COLOR,
525 "saccade_color_mode": GLOBAL_SACCADE_COLOR_MODE,
526 "saccade_color_forward": GLOBAL_SACCADE_CLASS_COLOR_FORWARD,
527 "saccade_color_skip": GLOBAL_SACCADE_CLASS_COLOR_SKIP,
528 "saccade_color_refixation": GLOBAL_SACCADE_CLASS_COLOR_REFIXATION,
529 "saccade_color_return_sweep": GLOBAL_SACCADE_CLASS_COLOR_RETURN_SWEEP,
530 "saccade_color_regression": GLOBAL_SACCADE_CLASS_COLOR_REGRESSION,
531 "saccade_classes": GLOBAL_SACCADE_CLASSES,
532 "order_font_color": GLOBAL_ORDER_FONT_COLOR,
533 "text_color": GLOBAL_TEXT_COLOR,
534 "highlight_text_color": GLOBAL_HIGHLIGHT_TEXT_COLOR,
535 "bg_choice": GLOBAL_BG_CHOICE,
536 "bg_custom": GLOBAL_BG_CUSTOM,
537 "font_family": GLOBAL_FONT_FAMILY,
538 "word_hover_measure": GLOBAL_WORD_HOVER_MEASURE,
539 "word_hover_fields": GLOBAL_WORD_HOVER_FIELDS,
540 "fixation_hover_fields": GLOBAL_FIXATION_HOVER_FIELDS,
541 "illustration_label": GLOBAL_ILLUSTRATION_LABEL,
542 "illustration_text": GLOBAL_ILLUSTRATION_TEXT,
543 "preproc_short_policy": GLOBAL_PREPROC_SHORT_POLICY,
544 "title_pattern": GLOBAL_TITLE_PATTERN,
545 "caption_pattern": GLOBAL_CAPTION_PATTERN,
546 # CMP-11. Compare-mode settings rather than viz settings, but they ride
547 # the same value encoding, and a compare link that restores neither the
548 # layout nor the stimulus source restores the wrong figure.
549 COMPARE_LAYOUT_PARAM: SINGLE_COMPARE_LAYOUT,
550 COMPARE_STIMULUS_PARAM: SINGLE_COMPARE_STIMULUS,
551 # EXP-18.
552 "fixation_colorbar_orientation": GLOBAL_FIXATION_COLORBAR_ORIENTATION,
553 "heatmap_colorbar_orientation": GLOBAL_HEATMAP_COLORBAR_ORIENTATION,
554 "span_border_color": GLOBAL_SPAN_BORDER_COLOR,
555 "fixclass_short_mode": GLOBAL_FIXCLASS_SHORT_MODE,
556 "fixclass_short_symbol": GLOBAL_FIXCLASS_SHORT_SYMBOL,
557 "fixclass_short_color": GLOBAL_FIXCLASS_SHORT_COLOR,
558 "fixclass_long_mode": GLOBAL_FIXCLASS_LONG_MODE,
559 "fixclass_long_symbol": GLOBAL_FIXCLASS_LONG_SYMBOL,
560 "fixclass_long_color": GLOBAL_FIXCLASS_LONG_COLOR,
561 "fixclass_oob_mode": GLOBAL_FIXCLASS_OOB_MODE,
562 "fixclass_oob_symbol": GLOBAL_FIXCLASS_OOB_SYMBOL,
563 "fixclass_oob_color": GLOBAL_FIXCLASS_OOB_COLOR,
564 "fixclass_blink_mode": GLOBAL_FIXCLASS_BLINK_MODE,
565 "fixclass_blink_symbol": GLOBAL_FIXCLASS_BLINK_SYMBOL,
566 "fixclass_blink_color": GLOBAL_FIXCLASS_BLINK_COLOR,
567 # EXP-19.
568 **_compare_style_params(
569 "fix_color",
570 "saccade_color",
571 "saccade_style",
572 "label_pattern",
573 "box_color",
574 "box_fill_color",
575 "raw_gaze_color",
576 "heatmap_colorscale",
577 ),
578 # CMP-24.
579 "cmp_b_saccade_classes": CMP_B_SACCADE_CLASSES,
580 "cmp_b_fixclass_short_mode": CMP_B_FIXCLASS_SHORT_MODE,
581 "cmp_b_fixclass_long_mode": CMP_B_FIXCLASS_LONG_MODE,
582 "cmp_b_fixclass_oob_mode": CMP_B_FIXCLASS_OOB_MODE,
583 "cmp_b_fixclass_blink_mode": CMP_B_FIXCLASS_BLINK_MODE,
584 }
585)
587# int
588SHARE_INT_PARAMS: Mapping[str, str] = MappingProxyType(
589 {
590 "export_dpi": EXPORT_FIGURE_DPI,
591 "order_font_size": GLOBAL_ORDER_FONT_SIZE,
592 "anim_grid_step_ms": GLOBAL_ANIM_GRID_STEP_MS,
593 "anim_max_frames": GLOBAL_ANIM_MAX_FRAMES,
594 # EXP-18.
595 "fixation_colorbar_tickangle": GLOBAL_FIXATION_COLORBAR_TICKANGLE,
596 "fixation_colorbar_tickfont_size": GLOBAL_FIXATION_COLORBAR_TICKFONT_SIZE,
597 "heatmap_colorbar_tickangle": GLOBAL_HEATMAP_COLORBAR_TICKANGLE,
598 "heatmap_colorbar_tickfont_size": GLOBAL_HEATMAP_COLORBAR_TICKFONT_SIZE,
599 "fixclass_short_threshold_ms": GLOBAL_FIXCLASS_SHORT_THRESHOLD_MS,
600 "fixclass_long_threshold_ms": GLOBAL_FIXCLASS_LONG_THRESHOLD_MS,
601 # EXP-19.
602 "canvas_width": GLOBAL_CANVAS_WIDTH,
603 "canvas_height": GLOBAL_CANVAS_HEIGHT,
604 "base_font_size": GLOBAL_BASE_FONT_SIZE,
605 # CMP-24.
606 "cmp_b_fixclass_short_threshold_ms": CMP_B_FIXCLASS_SHORT_THRESHOLD_MS,
607 "cmp_b_fixclass_long_threshold_ms": CMP_B_FIXCLASS_LONG_THRESHOLD_MS,
608 }
609)
611# float
612SHARE_FLOAT_PARAMS: Mapping[str, str] = MappingProxyType(
613 {
614 "export_width": EXPORT_FIGURE_WIDTH,
615 "line_spacing": GLOBAL_LINE_SPACING,
616 "heatmap_sigma_px": GLOBAL_HEATMAP_SIGMA_PX,
617 "preproc_short_threshold_ms": GLOBAL_PREPROC_SHORT_THRESHOLD_MS,
618 "preproc_merge_distance_chars": GLOBAL_PREPROC_MERGE_DISTANCE_CHARS,
619 "saccade_width": GLOBAL_SACCADE_WIDTH,
620 "fixation_opacity": GLOBAL_FIXATION_OPACITY,
621 "stimulus_image_opacity": GLOBAL_STIMULUS_IMAGE_OPACITY,
622 "stimulus_image_offset_x": GLOBAL_STIMULUS_IMAGE_OFFSET_X,
623 "stimulus_image_offset_y": GLOBAL_STIMULUS_IMAGE_OFFSET_Y,
624 "stimulus_image_scale": GLOBAL_STIMULUS_IMAGE_SCALE,
625 "coordinate_grid_spacing": GLOBAL_COORDINATE_GRID_SPACING,
626 "raw_gaze_marker_size": GLOBAL_RAW_GAZE_MARKER_SIZE,
627 "raw_gaze_opacity": GLOBAL_RAW_GAZE_OPACITY,
628 "word_box_line_opacity": GLOBAL_WORD_BOX_LINE_OPACITY,
629 "word_box_fill_opacity": GLOBAL_WORD_BOX_FILL_OPACITY,
630 # EXP-18.
631 "playback_speed": SINGLE_PLAYBACK_SPEED,
632 # EXP-19.
633 "monitor_width_mm": GLOBAL_MONITOR_WIDTH_MM,
634 "viewing_distance_mm": GLOBAL_VIEWING_DISTANCE_MM,
635 "display_dpi": GLOBAL_DISPLAY_DPI,
636 "stimulus_font_pt": GLOBAL_STIMULUS_FONT_PT,
637 **_compare_style_params("saccade_width", "opacity"),
638 }
639)
641# (int, int) -> "lo,hi"
642SHARE_INT_RANGE_PARAMS: Mapping[str, str] = MappingProxyType(
643 {
644 "marker_size_range": GLOBAL_MARKER_SIZE_RANGE,
645 "marker_duration_range": GLOBAL_MARKER_DURATION_RANGE,
646 # UX-135 — VIZ-7's fixation-index window. Optional on the wire (see
647 # `URL_OPTIONAL_PARAMS`): the slider *defaults* to the trial's own full
648 # range, so an untouched one is not a setting and must not be stamped
649 # onto every link.
650 FIX_RANGE_PARAM: SINGLE_FIX_RANGE,
651 # EXP-19.
652 **_compare_style_params("marker_size_range"),
653 # CMP-24 — B's window; optional on the same terms as A's.
654 COMPARE_FIX_RANGE_PARAM: SINGLE_COMPARE_FIX_RANGE,
655 }
656)
658# (float, float) -> "lo,hi"
659SHARE_FLOAT_RANGE_PARAMS: Mapping[str, str] = MappingProxyType(
660 {
661 "fixation_color_range": GLOBAL_FIXATION_COLOR_RANGE,
662 "heatmap_color_range": GLOBAL_HEATMAP_COLOR_RANGE,
663 }
664)
666# Saved-config `layers` key -> session key (the config's own naming,
667# deliberately different from the URL params above).
668PLOT_CONFIG_LAYER_KEYS: Mapping[str, str] = MappingProxyType(
669 {
670 "words": GLOBAL_SHOW_WORDS,
671 "word_labels": GLOBAL_SHOW_LABELS,
672 "stimulus": GLOBAL_SHOW_STIMULUS,
673 "fixations": GLOBAL_SHOW_FIX,
674 "order_labels": GLOBAL_SHOW_ORDER,
675 "saccades": GLOBAL_SHOW_SACCADES,
676 "saccade_arrows": GLOBAL_SHOW_SACCADE_ARROWS,
677 "heatmap": GLOBAL_SHOW_HEATMAP,
678 "raw_gaze": GLOBAL_SHOW_RAW_GAZE,
679 "stimulus_image": GLOBAL_SHOW_STIMULUS_IMAGE,
680 "full_monitor": GLOBAL_FIT_TO_MONITOR,
681 "autoplay": GLOBAL_ANIM_AUTOPLAY,
682 }
683)
685# Every viz-setting param, in every encoding — the settings half of a link.
686SHARE_PARAMS: Mapping[str, str] = MappingProxyType(
687 {
688 **SHARE_TOGGLE_PARAMS,
689 **SHARE_VALUE_PARAMS,
690 **SHARE_INT_PARAMS,
691 **SHARE_FLOAT_PARAMS,
692 **SHARE_INT_RANGE_PARAMS,
693 **SHARE_FLOAT_RANGE_PARAMS,
694 }
695)
697# Params `_apply_url_preset` / `_apply_url_trial_selection` accept but that are
698# not viz settings (the data source + which trial to land on).
699URL_SELECTION_PARAMS = frozenset(
700 {
701 PARAM_SOURCE,
702 PARAM_PARTICIPANT,
703 PARAM_TRIAL,
704 PARAM_TRIAL_ID,
705 PARAM_TAB,
706 PARAM_ONESTOP_VARIANT,
707 PARAM_ONESTOP_REGIME,
708 PARAM_ONESTOP_PARTS,
709 PARAM_CORPUS,
710 PARAM_DATASET,
711 COMPARE_PARAM,
712 COMPARE_SOURCE_PARAM,
713 COMPARE_SCREEN_PARAM,
714 }
715)
717# EXP-19 — the settings that used to travel in the saved config only, by what
718# they describe. Both groups ride the link only when they differ from what the
719# recipient's own session would resolve for the same source and trial (so a link
720# does not stamp the demo's 2560x1440 onto itself, and does not override a
721# corpus' declared monitor with a copy of that same monitor), and the compare
722# styles only alongside a `compare=` — they restore nothing without one.
723SETUP_PARAMS: Mapping[str, str] = MappingProxyType(
724 {
725 "canvas_width": GLOBAL_CANVAS_WIDTH,
726 "canvas_height": GLOBAL_CANVAS_HEIGHT,
727 "base_font_size": GLOBAL_BASE_FONT_SIZE,
728 "monitor_width_mm": GLOBAL_MONITOR_WIDTH_MM,
729 "viewing_distance_mm": GLOBAL_VIEWING_DISTANCE_MM,
730 "display_dpi": GLOBAL_DISPLAY_DPI,
731 "stimulus_font_pt": GLOBAL_STIMULUS_FONT_PT,
732 "use_stimulus_font_pt": GLOBAL_USE_STIMULUS_FONT_PT,
733 }
734)
735#: CMP-24 — B's filters travel on the terms of B's styles: only beside a
736#: `compare=`, and only when they differ from a fresh session's.
737COMPARE_B_FILTER_PARAMS: Mapping[str, str] = MappingProxyType(
738 {
739 "cmp_b_saccade_classes": CMP_B_SACCADE_CLASSES,
740 "cmp_b_fixclass_short_mode": CMP_B_FIXCLASS_SHORT_MODE,
741 "cmp_b_fixclass_short_threshold_ms": CMP_B_FIXCLASS_SHORT_THRESHOLD_MS,
742 "cmp_b_fixclass_long_mode": CMP_B_FIXCLASS_LONG_MODE,
743 "cmp_b_fixclass_long_threshold_ms": CMP_B_FIXCLASS_LONG_THRESHOLD_MS,
744 "cmp_b_fixclass_oob_mode": CMP_B_FIXCLASS_OOB_MODE,
745 "cmp_b_fixclass_blink_mode": CMP_B_FIXCLASS_BLINK_MODE,
746 }
747)
748#: #374 F28 — Export → Current figure's print size. On the link only while a
749#: width is set: without one the PNG is drawn at the screen size, the default.
750EXPORT_PARAMS: Mapping[str, str] = MappingProxyType(
751 {
752 "export_width": EXPORT_FIGURE_WIDTH,
753 "export_width_unit": EXPORT_FIGURE_WIDTH_UNIT,
754 "export_dpi": EXPORT_FIGURE_DPI,
755 }
756)
757COMPARE_STYLE_PARAMS: Mapping[str, str] = MappingProxyType(
758 {
759 **_compare_style_params(
760 "fix_color",
761 "saccade_color",
762 "saccade_style",
763 "saccade_width",
764 "marker_size_range",
765 "hollow",
766 "opacity",
767 "label_pattern",
768 "box_color",
769 "box_fill_color",
770 "raw_gaze_color",
771 "heatmap_colorscale",
772 ),
773 **COMPARE_B_FILTER_PARAMS,
774 }
775)
777# Session keys `app.main`'s `?source=` dispatch writes when a link names a data
778# source. Separate from `URL_SEEDED_STATE_KEYS` because they are seeded there,
779# after `_apply_url_preset` has returned the token — see PARAM_CORPUS above.
780URL_SOURCE_STATE_KEYS = frozenset({DATA_SOURCE_CHOICE, PUBLIC_DATASET_CHOICE})
782# Params the reader accepts and the writer emits only *when there is something to
783# say* — as opposed to `SHARE_QUERY_PARAMS`, which every fully-populated session
784# emits. `setup_prov` is absent for a corpus that never declared a recording
785# setup, and an absent badge is the honest outcome there: emitting
786# "assumed,assumed,assumed" would manufacture a claim the sender never made.
787# `corpus` is absent for every source that is not a public corpus, and for the
788# one public corpus that still travels under its own older token. EXP-19's two
789# groups are here because they are emitted only when they differ from the
790# recipient's own defaults — see `SETUP_PARAMS` above.
791# Each legend's placement as one param, ``legend_<kind>=SPOT[,ARRANGEMENT][,SIZE]``
792# (`plots.parse_legend_spec`, the spelling `render --legend` takes), written only
793# for a legend moved off Auto — so they are optional, below.
794LEGEND_PARAMS: Mapping[str, str] = MappingProxyType(
795 {f"legend_{kind}": kind for kind in LEGEND_KIND_NAMES}
796)
798URL_OPTIONAL_PARAMS = frozenset(
799 {
800 *LEGEND_PARAMS,
801 SETUP_PROVENANCE_PARAM,
802 COMPARE_PARAM,
803 COMPARE_SOURCE_PARAM,
804 COMPARE_SCREEN_PARAM,
805 PARAM_CORPUS,
806 PARAM_DATASET,
807 FIX_RANGE_PARAM,
808 COMPARE_FIX_RANGE_PARAM,
809 *SETUP_PARAMS,
810 *COMPARE_STYLE_PARAMS,
811 *EXPORT_PARAMS,
812 }
813)
815# DATA-63: params the reader still accepts from links written before, and the
816# writer never emits. DATA-3's public OneStop link carried its variant, regime
817# and parts; each regime is now its own dataset with its own `?source=` token,
818# holding every part from the public release, so a link has nothing to add —
819# but `onestop_public` + `onestop_regime` must keep opening the regime it named.
820URL_LEGACY_PARAMS = frozenset(
821 {
822 PARAM_ONESTOP_VARIANT,
823 PARAM_ONESTOP_REGIME,
824 PARAM_ONESTOP_PARTS,
825 PARAM_SHOW_TITLE_CAPTION,
826 *PARAM_LEGACY_COLORBAR,
827 }
828)
830# The exact key set of `url_state._URL_PRESETS` — every param a deep link can
831# carry that presets a widget.
832URL_PRESET_PARAMS = frozenset(SHARE_PARAMS) | {PARAM_HIDE_FIXATION_NUMBERS}
834# The exact param set `_build_share_query` emits for a fully-populated session
835# on a shareable source. `trial` is absent on purpose: Share writes the
836# canonical `trial_id` instead of a slider index.
837SHARE_QUERY_PARAMS = (
838 (frozenset(SHARE_PARAMS) | (URL_SELECTION_PARAMS - {PARAM_TRIAL}))
839 - URL_OPTIONAL_PARAMS
840 - URL_LEGACY_PARAMS
841)
843# Session keys `_URL_BOUNDED` clamps on the way in (a hand-crafted link with an
844# out-of-range value would otherwise crash the widget on render).
845URL_BOUNDED_STATE_KEYS = frozenset(
846 {
847 GLOBAL_LINE_SPACING,
848 GLOBAL_SACCADE_WIDTH,
849 GLOBAL_ORDER_FONT_SIZE,
850 GLOBAL_ANIM_GRID_STEP_MS,
851 GLOBAL_ANIM_MAX_FRAMES,
852 GLOBAL_MARKER_SIZE_RANGE,
853 GLOBAL_MARKER_DURATION_RANGE,
854 GLOBAL_FIXATION_OPACITY,
855 GLOBAL_STIMULUS_IMAGE_OPACITY,
856 GLOBAL_STIMULUS_IMAGE_OFFSET_X,
857 GLOBAL_STIMULUS_IMAGE_OFFSET_Y,
858 GLOBAL_STIMULUS_IMAGE_SCALE,
859 GLOBAL_HEATMAP_SIGMA_PX,
860 GLOBAL_PREPROC_SHORT_THRESHOLD_MS,
861 GLOBAL_PREPROC_MERGE_DISTANCE_CHARS,
862 GLOBAL_COORDINATE_GRID_SPACING,
863 GLOBAL_RAW_GAZE_MARKER_SIZE,
864 GLOBAL_RAW_GAZE_OPACITY,
865 GLOBAL_WORD_BOX_LINE_OPACITY,
866 GLOBAL_WORD_BOX_FILL_OPACITY,
867 GLOBAL_FIXATION_COLORBAR_TICKANGLE,
868 GLOBAL_FIXATION_COLORBAR_TICKFONT_SIZE,
869 GLOBAL_HEATMAP_COLORBAR_TICKANGLE,
870 GLOBAL_HEATMAP_COLORBAR_TICKFONT_SIZE,
871 GLOBAL_FIXCLASS_SHORT_THRESHOLD_MS,
872 GLOBAL_FIXCLASS_LONG_THRESHOLD_MS,
873 CMP_B_FIXCLASS_SHORT_THRESHOLD_MS,
874 CMP_B_FIXCLASS_LONG_THRESHOLD_MS,
875 # EXP-19 — every numeric one of the two new groups.
876 GLOBAL_CANVAS_WIDTH,
877 GLOBAL_CANVAS_HEIGHT,
878 GLOBAL_BASE_FONT_SIZE,
879 GLOBAL_MONITOR_WIDTH_MM,
880 GLOBAL_VIEWING_DISTANCE_MM,
881 GLOBAL_DISPLAY_DPI,
882 GLOBAL_STIMULUS_FONT_PT,
883 # #374 F28.
884 EXPORT_FIGURE_WIDTH,
885 EXPORT_FIGURE_DPI,
886 *(
887 template.format(idx=idx)
888 for template in (CMP_SACCADE_WIDTH, CMP_MARKER_SIZE_RANGE, CMP_OPACITY)
889 for _side, idx in COMPARE_STYLE_SIDES
890 ),
891 }
892)
894# Session keys a deep link seeds beyond the viz settings: the trial picker, the
895# public-OneStop source options, and the two side-effect keys (`global_advanced`
896# opens the Advanced expander when a colorscale is linked; `_deeplink_participant`
897# is the one-shot capture the OneStop shard fast-path keys off).
898URL_SEEDED_STATE_KEYS = frozenset(
899 {
900 SINGLE_SELECT_TRIAL_MODE,
901 SINGLE_PARTICIPANT,
902 SINGLE_SLIDER,
903 SINGLE_ANIMATE,
904 SINGLE_COMPARE_TOGGLE,
905 COMPARE_SOURCE_STATE_KEY,
906 # Not a widget key — a one-shot handoff the compare picker consumes once
907 # its candidate labels exist. Pinned because a deep link writes it, so a
908 # rename here silently breaks `?compare=`.
909 PENDING_COMPARE_STATE_KEY,
910 ONESTOP_VARIANT,
911 ONESTOP_REGIME,
912 ONESTOP_PARTS,
913 DEEPLINK_PARTICIPANT,
914 GLOBAL_ADVANCED,
915 # EXP-19 — the one-shot "leave these alone" handoff to the source snap.
916 LINK_SETUP_STATE_KEY,
917 }
918)
920# ---------------------------------------------------------------------------
921# 🔗 Share → File (the settings file) and the Download setup file file
922# ---------------------------------------------------------------------------
923# The JSON schema version stamped by both writers and understood by the reader.
924# Bumping it in url_state without registering a migration (or without updating
925# this constant) is the failure the contract test catches.
926PLOT_CONFIG_SCHEMA_VERSION = 7
928# `cmp{idx}_*` templates the config's `compare` list restores, per entry.
929COMPARE_STATE_KEY_TEMPLATES = frozenset(
930 {
931 CMP_FIX_COLOR,
932 CMP_SACCADE_COLOR,
933 CMP_SACCADE_STYLE,
934 CMP_SACCADE_WIDTH,
935 CMP_MARKER_SIZE_RANGE,
936 CMP_HOLLOW,
937 CMP_OPACITY,
938 CMP_LABEL_PATTERN,
939 CMP_BOX_COLOR,
940 CMP_BOX_FILL_COLOR,
941 CMP_RAW_GAZE_COLOR,
942 CMP_HEATMAP_COLORSCALE,
943 }
944)
946# Every `global_*` session key `_restore_plot_config` writes from a complete,
947# fully-valid saved config. Anything missing here that the reader writes (or
948# vice versa) means the saved-config format moved.
949PLOT_CONFIG_STATE_KEYS = frozenset(
950 {
951 # Figure & canvas → Legends.
952 *LEGEND_STATE_KEYS,
953 # layers
954 GLOBAL_SHOW_WORDS,
955 GLOBAL_SHOW_LABELS,
956 GLOBAL_SHOW_STIMULUS,
957 GLOBAL_SHOW_FIX,
958 GLOBAL_SHOW_ORDER,
959 GLOBAL_SHOW_SACCADES,
960 GLOBAL_SHOW_SACCADE_ARROWS,
961 GLOBAL_SHOW_HEATMAP,
962 GLOBAL_SHOW_RAW_GAZE,
963 GLOBAL_SHOW_STIMULUS_IMAGE,
964 GLOBAL_FIT_TO_MONITOR,
965 GLOBAL_SHOW_COORDINATE_GRID,
966 GLOBAL_COORDINATE_GRID_AUTO,
967 GLOBAL_COORDINATE_GRID_SPACING,
968 GLOBAL_ANIM_AUTOPLAY,
969 # coloring
970 GLOBAL_PALETTE,
971 GLOBAL_COLOR_BY,
972 GLOBAL_HEATMAP_STYLE,
973 GLOBAL_HEATMAP_SIGMA_PX,
974 GLOBAL_HEATMAP_SIGMA_AUTO,
975 GLOBAL_HEATMAP_NORM,
976 GLOBAL_HEATMAP_METRIC,
977 GLOBAL_SHOW_FIXATION_COLORBAR,
978 GLOBAL_SHOW_HEATMAP_COLORBAR,
979 GLOBAL_FIXATION_COLORSCALE,
980 GLOBAL_HEATMAP_COLORSCALE,
981 GLOBAL_FIXATION_COLOR_RANGE,
982 GLOBAL_HEATMAP_COLOR_RANGE,
983 GLOBAL_SACCADE_COLOR,
984 GLOBAL_SACCADE_STYLE,
985 GLOBAL_SACCADE_WIDTH,
986 GLOBAL_SACCADE_RENDER_MODE,
987 GLOBAL_SACCADE_COLOR_MODE,
988 GLOBAL_SACCADE_TYPE_LEGEND,
989 GLOBAL_SACCADE_CLASS_COLOR_FORWARD,
990 GLOBAL_SACCADE_CLASS_COLOR_SKIP,
991 GLOBAL_SACCADE_CLASS_COLOR_REFIXATION,
992 GLOBAL_SACCADE_CLASS_COLOR_RETURN_SWEEP,
993 GLOBAL_SACCADE_CLASS_COLOR_REGRESSION,
994 GLOBAL_SACCADE_CLASSES,
995 GLOBAL_FIXATION_SNAP_TO_WORD,
996 GLOBAL_ALIGN_ALGORITHM,
997 GLOBAL_ALIGN_CONNECTORS,
998 GLOBAL_ILLUSTRATION_LABEL,
999 GLOBAL_ILLUSTRATION_TEXT,
1000 GLOBAL_PREPROC_ENABLED,
1001 GLOBAL_PREPROC_BLINK_ADJACENT,
1002 GLOBAL_PREPROC_SHORT_POLICY,
1003 GLOBAL_PREPROC_SHORT_THRESHOLD_MS,
1004 GLOBAL_PREPROC_MERGE_DISTANCE_CHARS,
1005 GLOBAL_FIXATION_SYMBOL,
1006 GLOBAL_FIXATION_COLOR,
1007 GLOBAL_HOLLOW_FIXATIONS,
1008 GLOBAL_FIXATION_OPACITY,
1009 GLOBAL_STIMULUS_IMAGE_OPACITY,
1010 GLOBAL_STIMULUS_IMAGE_OFFSET_X,
1011 GLOBAL_STIMULUS_IMAGE_OFFSET_Y,
1012 GLOBAL_STIMULUS_IMAGE_SCALE,
1013 GLOBAL_FIXATION_COLORBAR_ORIENTATION,
1014 GLOBAL_FIXATION_COLORBAR_TICKANGLE,
1015 GLOBAL_FIXATION_COLORBAR_TICKFONT_SIZE,
1016 GLOBAL_HEATMAP_COLORBAR_ORIENTATION,
1017 GLOBAL_HEATMAP_COLORBAR_TICKANGLE,
1018 GLOBAL_HEATMAP_COLORBAR_TICKFONT_SIZE,
1019 # sizing
1020 GLOBAL_MARKER_SIZE_RANGE,
1021 GLOBAL_MARKER_SIZE_SCALE,
1022 GLOBAL_MARKER_DURATION_RANGE,
1023 GLOBAL_DURATION_SIZE_LEGEND,
1024 GLOBAL_ORDER_FONT_SIZE,
1025 GLOBAL_ORDER_FONT_COLOR,
1026 GLOBAL_BASE_FONT_SIZE,
1027 # animation
1028 GLOBAL_ANIM_GRID_STEP_MS,
1029 GLOBAL_ANIM_MAX_FRAMES,
1030 # canvas_px + axes
1031 GLOBAL_CANVAS_WIDTH,
1032 GLOBAL_CANVAS_HEIGHT,
1033 GLOBAL_MONITOR_WIDTH_MM,
1034 GLOBAL_VIEWING_DISTANCE_MM,
1035 GLOBAL_DISPLAY_DPI,
1036 GLOBAL_STIMULUS_FONT_PT,
1037 GLOBAL_USE_STIMULUS_FONT_PT,
1038 GLOBAL_X_FIELD,
1039 GLOBAL_Y_FIELD,
1040 # text
1041 GLOBAL_SCALE_TEXT_TO_BOXES,
1042 GLOBAL_LINE_SPACING,
1043 GLOBAL_FONT_FAMILY,
1044 GLOBAL_TEXT_COLOR,
1045 GLOBAL_WORD_HOVER_FIELDS,
1046 GLOBAL_FIXATION_HOVER_FIELDS,
1047 # highlighting
1048 GLOBAL_CRITICAL_SPAN_STYLE,
1049 GLOBAL_HIGHLIGHT_COLUMN,
1050 GLOBAL_HIGHLIGHT_TEXT_COLOR,
1051 GLOBAL_BG_CHOICE,
1052 GLOBAL_BG_CUSTOM,
1053 GLOBAL_SPAN_BORDER_COLOR,
1054 GLOBAL_FIXCLASS_SHORT_MODE,
1055 GLOBAL_FIXCLASS_SHORT_THRESHOLD_MS,
1056 GLOBAL_FIXCLASS_SHORT_SYMBOL,
1057 GLOBAL_FIXCLASS_SHORT_COLOR,
1058 GLOBAL_FIXCLASS_LONG_MODE,
1059 GLOBAL_FIXCLASS_LONG_THRESHOLD_MS,
1060 GLOBAL_FIXCLASS_LONG_SYMBOL,
1061 GLOBAL_FIXCLASS_LONG_COLOR,
1062 GLOBAL_FIXCLASS_OOB_MODE,
1063 GLOBAL_FIXCLASS_OOB_SYMBOL,
1064 GLOBAL_FIXCLASS_OOB_COLOR,
1065 # BUG-72: the fourth category, always written, until now never read back.
1066 GLOBAL_FIXCLASS_BLINK_MODE,
1067 GLOBAL_FIXCLASS_BLINK_SYMBOL,
1068 GLOBAL_FIXCLASS_BLINK_COLOR,
1069 # compare_view (BUG-72) — a `global_*` key, unlike the view's other two.
1070 GLOBAL_SHOW_COMPARE_LEGEND,
1071 # labels
1072 GLOBAL_SHOW_TITLE,
1073 GLOBAL_SHOW_CAPTION,
1074 GLOBAL_TITLE_PATTERN,
1075 GLOBAL_CAPTION_PATTERN,
1076 # raw_gaze (VIZ-43)
1077 GLOBAL_RAW_GAZE_COLOR,
1078 GLOBAL_RAW_GAZE_MARKER_SIZE,
1079 GLOBAL_RAW_GAZE_OPACITY,
1080 # word_boxes
1081 GLOBAL_WORD_BOX_COLOR,
1082 GLOBAL_WORD_BOX_LINE_OPACITY,
1083 GLOBAL_WORD_BOX_FILL_COLOR,
1084 GLOBAL_WORD_BOX_FILL_OPACITY,
1085 }
1086)
1088# Non-`global_*` keys the same restore writes: the trial picker (`selection`)
1089# and the compare view's own settings.
1090PLOT_CONFIG_OTHER_STATE_KEYS = frozenset(
1091 {
1092 SINGLE_SELECT_TRIAL_MODE,
1093 SINGLE_TRIAL_ID,
1094 SINGLE_TRIAL_CHOSEN,
1095 # CMP-11 — the compare view's own two settings, restored from the
1096 # config's `compare_view` section. They are not `global_*` keys (compare
1097 # mode owns them, not the rail), which is why they live here rather than
1098 # in `PLOT_CONFIG_STATE_KEYS`.
1099 SINGLE_COMPARE_LAYOUT,
1100 SINGLE_COMPARE_STIMULUS,
1101 # BUG-72 — the replay speed, restored from the config's `animation`.
1102 SINGLE_PLAYBACK_SPEED,
1103 # CMP-24 — B's filters, from the config's second `compare` entry.
1104 *COMPARE_B_FILTER_STATE_KEYS,
1105 # Schema 6 — the figure mode, and scanpath B by identity: its dataset,
1106 # its screen, and the one-shot request B's picker resolves (the same
1107 # handoff a `?compare=` link uses).
1108 SINGLE_ANIMATE,
1109 SINGLE_COMPARE_TOGGLE,
1110 COMPARE_SOURCE_STATE_KEY,
1111 PENDING_COMPARE_STATE_KEY,
1112 SINGLE_COMPARE_SCREEN_ID,
1113 # #374 F28 — the config's `export` section.
1114 EXPORT_FIGURE_WIDTH,
1115 EXPORT_FIGURE_WIDTH_UNIT,
1116 EXPORT_FIGURE_DPI,
1117 }
1118)
1121def compare_state_keys(index: int) -> frozenset:
1122 """The `cmp{index}_*` session keys one `compare` config entry restores."""
1123 return frozenset(t.format(idx=index) for t in COMPARE_STATE_KEY_TEMPLATES)
1126#: Session keys that were renamed, each to the keys it now sets. A recovery
1127#: cache or saved design written before the rename still holds the old name.
1128LEGACY_SESSION_KEYS: Mapping[str, tuple[str, ...]] = MappingProxyType(
1129 {
1130 "global_show_title_caption": (GLOBAL_SHOW_TITLE, GLOBAL_SHOW_CAPTION),
1131 "global_show_colorbars": (
1132 GLOBAL_SHOW_FIXATION_COLORBAR,
1133 GLOBAL_SHOW_HEATMAP_COLORBAR,
1134 ),
1135 "global_colorbar_orientation": (
1136 GLOBAL_FIXATION_COLORBAR_ORIENTATION,
1137 GLOBAL_HEATMAP_COLORBAR_ORIENTATION,
1138 ),
1139 "global_colorbar_tickangle": (
1140 GLOBAL_FIXATION_COLORBAR_TICKANGLE,
1141 GLOBAL_HEATMAP_COLORBAR_TICKANGLE,
1142 ),
1143 "global_colorbar_tickfont_size": (
1144 GLOBAL_FIXATION_COLORBAR_TICKFONT_SIZE,
1145 GLOBAL_HEATMAP_COLORBAR_TICKFONT_SIZE,
1146 ),
1147 }
1148)
1151def rename_legacy_keys(values: Mapping) -> dict:
1152 """``values`` (a recovery-cache session or a saved design) with each renamed
1153 key moved to what it now sets; a new key already there wins. Returns a
1154 copy."""
1155 out = dict(values)
1156 for old, new_keys in LEGACY_SESSION_KEYS.items():
1157 if old in out:
1158 value = out.pop(old)
1159 for new in new_keys:
1160 out.setdefault(new, value)
1161 return out
1164def keep_legacy_marker_scale(values: Mapping) -> dict:
1165 """``values`` with the relative marker scale stamped in when it predates it.
1167 Old work keeps its old look. A design or a recovery-cache session saved
1168 before the fixed duration scale existed holds plot settings but no
1169 ``GLOBAL_MARKER_SIZE_SCALE``; left alone it would re-render on the new
1170 default. Anything saved since carries the key (it is seeded with the rest),
1171 so its absence beside other ``global_*`` keys marks the old kind. A mapping
1172 with no plot settings at all is returned unchanged. Returns a copy.
1173 """
1174 from .constants import LEGACY_MARKER_SIZE_SCALE
1176 out = dict(values)
1177 if GLOBAL_MARKER_SIZE_SCALE not in out and any(
1178 str(key).startswith("global_") for key in out
1179 ):
1180 out[GLOBAL_MARKER_SIZE_SCALE] = LEGACY_MARKER_SIZE_SCALE
1181 return out