scanpath-studio 0.44.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.44.0/scanpath_studio.egg-info → scanpath_studio-0.45.0}/PKG-INFO +11 -11
  2. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/pyproject.toml +12 -11
  3. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/__init__.py +3 -2
  4. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/aggregation.py +1 -1
  5. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/api.py +2 -1
  6. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/app.py +9 -13
  7. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/cli.py +70 -2
  8. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/code_snippet.py +59 -0
  9. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/column_names.py +8 -0
  10. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/controls.py +6 -4
  11. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/data.py +20 -10
  12. scanpath_studio-0.45.0/scanpath_studio/datasets.py +2936 -0
  13. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/export.py +2 -2
  14. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/plots.py +95 -16
  15. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/styles.py +17 -5
  16. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/tabs.py +56 -25
  17. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/tour.py +1 -1
  18. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/ui_mode.py +11 -0
  19. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/url_state.py +22 -9
  20. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/utils.py +2 -0
  21. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/wizard.py +647 -9
  22. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0/scanpath_studio.egg-info}/PKG-INFO +11 -11
  23. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio.egg-info/requires.txt +10 -10
  24. scanpath_studio-0.44.0/scanpath_studio/datasets.py +0 -1233
  25. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/LICENSE +0 -0
  26. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/MANIFEST.in +0 -0
  27. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/NOTICE +0 -0
  28. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/README.md +0 -0
  29. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/__main__.py +0 -0
  30. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/alignment.py +0 -0
  31. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/analysis_recipe.py +0 -0
  32. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/animation_export.py +0 -0
  33. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/annotations.py +0 -0
  34. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/assets/scanpath_studio_title_logo.png +0 -0
  35. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/assets/scanpath_studio_title_logo_dark.png +0 -0
  36. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/authoring.py +0 -0
  37. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/authoring_component.py +0 -0
  38. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/build_info.py +0 -0
  39. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/citation.py +0 -0
  40. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/cite_button.py +0 -0
  41. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/color_picker_commit.py +0 -0
  42. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/compare_source.py +0 -0
  43. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/computations.py +0 -0
  44. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/constants.py +0 -0
  45. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/crash_report.py +0 -0
  46. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/data_health.py +0 -0
  47. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/dataset_table.py +0 -0
  48. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/debug_log.py +0 -0
  49. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/desktop_update.py +0 -0
  50. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/easter_egg.py +0 -0
  51. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/experimental_setup.py +0 -0
  52. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/export_status.py +0 -0
  53. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/feedback.py +0 -0
  54. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/fields.py +0 -0
  55. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/file_citation.py +0 -0
  56. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/html_embed.py +0 -0
  57. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/illustration.py +0 -0
  58. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/loading.py +0 -0
  59. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/measures.py +0 -0
  60. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/menu.py +0 -0
  61. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/metadata.py +0 -0
  62. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/model_scanpaths.py +0 -0
  63. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/multipart.py +0 -0
  64. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/onestop_shard.py +0 -0
  65. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/persistence.py +0 -0
  66. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/plot_config.py +0 -0
  67. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/preprocessing.py +0 -0
  68. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/progress.py +0 -0
  69. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/fixations.csv +0 -0
  70. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/fixations.parquet +0 -0
  71. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/ia.csv +0 -0
  72. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/ia.parquet +0 -0
  73. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_a.png +0 -0
  74. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_b.png +0 -0
  75. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_c.png +0 -0
  76. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_d.png +0 -0
  77. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_1_Ele__paragraph.png +0 -0
  78. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_1_Ele__question.png +0 -0
  79. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_a.png +0 -0
  80. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_b.png +0 -0
  81. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_c.png +0 -0
  82. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_d.png +0 -0
  83. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_2_Ele__paragraph.png +0 -0
  84. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_2_Ele__question.png +0 -0
  85. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_a.png +0 -0
  86. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_b.png +0 -0
  87. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_c.png +0 -0
  88. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_d.png +0 -0
  89. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_3_Adv__paragraph.png +0 -0
  90. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_3_Adv__question.png +0 -0
  91. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_a.png +0 -0
  92. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_b.png +0 -0
  93. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_c.png +0 -0
  94. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_d.png +0 -0
  95. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_4_Adv__paragraph.png +0 -0
  96. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_4_Adv__question.png +0 -0
  97. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_a.png +0 -0
  98. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_b.png +0 -0
  99. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_c.png +0 -0
  100. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_d.png +0 -0
  101. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_5_Adv__paragraph.png +0 -0
  102. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_5_Adv__question.png +0 -0
  103. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_a.png +0 -0
  104. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_b.png +0 -0
  105. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_c.png +0 -0
  106. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_d.png +0 -0
  107. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_6_Ele__paragraph.png +0 -0
  108. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_1_6_Ele__question.png +0 -0
  109. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_a.png +0 -0
  110. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_b.png +0 -0
  111. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_c.png +0 -0
  112. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_d.png +0 -0
  113. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_1_Adv__paragraph.png +0 -0
  114. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_1_Adv__question.png +0 -0
  115. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_a.png +0 -0
  116. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_b.png +0 -0
  117. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_c.png +0 -0
  118. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_d.png +0 -0
  119. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_2_Adv__paragraph.png +0 -0
  120. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_2_Adv__question.png +0 -0
  121. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_a.png +0 -0
  122. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_b.png +0 -0
  123. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_c.png +0 -0
  124. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_d.png +0 -0
  125. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_3_Adv__paragraph.png +0 -0
  126. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_3_Adv__question.png +0 -0
  127. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_a.png +0 -0
  128. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_b.png +0 -0
  129. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_c.png +0 -0
  130. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_d.png +0 -0
  131. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_4_Ele__paragraph.png +0 -0
  132. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_4_Ele__question.png +0 -0
  133. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_a.png +0 -0
  134. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_b.png +0 -0
  135. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_c.png +0 -0
  136. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_d.png +0 -0
  137. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_5_Ele__paragraph.png +0 -0
  138. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_5_Ele__question.png +0 -0
  139. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_a.png +0 -0
  140. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_b.png +0 -0
  141. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_c.png +0 -0
  142. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_d.png +0 -0
  143. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_6_Ele__paragraph.png +0 -0
  144. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/2_2_6_Ele__question.png +0 -0
  145. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/images/manifest.csv +0 -0
  146. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/raw_gaze.csv +0 -0
  147. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/sample_data/raw_gaze.parquet +0 -0
  148. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/save_dialog.py +0 -0
  149. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/session_keys.py +0 -0
  150. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/similarity.py +0 -0
  151. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/streamlit_entry.py +0 -0
  152. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/synthetic.py +0 -0
  153. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/truncation_tooltip.py +0 -0
  154. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/update_sample_data.py +0 -0
  155. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/updates.py +0 -0
  156. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/wizard_shell.py +0 -0
  157. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio/wizard_watch.py +0 -0
  158. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio.egg-info/SOURCES.txt +0 -0
  159. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio.egg-info/dependency_links.txt +0 -0
  160. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio.egg-info/entry_points.txt +0 -0
  161. {scanpath_studio-0.44.0 → scanpath_studio-0.45.0}/scanpath_studio.egg-info/top_level.txt +0 -0
  162. {scanpath_studio-0.44.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.44.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.44.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
  """
@@ -3974,7 +3974,8 @@ def figure_code(
3974
3974
  kind: ``"static"``, ``"animation"`` or ``"comparison"``: the builder the snippet
3975
3975
  calls.
3976
3976
  source: How the data is loaded: ``"demo"``, ``"synthetic"``, ``"files"``,
3977
- ``"potec"``, ``"onestop"``, ``"author"``, ``"raw_gaze"`` (a dataset recorded
3977
+ ``"potec"``, ``"onestop"``, ``"multipleye"``, ``"author"``, ``"raw_gaze"``
3978
+ (a dataset recorded
3978
3979
  as raw gaze alone, whose samples at ``source_options["raw_gaze"]`` are the
3979
3980
  data), or ``"unknown"`` for data a snippet can't name.
3980
3981
  source_options: That loader's arguments: ``{"root": …}``, ``{"words": [...],
@@ -6321,7 +6321,7 @@ def _dataset_added_dialog(message: str) -> None:
6321
6321
 
6322
6322
  Raised once by ``main`` on the run after ✅ Add dataset, once the new
6323
6323
  dataset's frames are loaded, so ``message`` carries its counts. Closing it
6324
- 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
6325
6325
  list of datasets does the same later. Buttons are handled by their return
6326
6326
  value, never ``on_click`` — a dialog body is a fragment (see
6327
6327
  ``_leave_dataset_editor_dialog``).
@@ -6329,7 +6329,7 @@ def _dataset_added_dialog(message: str) -> None:
6329
6329
  st.markdown(message)
6330
6330
  open_col, again_col = st.columns(2, gap="small")
6331
6331
  if open_col.button(
6332
- f"{ICONS['view_scanpath']} Open in Scanpath",
6332
+ f"{ICONS['view_scanpath']} Open in Scanpath view",
6333
6333
  key="dataset_added_open",
6334
6334
  type="primary",
6335
6335
  width="stretch",
@@ -6349,7 +6349,7 @@ def _dataset_added_dialog(message: str) -> None:
6349
6349
  st.rerun(scope="app")
6350
6350
  st.caption(
6351
6351
  f"Close this to stay on {ICONS['view_data']} Data Management. "
6352
- "**Open in Scanpath**, under the list of datasets, opens it later."
6352
+ "**Open in Scanpath view**, under the list of datasets, opens it later."
6353
6353
  )
6354
6354
 
6355
6355
 
@@ -9071,12 +9071,8 @@ def _render_backup_reminder(host, active_view: str) -> None:
9071
9071
  return
9072
9072
  box = host.container(key="sps_backup_reminder", border=True)
9073
9073
  box.markdown(
9074
- f"{ICONS['warning']} **This server saves nothing.** Closing or "
9075
- "refreshing the tab loses the datasets you added, their column mappings, "
9076
- "your annotations and designs. Keep the files you uploaded, and export your "
9077
- f"annotations from {ICONS['view_data']} **Data Management → Annotations** and each "
9078
- "dataset's mapping from "
9079
- 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. "
9080
9076
  f"[How to keep copies of your work ↗]({BACKUP_GUIDE_URL})"
9081
9077
  )
9082
9078
  row = box.container(horizontal=True, gap="small")
@@ -9788,7 +9784,7 @@ def _run_app() -> None:
9788
9784
  dataset_table_slot = setup_source_slot.container(key="tutorial_available_datasets")
9789
9785
  # The two ways to make a dataset are one **+ Add
9790
9786
  # dataset** menu (the Scanpath picker's + with its name spelled out), under
9791
- # 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
9792
9788
  # marks Current. Filled once `data_choice` is known.
9793
9789
  add_dataset_slot = (
9794
9790
  setup_source_slot.container(key="data_page_add", horizontal=True)
@@ -9843,7 +9839,7 @@ def _run_app() -> None:
9843
9839
  # picked from the list: the view switch is a request, since this is
9844
9840
  # an `on_click` callback (see `url_state._go_scanpath`).
9845
9841
  add_dataset_slot.button(
9846
- "Open in Scanpath",
9842
+ "Open in Scanpath view",
9847
9843
  icon=ICONS["view_scanpath"],
9848
9844
  key="data_open_in_scanpath",
9849
9845
  on_click=_go_scanpath,
@@ -10362,10 +10358,10 @@ def _run_app() -> None:
10362
10358
  st.session_state[TRIAL_IDENTITY_CHECK_KEY] = asked_by
10363
10359
  if added:
10364
10360
  # Say what ✅ Add dataset brought in, and offer the next step: a
10365
- # dialog with Open in Scanpath / Add another. The Trial ID verdict
10361
+ # dialog with Open in Scanpath view / Add another. The Trial ID verdict
10366
10362
  # takes the run's one dialog when it has something to say, and any
10367
10363
  # other dialog already open wins too; the counts then go to a toast,
10368
- # and **Open in Scanpath** under the list is the way on.
10364
+ # and **Open in Scanpath view** under the list is the way on.
10369
10365
  message = dataset_added_message(str(added), words_all, fixations_all)
10370
10366
  offered = False
10371
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:
@@ -87,6 +87,7 @@ SOURCE_FILES = "files"
87
87
  SOURCE_AUTHOR = "author"
88
88
  SOURCE_POTEC = "potec"
89
89
  SOURCE_ONESTOP = "onestop"
90
+ SOURCE_MULTIPLEYE = "multipleye"
90
91
  #: A dataset recorded as raw gaze alone — no words or fixations table,
91
92
  #: so its data half is the samples (``options["raw_gaze"]``, the paths) and the
92
93
  #: builder is handed ``None`` for the other two.
@@ -155,6 +156,8 @@ def source_canvas(kind: str) -> tuple[int, int] | None:
155
156
  return (1680, 1050) # PoTeC monitor (DELL P2210)
156
157
  if kind == SOURCE_AUTHOR:
157
158
  return (1200, 800)
159
+ # MultiplEYE stamps each session set's own monitor on its fixations, so it
160
+ # is read off the data like any other dataset's.
158
161
  return None
159
162
 
160
163
 
@@ -306,6 +309,19 @@ def _onestop_python(source: SnippetSource) -> list[str]:
306
309
  return lines
307
310
 
308
311
 
312
+ #: The folder a MultiplEYE snippet loads when the dataset was uploaded, and so
313
+ #: has no folder the server could quote.
314
+ MULTIPLEYE_ROOT_PLACEHOLDER = "path/to/MultiplEYE_session_set"
315
+
316
+
317
+ def _multipleye_python(source: SnippetSource) -> list[str]:
318
+ return [
319
+ "words, fixations = sps.load_multipleye(",
320
+ f" {_py(_root(source, MULTIPLEYE_ROOT_PLACEHOLDER))},",
321
+ ")",
322
+ ]
323
+
324
+
309
325
  def _unknown_python(source: SnippetSource) -> list[str]:
310
326
  if source.options.get("keep_columns"):
311
327
  return [
@@ -356,6 +372,10 @@ def _onestop_cli(source: SnippetSource) -> list[str]:
356
372
  return argv
357
373
 
358
374
 
375
+ def _multipleye_cli(source: SnippetSource) -> list[str]:
376
+ return ["--multipleye", _root(source, MULTIPLEYE_ROOT_PLACEHOLDER)]
377
+
378
+
359
379
  def _unknown_cli(source: SnippetSource) -> list[str]:
360
380
  return ["--words", "words.csv", "--fixations", "fixations.csv", *_keep_cli(source)]
361
381
 
@@ -392,6 +412,8 @@ def upload_source(
392
412
  placeholders: an upload has no path the server could quote.
393
413
  """
394
414
  recipe = recipe if isinstance(recipe, dict) else {}
415
+ if isinstance(multipleye := recipe.get("multipleye"), dict):
416
+ return multipleye_source(label, multipleye, edited=bool(recipe.get("schemas")))
395
417
  schemas = recipe.get("schemas") if isinstance(recipe.get("schemas"), dict) else {}
396
418
  steps = [str(s) for s in recipe.get("steps") or ()]
397
419
  notes = [
@@ -452,6 +474,42 @@ def upload_source(
452
474
  )
453
475
 
454
476
 
477
+ def multipleye_source(
478
+ label: str, recipe: dict, *, edited: bool = False
479
+ ) -> SnippetSource:
480
+ """The data half for a MultiplEYE session set added through the app.
481
+
482
+ ``recipe`` is the dataset's ``source_recipe["multipleye"]``: the ``root``
483
+ folder it was read from, ``None`` for an upload. ``load_multipleye`` reads
484
+ the same files the app did, so a folder-added dataset's snippet runs as it
485
+ is; an uploaded
486
+ one names a placeholder folder instead, and the whole session set there may
487
+ hold more than the files uploaded. ``edited`` says ✏️ Edit dataset changed
488
+ the mapping since, which the loader does not replay."""
489
+ root = recipe.get("root")
490
+ notes = []
491
+ if not root:
492
+ notes.append(
493
+ "This dataset was added from uploaded files, so the snippet can't "
494
+ f"name their folder. Replace `{MULTIPLEYE_ROOT_PLACEHOLDER}` with the "
495
+ "MultiplEYE session set they came from. The loader reads the whole "
496
+ "session set, so it can include trials, question screens, reading "
497
+ "measures and page images that the upload left out."
498
+ )
499
+ if edited:
500
+ notes.append(
501
+ "Its mapping was changed in Edit dataset after it was added. The "
502
+ "loader reads the session set as it was added, so make the same "
503
+ "change in your script."
504
+ )
505
+ return SnippetSource(
506
+ kind=SOURCE_MULTIPLEYE,
507
+ label=label,
508
+ options={"root": str(root) if root else None},
509
+ note=" ".join(notes),
510
+ )
511
+
512
+
455
513
  def _compact_schema(schema: dict) -> dict:
456
514
  """A mapping without its unmapped fields, so the snippet stays readable.
457
515
 
@@ -551,6 +609,7 @@ _SOURCE_WRITERS: dict[str, tuple[Any, Any]] = {
551
609
  SOURCE_AUTHOR: (_author_python, _author_cli),
552
610
  SOURCE_POTEC: (_potec_python, _potec_cli),
553
611
  SOURCE_ONESTOP: (_onestop_python, _onestop_cli),
612
+ SOURCE_MULTIPLEYE: (_multipleye_python, _multipleye_cli),
554
613
  SOURCE_RAW_GAZE: (_raw_gaze_only_python, _raw_gaze_only_cli),
555
614
  SOURCE_UPLOAD: (_upload_python, _upload_cli),
556
615
  SOURCE_UNKNOWN: (_unknown_python, _unknown_cli),
@@ -1012,6 +1012,14 @@ def stored_source_recipe(stored: Mapping) -> dict:
1012
1012
  return {"schemas": schemas, "unresolved": unresolved}
1013
1013
 
1014
1014
 
1015
+ def is_multipleye_entry(stored) -> bool:
1016
+ """Whether a stored dataset was added as a MultiplEYE session set: its
1017
+ ``source_recipe`` records the session set (``"multipleye"``), not a
1018
+ mapping of the user's files."""
1019
+ recipe = stored.get("source_recipe") if isinstance(stored, Mapping) else None
1020
+ return isinstance(recipe, Mapping) and isinstance(recipe.get("multipleye"), Mapping)
1021
+
1022
+
1015
1023
  # --- A frame that carries its own names ---------------------------------------
1016
1024
 
1017
1025
  #: The `DataFrame.attrs` key a frame under the dataset's own names carries its
@@ -8142,8 +8142,8 @@ def render_plot_controls(
8142
8142
  label_visibility="collapsed",
8143
8143
  )
8144
8144
 
8145
- # A stimulus image can come from the dataset (a per-trial
8146
- # `image_path`) OR be uploaded here for any dataset (a
8145
+ # A stimulus image can come from the dataset (MultiplEYE stamps a
8146
+ # per-trial `image_path`) OR be uploaded here for any dataset (a
8147
8147
  # full-monitor screenshot of the reading screen). The upload's `data:`
8148
8148
  # URI is stashed in session for the tab to place + the (pure) collector
8149
8149
  # to read; the switch is enabled whenever either source exists.
@@ -9319,7 +9319,7 @@ _DEFAULT_FILTER_FIELDS = [
9319
9319
  # on every export) and worth filtering on — it is how you exclude the start
9320
9320
  # or the tail of a session when you suspect practice or fatigue effects.
9321
9321
  "TRIAL_INDEX",
9322
- # Common trial-level facets (offered only when the data has them).
9322
+ # MultiplEYE facets (present only when that corpus is loaded).
9323
9323
  "genre",
9324
9324
  "session",
9325
9325
  "is_practice",
@@ -9693,9 +9693,11 @@ _CHIP_DEFAULT_CONDITIONS = [
9693
9693
  "question_preview",
9694
9694
  "repeated_reading_trial",
9695
9695
  "is_correct",
9696
- # Common trial-level facets (shown only when the data has them).
9696
+ # MultiplEYE facets + reader metadata (present only for that corpus).
9697
9697
  "genre",
9698
9698
  "session",
9699
+ "pp_age",
9700
+ "pp_gender",
9699
9701
  ]
9700
9702
  # Virtual chip fields → label. These are computed per trial (not data columns)
9701
9703
  # and always trial-level.
@@ -3813,7 +3813,7 @@ def source_labels(paths: Sequence[str]) -> list[str]:
3813
3813
  def source_file_name(label: str) -> str:
3814
3814
  """The file stem inside a :func:`source_labels` label — without the folders
3815
3815
  that qualify it, its ``#n`` occurrence number or a kept extension — for
3816
- code that parses identity out of a file's name."""
3816
+ code that parses identity out of a file's name (MultiplEYE uploads)."""
3817
3817
  name = re.sub(r"#\d+$", "", str(label).rsplit("/", 1)[-1])
3818
3818
  stem = Path(name).stem
3819
3819
  return stem if Path(name).suffix.lower().lstrip(".") in UPLOAD_FILE_TYPES else name
@@ -5744,7 +5744,7 @@ def detect_word_id_offset(words: pd.DataFrame, fixations: pd.DataFrame) -> int:
5744
5744
  .groupby(keys, sort=False)["_id"]
5745
5745
  .agg(["min", "max"])
5746
5746
  )
5747
- joined = w_stats.join(f_stats, how="inner", lsuffix="_w", rsuffix="_f")
5747
+ joined = w_stats.join(f_stats, how="inner", suffixes=("_w", "_f"))
5748
5748
  if joined.empty:
5749
5749
  return 0
5750
5750
  zero_based = joined["min_w"] == 0
@@ -6245,9 +6245,9 @@ WORD_OPTIONAL_FIELDS = [
6245
6245
  ("dspan_ind_end", "dspan_ind_end", "passthrough", "meta"),
6246
6246
  ("is_in_aspan", "is_in_aspan", "boolean", "meta"),
6247
6247
  ("is_in_dspan", "is_in_dspan", "boolean", "meta"),
6248
- # Side data (also see FIX_OPTIONAL_FIELDS): the comprehension questions
6249
- # JSON + the per-trial stimulus-image path + the genre facet, kept so the
6250
- # panels / image layer can read them off the word frame too.
6248
+ # MultiplEYE side-data (also see FIX_OPTIONAL_FIELDS): the comprehension
6249
+ # questions JSON + the per-trial stimulus-image path + the genre facet, kept
6250
+ # so the panels / image layer can read them off the word frame too.
6251
6251
  ("comprehension_questions", "comprehension_questions", "passthrough", "meta"),
6252
6252
  ("image_path", "image_path", "passthrough", "meta"),
6253
6253
  ("image_x", "image_x", "numeric", "meta"),
@@ -6257,9 +6257,12 @@ WORD_OPTIONAL_FIELDS = [
6257
6257
  ("stimulus_font_px", "stimulus_font_px", "numeric", "meta"),
6258
6258
  ("stimulus_font_family", "stimulus_font_family", "passthrough", "meta"),
6259
6259
  ("genre", "genre", "string", "meta"),
6260
- # Multipart screens: what a screen *is* (e.g. `reading` / `question` — a
6261
- # trial can mix both, and their fixations can have different provenance).
6260
+ # Multipart screens: what a screen *is* (`reading` / `question` on
6261
+ # MultiplEYE — a trial mixes both, and their fixations have different
6262
+ # provenance), and which block of a question screen a word belongs to
6263
+ # (`stem` / `target` / `distractor_a`… — a per-word facet).
6262
6264
  ("screen_kind", "screen_kind", "passthrough", "meta"),
6265
+ ("aoi_block", "aoi_block", "passthrough", "meta"),
6263
6266
  ]
6264
6267
 
6265
6268
  FIX_OPTIONAL_FIELDS = [
@@ -6303,9 +6306,10 @@ FIX_OPTIONAL_FIELDS = [
6303
6306
  ("blink", "is_blink", "boolean", "fixation"),
6304
6307
  ("blink_flag", "is_blink", "boolean", "fixation"),
6305
6308
  ("BLINK", "is_blink", "boolean", "fixation"),
6306
- # Trial-level facets + side data → Trial Info chips / filter facets / the
6307
- # comprehension panel / the stimulus-image layer. Carried only when the
6308
- # data has them, so they're inert for other corpora.
6309
+ # MultiplEYE trial-level facets + side-data → Trial Info chips / filter
6310
+ # facets / the comprehension panel / the stimulus-image layer. All are
6311
+ # MultiplEYE-specific source names (carried only when the loader emits them),
6312
+ # so they're inert for other corpora.
6309
6313
  ("genre", "genre", "string", "meta"),
6310
6314
  ("session", "session", "string", "meta"),
6311
6315
  ("participant", "participant", "string", "meta"),
@@ -6317,6 +6321,12 @@ FIX_OPTIONAL_FIELDS = [
6317
6321
  ("image_y", "image_y", "numeric", "meta"),
6318
6322
  ("stimulus_font_px", "stimulus_font_px", "numeric", "meta"),
6319
6323
  ("stimulus_font_family", "stimulus_font_family", "passthrough", "meta"),
6324
+ # Reader metadata merged from participant_data.csv (namespaced pp_*).
6325
+ ("pp_age", "pp_age", "numeric", "meta"),
6326
+ ("pp_gender", "pp_gender", "string", "meta"),
6327
+ ("pp_native_language", "pp_native_language", "string", "meta"),
6328
+ ("pp_years_education", "pp_years_education", "numeric", "meta"),
6329
+ ("pp_education_level", "pp_education_level", "string", "meta"),
6320
6330
  # Which kind of screen a fixation happened on (see the word table).
6321
6331
  ("screen_kind", "screen_kind", "passthrough", "meta"),
6322
6332
  ]