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

1"""Session-state keys that carry an **external contract** — a wire format. 

2 

3These strings are not local variable names. Each one appears in at least one of 

4three places that outlive the running process: 

5 

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. 

16 

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. 

22 

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. 

26 

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""" 

32 

33from __future__ import annotations 

34 

35from collections.abc import Mapping 

36from types import MappingProxyType 

37 

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" 

183 

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" 

196 

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" 

203 

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" 

234 

235# --- Public-OneStop source options that ride the deep link (DATA-3) --------- 

236ONESTOP_VARIANT = "onestop_variant" 

237ONESTOP_REGIME = "onestop_regime" 

238ONESTOP_PARTS = "onestop_parts" 

239 

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_" 

259 

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" 

279 

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) 

311 

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)) 

318 

319 

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 } 

327 

328 

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" 

335 

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) 

361 

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" 

375 

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" 

382 

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}) 

390 

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. 

396 

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" 

406 

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" 

416 

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" 

453 

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) 

499 

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) 

586 

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) 

610 

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) 

640 

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) 

657 

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) 

665 

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) 

684 

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) 

696 

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) 

716 

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) 

776 

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}) 

781 

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) 

797 

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) 

814 

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) 

829 

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} 

833 

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) 

842 

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) 

893 

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) 

919 

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 

927 

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) 

945 

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) 

1087 

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) 

1119 

1120 

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) 

1124 

1125 

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) 

1149 

1150 

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 

1162 

1163 

1164def keep_legacy_marker_scale(values: Mapping) -> dict: 

1165 """``values`` with the relative marker scale stamped in when it predates it. 

1166 

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 

1175 

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