scanpath-studio 0.43.0__tar.gz → 0.45.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. {scanpath_studio-0.43.0/scanpath_studio.egg-info → scanpath_studio-0.45.0}/PKG-INFO +11 -11
  2. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/pyproject.toml +12 -11
  3. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/__init__.py +3 -2
  4. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/aggregation.py +1 -1
  5. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/api.py +12 -3
  6. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/app.py +79 -31
  7. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/cli.py +70 -2
  8. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/code_snippet.py +59 -0
  9. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/column_names.py +33 -2
  10. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/computations.py +40 -5
  11. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/constants.py +12 -3
  12. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/controls.py +208 -81
  13. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/data.py +522 -101
  14. scanpath_studio-0.45.0/scanpath_studio/datasets.py +2936 -0
  15. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/export.py +2 -2
  16. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/plots.py +150 -27
  17. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/styles.py +55 -9
  18. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/tabs.py +80 -28
  19. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/tour.py +1 -1
  20. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/ui_mode.py +11 -0
  21. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/url_state.py +22 -9
  22. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/utils.py +2 -0
  23. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/wizard.py +673 -9
  24. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0/scanpath_studio.egg-info}/PKG-INFO +11 -11
  25. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio.egg-info/requires.txt +10 -10
  26. scanpath_studio-0.43.0/scanpath_studio/datasets.py +0 -1233
  27. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/LICENSE +0 -0
  28. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/MANIFEST.in +0 -0
  29. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/NOTICE +0 -0
  30. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/README.md +0 -0
  31. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/__main__.py +0 -0
  32. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/alignment.py +0 -0
  33. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/analysis_recipe.py +0 -0
  34. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/animation_export.py +0 -0
  35. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/annotations.py +0 -0
  36. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/assets/scanpath_studio_title_logo.png +0 -0
  37. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/assets/scanpath_studio_title_logo_dark.png +0 -0
  38. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/authoring.py +0 -0
  39. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/authoring_component.py +0 -0
  40. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/build_info.py +0 -0
  41. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/citation.py +0 -0
  42. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/cite_button.py +0 -0
  43. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/color_picker_commit.py +0 -0
  44. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/compare_source.py +0 -0
  45. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/crash_report.py +0 -0
  46. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/data_health.py +0 -0
  47. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/dataset_table.py +0 -0
  48. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/debug_log.py +0 -0
  49. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/desktop_update.py +0 -0
  50. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/easter_egg.py +0 -0
  51. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/experimental_setup.py +0 -0
  52. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/export_status.py +0 -0
  53. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/feedback.py +0 -0
  54. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/fields.py +0 -0
  55. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/file_citation.py +0 -0
  56. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/html_embed.py +0 -0
  57. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/illustration.py +0 -0
  58. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/loading.py +0 -0
  59. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/measures.py +0 -0
  60. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/menu.py +0 -0
  61. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/metadata.py +0 -0
  62. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/model_scanpaths.py +0 -0
  63. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/multipart.py +0 -0
  64. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/onestop_shard.py +0 -0
  65. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/persistence.py +0 -0
  66. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/plot_config.py +0 -0
  67. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/preprocessing.py +0 -0
  68. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/progress.py +0 -0
  69. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/fixations.csv +0 -0
  70. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/fixations.parquet +0 -0
  71. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/ia.csv +0 -0
  72. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/ia.parquet +0 -0
  73. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_a.png +0 -0
  74. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_b.png +0 -0
  75. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_c.png +0 -0
  76. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_d.png +0 -0
  77. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_1_Ele__paragraph.png +0 -0
  78. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_1_Ele__question.png +0 -0
  79. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_a.png +0 -0
  80. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_b.png +0 -0
  81. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_c.png +0 -0
  82. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_d.png +0 -0
  83. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_2_Ele__paragraph.png +0 -0
  84. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_2_Ele__question.png +0 -0
  85. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_a.png +0 -0
  86. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_b.png +0 -0
  87. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_c.png +0 -0
  88. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_d.png +0 -0
  89. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_3_Adv__paragraph.png +0 -0
  90. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_3_Adv__question.png +0 -0
  91. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_a.png +0 -0
  92. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_b.png +0 -0
  93. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_c.png +0 -0
  94. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_d.png +0 -0
  95. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_4_Adv__paragraph.png +0 -0
  96. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_4_Adv__question.png +0 -0
  97. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_a.png +0 -0
  98. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_b.png +0 -0
  99. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_c.png +0 -0
  100. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_d.png +0 -0
  101. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_5_Adv__paragraph.png +0 -0
  102. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_5_Adv__question.png +0 -0
  103. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_a.png +0 -0
  104. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_b.png +0 -0
  105. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_c.png +0 -0
  106. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_d.png +0 -0
  107. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_6_Ele__paragraph.png +0 -0
  108. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_6_Ele__question.png +0 -0
  109. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_a.png +0 -0
  110. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_b.png +0 -0
  111. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_c.png +0 -0
  112. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_d.png +0 -0
  113. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_1_Adv__paragraph.png +0 -0
  114. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_1_Adv__question.png +0 -0
  115. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_a.png +0 -0
  116. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_b.png +0 -0
  117. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_c.png +0 -0
  118. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_d.png +0 -0
  119. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_2_Adv__paragraph.png +0 -0
  120. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_2_Adv__question.png +0 -0
  121. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_a.png +0 -0
  122. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_b.png +0 -0
  123. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_c.png +0 -0
  124. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_d.png +0 -0
  125. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_3_Adv__paragraph.png +0 -0
  126. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_3_Adv__question.png +0 -0
  127. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_a.png +0 -0
  128. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_b.png +0 -0
  129. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_c.png +0 -0
  130. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_d.png +0 -0
  131. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_4_Ele__paragraph.png +0 -0
  132. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_4_Ele__question.png +0 -0
  133. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_a.png +0 -0
  134. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_b.png +0 -0
  135. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_c.png +0 -0
  136. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_d.png +0 -0
  137. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_5_Ele__paragraph.png +0 -0
  138. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_5_Ele__question.png +0 -0
  139. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_a.png +0 -0
  140. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_b.png +0 -0
  141. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_c.png +0 -0
  142. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_d.png +0 -0
  143. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_6_Ele__paragraph.png +0 -0
  144. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_6_Ele__question.png +0 -0
  145. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/manifest.csv +0 -0
  146. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/raw_gaze.csv +0 -0
  147. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/raw_gaze.parquet +0 -0
  148. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/save_dialog.py +0 -0
  149. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/session_keys.py +0 -0
  150. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/similarity.py +0 -0
  151. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/streamlit_entry.py +0 -0
  152. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/synthetic.py +0 -0
  153. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/truncation_tooltip.py +0 -0
  154. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/update_sample_data.py +0 -0
  155. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/updates.py +0 -0
  156. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/wizard_shell.py +0 -0
  157. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio/wizard_watch.py +0 -0
  158. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio.egg-info/SOURCES.txt +0 -0
  159. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio.egg-info/dependency_links.txt +0 -0
  160. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio.egg-info/entry_points.txt +0 -0
  161. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/scanpath_studio.egg-info/top_level.txt +0 -0
  162. {scanpath_studio-0.43.0 → scanpath_studio-0.45.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scanpath-studio
3
- Version: 0.43.0
3
+ Version: 0.45.0
4
4
  Summary: Interactive visualization of eye-tracking data from reading studies: draw, replay and compare scanpaths, fixations, saccades and gaze heatmaps over the text, and export figures and tables.
5
5
  Author: Keren Gruteke Klein, Maya Grossman, Ella Lion, Deborah N. Jakobi, David R. Reich, Lena Jäger, Yevgeni Berzak
6
6
  Author-email: Omer Shubi <lacclab.technion@gmail.com>
@@ -24,20 +24,20 @@ Requires-Python: >=3.11
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  License-File: NOTICE
27
- Requires-Dist: streamlit[performance]>=1.66.0
27
+ Requires-Dist: streamlit[performance]>=1.67.0
28
28
  Requires-Dist: packaging>=26.3
29
- Requires-Dist: pandas>=3.0
29
+ Requires-Dist: pandas>=3.1
30
30
  Requires-Dist: plotly>=7.1
31
31
  Requires-Dist: numpy>=2.4
32
32
  Requires-Dist: scipy>=1.17
33
- Requires-Dist: pyarrow>=25.0
34
- Requires-Dist: kaleido>=1.4
35
- Requires-Dist: watchdog>=6.0.0
33
+ Requires-Dist: pyarrow>=26.0
34
+ Requires-Dist: kaleido>=1.5
35
+ Requires-Dist: watchdog>=7.1.0
36
36
  Requires-Dist: streamlit-sortables>=0.3.1
37
- Requires-Dist: pillow>=12.3
38
- Requires-Dist: imageio[ffmpeg]>=2.37
37
+ Requires-Dist: pillow>=13.0
38
+ Requires-Dist: imageio[ffmpeg]>=2.38
39
39
  Requires-Dist: openpyxl>=3.1.5
40
- Requires-Dist: truststore>=0.10
40
+ Requires-Dist: truststore>=0.10.5
41
41
  Requires-Dist: xlrd>=2.0.2
42
42
  Provides-Extra: test
43
43
  Requires-Dist: pytest>=8.0.0; extra == "test"
@@ -45,7 +45,7 @@ Requires-Dist: pytest-cov>=4.0.0; extra == "test"
45
45
  Requires-Dist: pytest-xdist>=3.0.0; extra == "test"
46
46
  Requires-Dist: pytest-timeout>=2.1.0; extra == "test"
47
47
  Provides-Extra: lint
48
- Requires-Dist: ruff==0.16.8; extra == "lint"
48
+ Requires-Dist: ruff==0.17.4; extra == "lint"
49
49
  Provides-Extra: docs
50
50
  Requires-Dist: mkdocs-material[imaging]>=9.7.7; extra == "docs"
51
51
  Requires-Dist: mkdocstrings[python]>=1.0.6; extra == "docs"
@@ -53,7 +53,7 @@ Requires-Dist: markdown-exec>=1.12.3; extra == "docs"
53
53
  Requires-Dist: mkdocs-redirects>=1.2.3; extra == "docs"
54
54
  Requires-Dist: mkdocs-git-revision-date-localized-plugin>=1.6.0; extra == "docs"
55
55
  Requires-Dist: mkdocs-llmstxt>=0.5.0; extra == "docs"
56
- Requires-Dist: ruff==0.16.8; extra == "docs"
56
+ Requires-Dist: ruff==0.17.4; extra == "docs"
57
57
  Dynamic: license-file
58
58
 
59
59
  # Scanpath Studio
@@ -38,34 +38,35 @@ classifiers = [
38
38
  "Topic :: Scientific/Engineering :: Visualization",
39
39
  ]
40
40
  dependencies = [
41
- "streamlit[performance]>=1.66.0",
41
+ "streamlit[performance]>=1.67.0",
42
42
  # Build versions sort and compare as PEP 440 (`build_info.py`, `updates.py`).
43
43
  "packaging>=26.3",
44
- "pandas>=3.0",
44
+ "pandas>=3.1",
45
45
  "plotly>=7.1",
46
46
  "numpy>=2.4",
47
47
  # The drift-correction port (`alignment.py`); BSD-3, MIT-compatible.
48
48
  "scipy>=1.17",
49
- "pyarrow>=25.0",
50
- "kaleido>=1.4",
49
+ "pyarrow>=26.0",
50
+ "kaleido>=1.5",
51
51
  # Optional Streamlit runtime dep: enables fast event-based file-watching
52
52
  # (hot reload) for `streamlit run`. Not imported by app code — Streamlit
53
53
  # auto-detects it and falls back to polling (+ an "install Watchdog" log) if
54
54
  # absent.
55
- "watchdog>=6.0.0",
55
+ "watchdog>=7.1.0",
56
56
  # Drag-to-reorder the trial chip fields (the chips popover).
57
57
  "streamlit-sortables>=0.3.1",
58
58
  # Rasterized animation export (GIF/MP4): Pillow encodes GIF; imageio's ffmpeg
59
59
  # extra bundles an ffmpeg binary for MP4 (no system package needed).
60
- "pillow>=12.3",
61
- "imageio[ffmpeg]>=2.37",
60
+ "pillow>=13.0",
61
+ "imageio[ffmpeg]>=2.38",
62
62
  # pandas' .xlsx engine. Not optional in practice: the upload path reads
63
- # spreadsheets (`data._read_one`).
63
+ # spreadsheets (`data._read_one`), and MultiplEYE's comprehension questions
64
+ # come as a workbook.
64
65
  "openpyxl>=3.1.5",
65
66
  # Dataset downloads verify HTTPS against the OS certificate store, so a
66
67
  # python.org Python without its CA bundle, or a TLS-inspecting network,
67
68
  # can still fetch OneStop / PoTeC (`datasets._open_url`).
68
- "truststore>=0.10",
69
+ "truststore>=0.10.5",
69
70
  # The Excel 97–2003 (.xls) engine, for the same readers.
70
71
  "xlrd>=2.0.2",
71
72
  ]
@@ -84,7 +85,7 @@ test = [
84
85
  # unpinned ruff means a local run and CI can disagree — a new release lands new
85
86
  # rules and reds a branch that changed nothing. Bump both together.
86
87
  lint = [
87
- "ruff==0.16.8",
88
+ "ruff==0.17.4",
88
89
  ]
89
90
  # Build the documentation site (MkDocs Material + API autodoc from docstrings).
90
91
  # Material caps MkDocs below 2.0, which has no plugin system; every plugin here
@@ -101,7 +102,7 @@ docs = [
101
102
  "mkdocs-llmstxt>=0.5.0",
102
103
  # mkdocstrings wraps the API reference's signatures with it. Pinned with
103
104
  # the `lint` extra's, so the two extras resolve together.
104
- "ruff==0.16.8",
105
+ "ruff==0.17.4",
105
106
  ]
106
107
 
107
108
  [project.urls]
@@ -19,6 +19,7 @@ __all__ = [
19
19
  "list_parts",
20
20
  "list_trials",
21
21
  "load_authored_scanpath",
22
+ "load_multipleye",
22
23
  "load_onestop",
23
24
  "load_participant_metadata",
24
25
  "load_potec",
@@ -42,7 +43,7 @@ __all__ = [
42
43
  # against. `__version__` is the exact build, worked out from it lazily
43
44
  # (build_info.py) — "0.35.0" at the release itself,
44
45
  # "0.35.0.post3+g8f18219" three commits after it.
45
- __release__ = "0.43.0"
46
+ __release__ = "0.45.0"
46
47
 
47
48
  # Public headless API (see api.py / datasets.py). Resolved lazily so
48
49
  # `import scanpath_studio` stays cheap and doesn't pull in pandas/plotly/
@@ -60,7 +61,7 @@ _HELD_BACK_EXPORTS = frozenset(
60
61
  "trial_summary",
61
62
  }
62
63
  )
63
- _DATASET_EXPORTS = frozenset({"load_potec", "load_onestop"})
64
+ _DATASET_EXPORTS = frozenset({"load_potec", "load_multipleye", "load_onestop"})
64
65
  _API_EXPORTS = (
65
66
  (frozenset(__all__) | _HELD_BACK_EXPORTS)
66
67
  - {"__version__", "main"}
@@ -363,7 +363,7 @@ def text_screen_options(frame: pd.DataFrame, text_col: str, text_id) -> list[str
363
363
 
364
364
  Empty for a single-screen dataset — which is every corpus without
365
365
  screen identity, so callers can treat "no options" as "there is nothing to
366
- pick". Ordered by ``screen_index`` when present (it can be ranked by first
366
+ pick". Ordered by ``screen_index`` when present (MultiplEYE ranks it by first
367
367
  fixation onset, i.e. the order the reader actually went through the pages),
368
368
  else by first appearance.
369
369
  """
@@ -840,6 +840,13 @@ def load_scanpath_data(
840
840
  Trials that match none raise a ``data.StimulusJoinWarning`` (a ``UserWarning``).
841
841
  - **Fixations without x/y** but with a word ID are placed at the centers of their
842
842
  word boxes.
843
+ - **Text word IDs.** A word ID column that holds text and no numbers (``w1``,
844
+ ``Hello_3``) is read as keys, kept in a ``word_key`` column (under the column's
845
+ own name with ``names="source"``). Each word's ``word_id`` is then its position
846
+ in its text, 1, 2, … in the Words table's row order (the order of the reading
847
+ that lists the most of the text), one number for every reader, and a
848
+ fixation's is the number of the word with the same text ID in its trial —
849
+ exactly the same text, else none. A text ID never matches a number.
843
850
  - **Bookkeeping columns.** Columns named in ``data.INTERNAL_COLUMNS`` are the
844
851
  pipeline's own; ``data.drop_internal_columns`` removes them.
845
852
 
@@ -887,8 +894,9 @@ def load_scanpath_data(
887
894
  data.StimulusJoinError: A stimulus-level Words table shares neither a trial ID
888
895
  nor a ``text_id`` with any trial (or, multipart, with every screen a trial
889
896
  has fixations on). A ``ValueError``.
890
- data.UnplacedFixationsError: The fixations map no x/y and their word ID column
891
- holds no numbers, so none could be placed. A ``ValueError``.
897
+ data.UnplacedFixationsError: The fixations have no x/y, and not one of their
898
+ word IDs names a word: text IDs the Words table does not have, or numbers
899
+ against a Words table of text IDs. A ``ValueError``.
892
900
  """
893
901
  _check_names_choice(names)
894
902
  if words is None and fixations is None:
@@ -3966,7 +3974,8 @@ def figure_code(
3966
3974
  kind: ``"static"``, ``"animation"`` or ``"comparison"``: the builder the snippet
3967
3975
  calls.
3968
3976
  source: How the data is loaded: ``"demo"``, ``"synthetic"``, ``"files"``,
3969
- ``"potec"``, ``"onestop"``, ``"author"``, ``"raw_gaze"`` (a dataset recorded
3977
+ ``"potec"``, ``"onestop"``, ``"multipleye"``, ``"author"``, ``"raw_gaze"``
3978
+ (a dataset recorded
3970
3979
  as raw gaze alone, whose samples at ``source_options["raw_gaze"]`` are the
3971
3980
  data), or ``"unknown"`` for data a snippet can't name.
3972
3981
  source_options: That loader's arguments: ``{"root": …}``, ``{"words": [...],
@@ -158,6 +158,7 @@ from scanpath_studio.controls import (
158
158
  background_swatch,
159
159
  clear_trial_filter,
160
160
  clear_trial_filters,
161
+ column_mapping_from_state,
161
162
  column_mapping_ui,
162
163
  has_active_trial_filters,
163
164
  read_trial_filters,
@@ -304,7 +305,7 @@ from scanpath_studio.session_keys import (
304
305
  PARAM_CORPUS,
305
306
  PARAM_DATASET,
306
307
  )
307
- from scanpath_studio.styles import get_app_css, widen_menu
308
+ from scanpath_studio.styles import compact_css, get_app_css, widen_menu
308
309
  from scanpath_studio.tabs import (
309
310
  _EDITOR_KEY_NOISE,
310
311
  _REMAP_DIRTY_KEY,
@@ -730,7 +731,7 @@ def configure_page() -> None:
730
731
  page_icon="👀",
731
732
  layout="wide",
732
733
  )
733
- st.markdown(get_app_css(), unsafe_allow_html=True)
734
+ st.html(_app_stylesheet())
734
735
  # Seeded here, not later in the run: the stylesheet below hides one mode's
735
736
  # containers, so it needs the mode on the very first run.
736
737
  ui_mode.seed_mode(st.session_state, st.query_params, _cookies())
@@ -752,6 +753,19 @@ LOGO_PATH = Path(__file__).parent / "assets" / "scanpath_studio_title_logo.png"
752
753
  LOGO_DARK_PATH = LOGO_PATH.with_name("scanpath_studio_title_logo_dark.png")
753
754
 
754
755
 
756
+ @functools.cache
757
+ def _app_stylesheet() -> str:
758
+ """The app stylesheet as sent: compacted, for a style-only `st.html`.
759
+
760
+ Not a markdown block: Streamlit applies a markdown block's raw HTML only
761
+ once its own HTML renderer has loaded and parsed the whole block, which on
762
+ a slow connection left the first screen unstyled for seconds. Built once
763
+ per process — the sheet depends on nothing but module constants, and
764
+ compacting it costs ~10 ms, on every rerun otherwise.
765
+ """
766
+ return compact_css(get_app_css())
767
+
768
+
755
769
  @functools.cache
756
770
  def _logo_theme_css() -> str:
757
771
  """CSS that swaps the header wordmark for its dark variant in dark mode.
@@ -3775,7 +3789,8 @@ def reset_column_mapping() -> None:
3775
3789
  #: ✕ Cancel puts the keys back.
3776
3790
  BUILTIN_MAPPING_HELD_KEY = "_builtin_mapping_held"
3777
3791
  #: The panels' current picks, ``{"words": schema, "fixations": schema}``,
3778
- #: written by `prepare_data` on every run that draws them.
3792
+ #: written by `prepare_data` on every run of a source that has them, drawn or
3793
+ #: read from their keys.
3779
3794
  BUILTIN_MAPPING_PENDING_KEY = "_builtin_mapping_pending"
3780
3795
  #: Set by ✅ Save changes for the success line on the screen it returns to.
3781
3796
  BUILTIN_MAPPING_SAVED_KEY = "_builtin_mapping_saved"
@@ -4040,12 +4055,18 @@ def prepare_data(
4040
4055
  declared_fix_schema: dict | None = None,
4041
4056
  mapping_dataset: object = None,
4042
4057
  held_schemas: dict | None = None,
4058
+ render_mapping: bool = True,
4043
4059
  ) -> tuple[pd.DataFrame, pd.DataFrame, list]:
4044
4060
  """Infer schemas and normalize incoming dataframes to canonical column names.
4045
4061
 
4046
- When ``allow_override`` is True, render the mapping panels that let the
4047
- user pick the exact column names for each field (pre-filled with auto-detection).
4048
- Otherwise just auto-detect.
4062
+ When ``allow_override`` is True, the user's picks in the mapping panels
4063
+ (pre-filled with auto-detection) decide each field. Otherwise just
4064
+ auto-detect.
4065
+
4066
+ ``render_mapping`` says whether those panels are drawn this run. They are
4067
+ only on screen while ✏️ Edit dataset is open, so every other run reads the
4068
+ same picks from session state (`controls.column_mapping_from_state`)
4069
+ instead of building sixty-odd hidden selectboxes.
4049
4070
 
4050
4071
  Returns ``(words_norm, fixations_norm, problems)``. ``problems`` is a list
4051
4072
  of human-readable strings; when it's non-empty the column mapping isn't
@@ -4066,8 +4087,9 @@ def prepare_data(
4066
4087
  happen to match (``controls.forget_mapping_for_other_table``).
4067
4088
 
4068
4089
  ``held_schemas`` (``{"words": …, "fixations": …}``) is the mapping the
4069
- dataset keeps while ✏️ Edit dataset is open: the panels still render and
4070
- their picks are published as the draft (``BUILTIN_MAPPING_PENDING_KEY``),
4090
+ dataset keeps while ✏️ Edit dataset is open: the panels' picks (drawn, or
4091
+ read from their keys on another view) are published as the draft
4092
+ (``BUILTIN_MAPPING_PENDING_KEY``),
4071
4093
  but the frames are normalized under the held mapping until ✅ Save changes.
4072
4094
  """
4073
4095
  has_words = not words_df.empty
@@ -4081,7 +4103,15 @@ def prepare_data(
4081
4103
  word_proposed = _apply_declared_schema(
4082
4104
  _proposed_schema("words", words_df), declared_word_schema
4083
4105
  )
4084
- if allow_override:
4106
+ if allow_override and not render_mapping:
4107
+ word_schema = column_mapping_from_state(
4108
+ words_df,
4109
+ "col_map_words",
4110
+ WORD_FIELD_SPECS,
4111
+ word_proposed,
4112
+ dataset=mapping_dataset,
4113
+ )
4114
+ elif allow_override:
4085
4115
  word_schema = column_mapping_ui(
4086
4116
  words_df,
4087
4117
  table_label="Words (interest areas)",
@@ -4099,6 +4129,7 @@ def prepare_data(
4099
4129
  stack_labels=True,
4100
4130
  dataset=mapping_dataset,
4101
4131
  )
4132
+ if allow_override:
4102
4133
  pending["words"] = word_schema
4103
4134
  if held_schemas and held_schemas.get("words") is not None:
4104
4135
  word_schema = held_schemas["words"]
@@ -4112,7 +4143,15 @@ def prepare_data(
4112
4143
  fix_proposed = _apply_declared_schema(
4113
4144
  _proposed_schema("fixations", fixations_df), declared_fix_schema
4114
4145
  )
4115
- if allow_override:
4146
+ if allow_override and not render_mapping:
4147
+ fix_schema = column_mapping_from_state(
4148
+ fixations_df,
4149
+ "col_map_fix",
4150
+ FIX_FIELD_SPECS,
4151
+ fix_proposed,
4152
+ dataset=mapping_dataset,
4153
+ )
4154
+ elif allow_override:
4116
4155
  fix_schema = column_mapping_ui(
4117
4156
  fixations_df,
4118
4157
  table_label="Fixations",
@@ -4126,6 +4165,7 @@ def prepare_data(
4126
4165
  stack_labels=True,
4127
4166
  dataset=mapping_dataset,
4128
4167
  )
4168
+ if allow_override:
4129
4169
  pending["fixations"] = fix_schema
4130
4170
  if held_schemas and held_schemas.get("fixations") is not None:
4131
4171
  fix_schema = held_schemas["fixations"]
@@ -6281,7 +6321,7 @@ def _dataset_added_dialog(message: str) -> None:
6281
6321
 
6282
6322
  Raised once by ``main`` on the run after ✅ Add dataset, once the new
6283
6323
  dataset's frames are loaded, so ``message`` carries its counts. Closing it
6284
- keeps the user on 🗂️ Data Management, where **Open in Scanpath** under the
6324
+ keeps the user on 🗂️ Data Management, where **Open in Scanpath view** under the
6285
6325
  list of datasets does the same later. Buttons are handled by their return
6286
6326
  value, never ``on_click`` — a dialog body is a fragment (see
6287
6327
  ``_leave_dataset_editor_dialog``).
@@ -6289,7 +6329,7 @@ def _dataset_added_dialog(message: str) -> None:
6289
6329
  st.markdown(message)
6290
6330
  open_col, again_col = st.columns(2, gap="small")
6291
6331
  if open_col.button(
6292
- f"{ICONS['view_scanpath']} Open in Scanpath",
6332
+ f"{ICONS['view_scanpath']} Open in Scanpath view",
6293
6333
  key="dataset_added_open",
6294
6334
  type="primary",
6295
6335
  width="stretch",
@@ -6309,7 +6349,7 @@ def _dataset_added_dialog(message: str) -> None:
6309
6349
  st.rerun(scope="app")
6310
6350
  st.caption(
6311
6351
  f"Close this to stay on {ICONS['view_data']} Data Management. "
6312
- "**Open in Scanpath**, under the list of datasets, opens it later."
6352
+ "**Open in Scanpath view**, under the list of datasets, opens it later."
6313
6353
  )
6314
6354
 
6315
6355
 
@@ -9031,12 +9071,8 @@ def _render_backup_reminder(host, active_view: str) -> None:
9031
9071
  return
9032
9072
  box = host.container(key="sps_backup_reminder", border=True)
9033
9073
  box.markdown(
9034
- f"{ICONS['warning']} **This server saves nothing.** Closing or "
9035
- "refreshing the tab loses the datasets you added, their column mappings, "
9036
- "your annotations and designs. Keep the files you uploaded, and export your "
9037
- f"annotations from {ICONS['view_data']} **Data Management → Annotations** and each "
9038
- "dataset's mapping from "
9039
- f"{ICONS['edit']} **Edit dataset → Download setup file**. "
9074
+ f"{ICONS['warning']} **This server saves nothing.** Refreshing or "
9075
+ "closing the tab loses your datasets, annotations and designs. "
9040
9076
  f"[How to keep copies of your work ↗]({BACKUP_GUIDE_URL})"
9041
9077
  )
9042
9078
  row = box.container(horizontal=True, gap="small")
@@ -9553,11 +9589,14 @@ def _run_app() -> None:
9553
9589
  # The page is built on EVERY run and merely hidden when another view is
9554
9590
  # active (`DATA_PAGE_OFFSCREEN_KEY` → `display: none` in styles.py). That is
9555
9591
  # not laziness: the slots below are filled by the loaders' directory input,
9556
- # ⬇ Download button, source options and column-mapping selectboxes, all of
9557
- # which *drive* `prepare_data` on every rerun — and Streamlit drops the key
9558
- # of a widget that did not render. Rendering-then-hiding keeps every
9559
- # loader's every-run semantics without splitting each into a render/resolve
9560
- # pair.
9592
+ # ⬇ Download button and source options, all of which *drive* the load on
9593
+ # every rerun — and Streamlit drops the key of a widget that did not
9594
+ # render. Rendering-then-hiding keeps every loader's every-run semantics
9595
+ # without splitting each into a render/resolve pair. The one exception is
9596
+ # the built-in sources' column-mapping panels, sixty-odd selectboxes and a
9597
+ # sixth of every rerun's bytes: they are split that way
9598
+ # (`controls.column_mapping_from_state`) and drawn only while ✏️ Edit
9599
+ # dataset shows them.
9561
9600
  #
9562
9601
  # The page has two *parts*, both built every run for the same reason the
9563
9602
  # page itself is (above): the overview always shows, and the editor opens
@@ -9745,7 +9784,7 @@ def _run_app() -> None:
9745
9784
  dataset_table_slot = setup_source_slot.container(key="tutorial_available_datasets")
9746
9785
  # The two ways to make a dataset are one **+ Add
9747
9786
  # dataset** menu (the Scanpath picker's + with its name spelled out), under
9748
- # the list it adds to, beside **Open in Scanpath** for the dataset the list
9787
+ # the list it adds to, beside **Open in Scanpath view** for the dataset the list
9749
9788
  # marks Current. Filled once `data_choice` is known.
9750
9789
  add_dataset_slot = (
9751
9790
  setup_source_slot.container(key="data_page_add", horizontal=True)
@@ -9800,7 +9839,7 @@ def _run_app() -> None:
9800
9839
  # picked from the list: the view switch is a request, since this is
9801
9840
  # an `on_click` callback (see `url_state._go_scanpath`).
9802
9841
  add_dataset_slot.button(
9803
- "Open in Scanpath",
9842
+ "Open in Scanpath view",
9804
9843
  icon=ICONS["view_scanpath"],
9805
9844
  key="data_open_in_scanpath",
9806
9845
  on_click=_go_scanpath,
@@ -9949,8 +9988,9 @@ def _run_app() -> None:
9949
9988
  else:
9950
9989
  deep_link_pid = None
9951
9990
  raw_gaze_df: pd.DataFrame | None = None
9952
- # Did this load already draw the editable pre-normalization mapping panels
9953
- # into the page's Column mapping section? (Mode A — see the dispatch below.)
9991
+ # Is the open source one whose mapping the editable pre-normalization
9992
+ # panels decide, in the page's Column mapping section? (Mode A — see the
9993
+ # dispatch below. They are drawn only while ✏️ Edit dataset is open.)
9954
9994
  mapping_editor_rendered = False
9955
9995
  # Start each load with a clean column-mapping stash; each branch below
9956
9996
  # records the schema it used for the Data page's mapping section.
@@ -10107,8 +10147,11 @@ def _run_app() -> None:
10107
10147
  # default first-load source; pre-filled with auto-detection, so an
10108
10148
  # untouched mapping normalizes identically.
10109
10149
  allow_override=mapping_editor_rendered,
10110
- # Mode A of the Data page's one Column mapping section.
10150
+ # Mode A of the Data page's one Column mapping section, drawn only
10151
+ # while ✏️ Edit dataset shows it; every other run reads the same
10152
+ # picks from their keys.
10111
10153
  mapping_host=mapping_body_slot,
10154
+ render_mapping=data_view and editing,
10112
10155
  # A corpus that publishes its schema starts from it, not from
10113
10156
  # auto-detection. The panels stay editable — this only changes what
10114
10157
  # they start at.
@@ -10154,6 +10197,11 @@ def _run_app() -> None:
10154
10197
  _render_dataset_load_failure(
10155
10198
  _dataset_display_name(_dataset_owner), mapping_problems
10156
10199
  )
10200
+ # The mapping panels on ✏️ Edit dataset are a draft until ✅ Save
10201
+ # changes, and the dataset keeps the mapping that cannot load — so the
10202
+ # editor needs its Save here too, or a field fixed there never applies.
10203
+ if data_view and editing and mapping_editor_rendered:
10204
+ _render_builtin_editor_footer(editor_footer_slot)
10157
10205
  _render_offpage_setup_notice(data_view)
10158
10206
  _finish_page()
10159
10207
  _render_datasets_table(None, None, None)
@@ -10310,10 +10358,10 @@ def _run_app() -> None:
10310
10358
  st.session_state[TRIAL_IDENTITY_CHECK_KEY] = asked_by
10311
10359
  if added:
10312
10360
  # Say what ✅ Add dataset brought in, and offer the next step: a
10313
- # dialog with Open in Scanpath / Add another. The Trial ID verdict
10361
+ # dialog with Open in Scanpath view / Add another. The Trial ID verdict
10314
10362
  # takes the run's one dialog when it has something to say, and any
10315
10363
  # other dialog already open wins too; the counts then go to a toast,
10316
- # and **Open in Scanpath** under the list is the way on.
10364
+ # and **Open in Scanpath view** under the list is the way on.
10317
10365
  message = dataset_added_message(str(added), words_all, fixations_all)
10318
10366
  offered = False
10319
10367
  if not identity_modal:
@@ -30,6 +30,7 @@ from . import column_names as _cn
30
30
  from .code_snippet import (
31
31
  SOURCE_AUTHOR,
32
32
  SOURCE_DEMO,
33
+ SOURCE_MULTIPLEYE,
33
34
  SOURCE_ONESTOP,
34
35
  SOURCE_POTEC,
35
36
  SnippetSource,
@@ -595,6 +596,17 @@ def _render_parser() -> argparse.ArgumentParser:
595
596
  "lacclab, a local lab-processed export with no download.",
596
597
  )
597
598
 
599
+ src.add_argument(
600
+ "--multipleye",
601
+ metavar="DIR",
602
+ help="Load a MultiplEYE session set from DIR, the folder holding its "
603
+ "scanpaths/ or fixations/ folder and its stimuli_*/ folder. A participant "
604
+ "is a session (e.g. 001_ZH_CH_1_ET1) and a trial is one reading of one "
605
+ "stimulus (e.g. 001_ZH_CH_1_ET1_Lit_Alchemist_4), with its pages and "
606
+ "comprehension questions as screens. The screen comes from the session "
607
+ "set's own config; "
608
+ "set the font with --font-size and --font-family.",
609
+ )
598
610
  src.add_argument(
599
611
  "--participant-metadata",
600
612
  metavar="FILE",
@@ -1113,7 +1125,8 @@ def _render_parser() -> argparse.ArgumentParser:
1113
1125
  metavar="WxH",
1114
1126
  help="Screen size in px, e.g. 2560x1440 (default: 2560x1440 for "
1115
1127
  "--sample and --onestop, 1680x1050 for --potec, 1200x800 for "
1116
- "--authoring, otherwise estimated from the data).",
1128
+ "--authoring, the session set's own monitor for --multipleye, "
1129
+ "otherwise estimated from the data).",
1117
1130
  )
1118
1131
  viz.add_argument(
1119
1132
  "--coordinate-grid",
@@ -2377,6 +2390,12 @@ def _snippet_data_source(args) -> SnippetSource:
2377
2390
  "parts": list(args.onestop_part or ["Paragraph"]),
2378
2391
  },
2379
2392
  )
2393
+ if args.multipleye:
2394
+ return cs.SnippetSource(
2395
+ kind=cs.SOURCE_MULTIPLEYE,
2396
+ label="MultiplEYE",
2397
+ options={"root": args.multipleye},
2398
+ )
2380
2399
  if args.words or args.fixations:
2381
2400
  options = {
2382
2401
  "words": list(args.words or []),
@@ -2574,6 +2593,36 @@ def _parse_xy(value: str | None, flag: str = "--stimulus-image-origin") -> tuple
2574
2593
  return (x, y)
2575
2594
 
2576
2595
 
2596
+ def _load_multipleye_render(root: str, participant: str | None):
2597
+ """`render --multipleye`: the session set, normalized like the app's.
2598
+
2599
+ ``participant`` is a session id, matched case-insensitively; given one, only
2600
+ that session is read. Returns ``(data, participant)`` — the loaded frames
2601
+ and the session id as the export spells it.
2602
+ """
2603
+ from .datasets import load_multipleye, multipleye_inventory
2604
+
2605
+ sessions, _ = multipleye_inventory(root)
2606
+ if not sessions:
2607
+ raise FileNotFoundError(
2608
+ f"No MultiplEYE sessions under {root}: expected a scanpaths/ or "
2609
+ "fixations/ folder holding one folder per session."
2610
+ )
2611
+ session = None
2612
+ if participant is not None:
2613
+ want = str(participant).strip().lower()
2614
+ session = next((s for s in sessions if s.lower() == want), None)
2615
+ if session is None:
2616
+ raise ValueError(
2617
+ f"No MultiplEYE session {participant!r} under {root} "
2618
+ f"(sessions: {', '.join(sessions)})."
2619
+ )
2620
+ data = load_multipleye(
2621
+ root, sessions=[session] if session else None, names="canonical"
2622
+ )
2623
+ return data, session
2624
+
2625
+
2577
2626
  def _apply_shared_colorbar_flags(args: argparse.Namespace) -> None:
2578
2627
  """Hand v0.33.0's shared ``--colorbar-*`` values to each bar that was not
2579
2628
  given its own; a bar's own flag wins, wherever it sits on the line.
@@ -2628,6 +2677,7 @@ def render(argv: list[str]) -> None:
2628
2677
  bool(args.authoring),
2629
2678
  bool(args.potec),
2630
2679
  bool(args.onestop),
2680
+ bool(args.multipleye),
2631
2681
  ]
2632
2682
  # Your own tables are words and/or fixations — or raw gaze alone, for a
2633
2683
  # dataset recorded as samples only. Beside any other input, --raw-gaze is
@@ -2636,7 +2686,13 @@ def render(argv: list[str]) -> None:
2636
2686
  bool(args.raw_gaze) and not any(corpus_inputs)
2637
2687
  )
2638
2688
  if sum([*corpus_inputs, own_tables]) != 1:
2639
- inputs = ["--sample", "--authoring PATH", "--potec DIR", "--onestop DIR"]
2689
+ inputs = [
2690
+ "--sample",
2691
+ "--authoring PATH",
2692
+ "--potec DIR",
2693
+ "--onestop DIR",
2694
+ "--multipleye DIR",
2695
+ ]
2640
2696
  raise SystemExit(
2641
2697
  f"Provide exactly one input: {', '.join(inputs)}, or your own tables "
2642
2698
  "(--words and/or --fixations; one of them is enough for "
@@ -2831,6 +2887,18 @@ def render(argv: list[str]) -> None:
2831
2887
  words, fixations = data
2832
2888
  column_names = dict(data.column_names)
2833
2889
  canvas = canvas or source_canvas(SOURCE_ONESTOP)
2890
+ elif args.multipleye:
2891
+ try:
2892
+ data, args.participant = _load_multipleye_render(
2893
+ args.multipleye, args.participant
2894
+ )
2895
+ except (ValueError, FileNotFoundError, OSError) as exc:
2896
+ raise SystemExit(str(exc))
2897
+ words, fixations = data
2898
+ column_names = dict(data.column_names)
2899
+ # Each fixation carries the session set's monitor (`canvas_width`), so
2900
+ # the screen is read off the data.
2901
+ canvas = canvas or source_canvas(SOURCE_MULTIPLEYE)
2834
2902
  else:
2835
2903
  manifest = None
2836
2904
  if args.trial_parts_manifest: