scanpath-studio 0.31.2__tar.gz → 0.32.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 (144) hide show
  1. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/MANIFEST.in +1 -1
  2. {scanpath_studio-0.31.2/scanpath_studio.egg-info → scanpath_studio-0.32.0}/PKG-INFO +5 -2
  3. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/README.md +4 -1
  4. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/pyproject.toml +1 -1
  5. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/__init__.py +1 -1
  6. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/annotations.py +250 -21
  7. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/api.py +51 -7
  8. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/app.py +1818 -1394
  9. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/cli.py +89 -15
  10. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/code_snippet.py +85 -4
  11. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/compare_source.py +6 -2
  12. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/computations.py +12 -12
  13. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/constants.py +41 -11
  14. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/controls.py +561 -87
  15. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/data.py +646 -26
  16. scanpath_studio-0.32.0/scanpath_studio/dataset_table.py +252 -0
  17. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/datasets.py +160 -43
  18. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/debug_log.py +59 -18
  19. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/export.py +165 -4
  20. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/eyegenbench.py +9 -4
  21. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/illustration.py +22 -7
  22. scanpath_studio-0.32.0/scanpath_studio/loading.py +926 -0
  23. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/menu.py +45 -114
  24. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/metadata.py +471 -6
  25. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/persistence.py +166 -51
  26. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/plots.py +196 -32
  27. scanpath_studio-0.32.0/scanpath_studio/progress.py +351 -0
  28. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/session_keys.py +92 -27
  29. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/styles.py +424 -108
  30. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/tabs.py +823 -258
  31. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/tour.py +35 -26
  32. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/url_state.py +179 -97
  33. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/utils.py +37 -12
  34. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/wizard.py +260 -25
  35. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/wizard_shell.py +14 -4
  36. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0/scanpath_studio.egg-info}/PKG-INFO +5 -2
  37. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio.egg-info/SOURCES.txt +6 -0
  38. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/LICENSE +0 -0
  39. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/NOTICE +0 -0
  40. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/__main__.py +0 -0
  41. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/aggregation.py +0 -0
  42. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/alignment.py +0 -0
  43. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/animation_export.py +0 -0
  44. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/assets/scanpath_studio_title_logo.png +0 -0
  45. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/authoring.py +0 -0
  46. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/authoring_component.py +0 -0
  47. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/easter_egg.py +0 -0
  48. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/experimental_setup.py +0 -0
  49. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/export_status.py +0 -0
  50. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/eyegenbench_geometry.py +0 -0
  51. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/fields.py +0 -0
  52. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/html_embed.py +0 -0
  53. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/measures.py +0 -0
  54. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/model_scanpaths.py +0 -0
  55. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/multipart.py +0 -0
  56. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/onestop_shard.py +0 -0
  57. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/preprocessing.py +0 -0
  58. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/fixations.csv +0 -0
  59. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/fixations.parquet +0 -0
  60. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/ia.csv +0 -0
  61. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/ia.parquet +0 -0
  62. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_a.png +0 -0
  63. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_b.png +0 -0
  64. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_c.png +0 -0
  65. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_d.png +0 -0
  66. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_1_Ele__paragraph.png +0 -0
  67. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_1_Ele__question.png +0 -0
  68. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_a.png +0 -0
  69. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_b.png +0 -0
  70. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_c.png +0 -0
  71. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_d.png +0 -0
  72. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_2_Ele__paragraph.png +0 -0
  73. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_2_Ele__question.png +0 -0
  74. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_a.png +0 -0
  75. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_b.png +0 -0
  76. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_c.png +0 -0
  77. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_d.png +0 -0
  78. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_3_Adv__paragraph.png +0 -0
  79. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_3_Adv__question.png +0 -0
  80. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_a.png +0 -0
  81. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_b.png +0 -0
  82. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_c.png +0 -0
  83. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_d.png +0 -0
  84. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_4_Adv__paragraph.png +0 -0
  85. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_4_Adv__question.png +0 -0
  86. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_a.png +0 -0
  87. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_b.png +0 -0
  88. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_c.png +0 -0
  89. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_d.png +0 -0
  90. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_5_Adv__paragraph.png +0 -0
  91. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_5_Adv__question.png +0 -0
  92. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_a.png +0 -0
  93. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_b.png +0 -0
  94. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_c.png +0 -0
  95. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_d.png +0 -0
  96. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_6_Ele__paragraph.png +0 -0
  97. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_6_Ele__question.png +0 -0
  98. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_a.png +0 -0
  99. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_b.png +0 -0
  100. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_c.png +0 -0
  101. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_d.png +0 -0
  102. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_1_Adv__paragraph.png +0 -0
  103. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_1_Adv__question.png +0 -0
  104. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_a.png +0 -0
  105. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_b.png +0 -0
  106. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_c.png +0 -0
  107. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_d.png +0 -0
  108. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_2_Adv__paragraph.png +0 -0
  109. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_2_Adv__question.png +0 -0
  110. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_a.png +0 -0
  111. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_b.png +0 -0
  112. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_c.png +0 -0
  113. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_d.png +0 -0
  114. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_3_Adv__paragraph.png +0 -0
  115. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_3_Adv__question.png +0 -0
  116. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_a.png +0 -0
  117. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_b.png +0 -0
  118. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_c.png +0 -0
  119. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_d.png +0 -0
  120. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_4_Ele__paragraph.png +0 -0
  121. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_4_Ele__question.png +0 -0
  122. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_a.png +0 -0
  123. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_b.png +0 -0
  124. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_c.png +0 -0
  125. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_d.png +0 -0
  126. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_5_Ele__paragraph.png +0 -0
  127. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_5_Ele__question.png +0 -0
  128. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_a.png +0 -0
  129. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_b.png +0 -0
  130. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_c.png +0 -0
  131. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_d.png +0 -0
  132. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_6_Ele__paragraph.png +0 -0
  133. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_6_Ele__question.png +0 -0
  134. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/manifest.csv +0 -0
  135. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/raw_gaze.csv +0 -0
  136. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/raw_gaze.parquet +0 -0
  137. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/similarity.py +0 -0
  138. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/synthetic.py +0 -0
  139. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/update_sample_data.py +0 -0
  140. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio.egg-info/dependency_links.txt +0 -0
  141. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio.egg-info/entry_points.txt +0 -0
  142. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio.egg-info/requires.txt +0 -0
  143. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio.egg-info/top_level.txt +0 -0
  144. {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/setup.cfg +0 -0
@@ -5,5 +5,5 @@ recursive-include scanpath_studio/sample_data *.csv
5
5
  recursive-include scanpath_studio/sample_data *.parquet
6
6
  recursive-include scanpath_studio/sample_data *.png
7
7
  # ENG-61: the suite needs the whole repository (streamlit_app.py, docs/,
8
- # CITATION.cff, tracker/), so a partial copy in the sdist could never run.
8
+ # CITATION.cff), so a partial copy in the sdist could never run.
9
9
  prune tests
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scanpath-studio
3
- Version: 0.31.2
3
+ Version: 0.32.0
4
4
  Summary: Interactive Streamlit workbench for visualizing eye-tracking-while-reading scanpaths, computing reading measures, and exporting figures and tabular data.
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>
@@ -83,7 +83,7 @@ paper.
83
83
  ```
84
84
 
85
85
  - **As a desktop app:** [Windows](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-windows-x86_64.zip) ·
86
- [macOS (Apple silicon)](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-macos-arm64.tar.gz) ·
86
+ [macOS (Apple silicon)](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-macos-arm64.dmg) ·
87
87
  [Linux](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-linux-x86_64.tar.gz).
88
88
 
89
89
  The pip and desktop installs keep your data on your own machine; the hosted demo
@@ -180,6 +180,9 @@ covers setup, the checks that gate CI, and how work is tracked in
180
180
  the architectural map. To preview the docs site locally, run
181
181
  `pip install -e ".[docs]"` and then `mkdocs serve`.
182
182
 
183
+ Taking part means following the
184
+ [Code of Conduct](https://github.com/lacclab/scanpath-studio/blob/main/CODE_OF_CONDUCT.md).
185
+
183
186
  ## Citation
184
187
 
185
188
  A paper is in preparation. Until then, cite the software by its DOI,
@@ -29,7 +29,7 @@ paper.
29
29
  ```
30
30
 
31
31
  - **As a desktop app:** [Windows](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-windows-x86_64.zip) ·
32
- [macOS (Apple silicon)](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-macos-arm64.tar.gz) ·
32
+ [macOS (Apple silicon)](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-macos-arm64.dmg) ·
33
33
  [Linux](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-linux-x86_64.tar.gz).
34
34
 
35
35
  The pip and desktop installs keep your data on your own machine; the hosted demo
@@ -126,6 +126,9 @@ covers setup, the checks that gate CI, and how work is tracked in
126
126
  the architectural map. To preview the docs site locally, run
127
127
  `pip install -e ".[docs]"` and then `mkdocs serve`.
128
128
 
129
+ Taking part means following the
130
+ [Code of Conduct](https://github.com/lacclab/scanpath-studio/blob/main/CODE_OF_CONDUCT.md).
131
+
129
132
  ## Citation
130
133
 
131
134
  A paper is in preparation. Until then, cite the software by its DOI,
@@ -177,7 +177,7 @@ ignore = [
177
177
  # each already carrying a comment saying why the failure is ignorable.
178
178
  "S110", "S112",
179
179
  # Naive local timestamps are correct here: these stamp a local recovery
180
- # cache and a local tracker, both single-machine by design. Making them
180
+ # cache, single-machine by design. Making them
181
181
  # tz-aware would imply a portability this data does not have.
182
182
  "DTZ005", "DTZ006",
183
183
  # Fires on scripts with a shebang that are run via `python <script>`; the
@@ -40,7 +40,7 @@ __all__ = [
40
40
  "save_figure_layers",
41
41
  "trial_summary",
42
42
  ]
43
- __version__ = "0.31.2"
43
+ __version__ = "0.32.0"
44
44
 
45
45
  # Public headless API (see api.py / datasets.py / eyegenbench.py). Resolved lazily so
46
46
  # `import scanpath_studio` stays cheap and doesn't pull in pandas/plotly/
@@ -2,9 +2,9 @@
2
2
 
3
3
  Annotations are keyed by ``(participant_id, trial_id)`` and live in Streamlit
4
4
  session state, so they persist across reruns within a session. There is no
5
- backend; to keep annotations across sessions or share them, the 💾 Session
6
- dialog offers a JSON **download** (a portable sidecar) and **restore**
7
- (re-upload).
5
+ backend; a local run keeps them in the recovery cache, and to share them 🗂️
6
+ Data → **Annotations** exports and imports a dataset's as JSON (UX-174). The
7
+ Scanpath view's Export bundle can include the same file (UX-179).
8
8
 
9
9
  The module is split into a *pure* core (``records_to_store`` /
10
10
  ``store_to_records`` / ``serialize`` / ``deserialize`` — no Streamlit, unit
@@ -15,17 +15,19 @@ tested) and a thin session-backed layer plus the small render helpers used by
15
15
  from __future__ import annotations
16
16
 
17
17
  import json
18
+ from collections.abc import Iterable
18
19
 
20
+ import pandas as pd
19
21
  import streamlit as st
20
22
 
21
- from .constants import ICONS
23
+ from .constants import ICONS, upload_limit_mb
22
24
  from .fields import PANEL_LABEL_W, panel_field, row_label
23
25
 
24
26
  ANNOTATIONS_STATE_KEY = "trial_annotations"
25
27
  SCHEMA_VERSION = 2
26
28
 
27
29
  # Per-trial annotation widgets use this prefix so they can be cleared on import
28
- # (forcing a re-seed from the freshly loaded store), e.g. by `restore_records`.
30
+ # (forcing a re-seed from the freshly loaded store), e.g. by `forget_records`.
29
31
  _WIDGET_PREFIX = "annotrial_"
30
32
 
31
33
  # Always-available tag suggestions (users can add their own on top).
@@ -120,6 +122,59 @@ def deserialize(text: str) -> dict[Key, Entry]:
120
122
  return records_to_store(records)
121
123
 
122
124
 
125
+ def _trial_of(key: Key) -> tuple[str, str]:
126
+ return (key[0], key[1])
127
+
128
+
129
+ def _trial_set(trials: Iterable[tuple[object, object]]) -> frozenset[tuple[str, str]]:
130
+ if isinstance(trials, frozenset):
131
+ return trials # already built by the caller, as strings
132
+ return frozenset((str(pid), str(tid)) for pid, tid in trials)
133
+
134
+
135
+ def records_in(store: dict[Key, Entry], trials) -> list[dict]:
136
+ """The records of ``store`` on the trials in ``trials``, sorted.
137
+
138
+ ``trials`` is ``(participant_id, trial_id)`` pairs — one dataset's trials.
139
+ The store is one per session and keyed by those two ids alone, so this is
140
+ what *a dataset's annotations* means: the ones on trials it has. A screen
141
+ annotation belongs to its parent trial.
142
+ """
143
+ keep = _trial_set(trials)
144
+ return store_to_records(
145
+ {key: entry for key, entry in store.items() if _trial_of(key) in keep}
146
+ )
147
+
148
+
149
+ def merge_records(
150
+ store: dict[Key, Entry], records: list[dict], trials
151
+ ) -> tuple[int, int]:
152
+ """Add ``records`` on the trials in ``trials`` to ``store``, in place.
153
+
154
+ An imported entry replaces the one already on its key — the file is the
155
+ newer statement about that trial — and a record for a trial the dataset
156
+ does not have is skipped, as a backup restore skips what does not match
157
+ (UX-174 r2). Returns ``(applied, skipped)``.
158
+ """
159
+ keep = _trial_set(trials)
160
+ incoming = records_to_store(records)
161
+ applied = {key: entry for key, entry in incoming.items() if _trial_of(key) in keep}
162
+ store.update(applied)
163
+ return len(applied), len(incoming) - len(applied)
164
+
165
+
166
+ def drop_records(store: dict[Key, Entry], records: list[dict]) -> int:
167
+ """Remove the entries ``records`` name from ``store``. Returns how many."""
168
+ removed = 0
169
+ for record in records:
170
+ pid, tid = str(record["participant_id"]), str(record["trial_id"])
171
+ screen_id = record.get("screen_id")
172
+ key = (pid, tid, str(screen_id)) if screen_id not in (None, "") else (pid, tid)
173
+ if store.pop(key, None) is not None:
174
+ removed += 1
175
+ return removed
176
+
177
+
123
178
  # ---------------------------------------------------------------------------
124
179
  # Session-backed layer
125
180
  # ---------------------------------------------------------------------------
@@ -172,21 +227,29 @@ def known_tags() -> list[str]:
172
227
 
173
228
 
174
229
  def current_records() -> list[dict]:
175
- """All annotations as a flat record list — for embedding in a saved config."""
230
+ """All annotations as a flat record list — for the Export bundle (UX-179)."""
176
231
  return store_to_records(_store())
177
232
 
178
233
 
179
- def restore_records(records: list[dict]) -> int:
180
- """Replace the session annotation store from a record list (e.g. a restored
181
- config) and clear the per-trial widget state so editors re-seed. Returns the
182
- number of annotations loaded."""
183
- store = records_to_store(records or [])
184
- st.session_state[ANNOTATIONS_STATE_KEY] = store
234
+ def forget_records(records: list[dict]) -> int:
235
+ """Drop ``records`` from the session store; the trial editors re-seed.
236
+
237
+ Returns how many entries were removed.
238
+ """
239
+ removed = drop_records(_store(), records)
240
+ if removed:
241
+ _reseed_trial_editors()
242
+ return removed
243
+
244
+
245
+ def _reseed_trial_editors() -> None:
246
+ """Drop the per-trial editors' widget state, so they re-seed from the store."""
185
247
  for key in [
186
- k for k in list(st.session_state.keys()) if k.startswith(_WIDGET_PREFIX)
248
+ k
249
+ for k in list(st.session_state.keys())
250
+ if isinstance(k, str) and k.startswith(_WIDGET_PREFIX)
187
251
  ]:
188
252
  del st.session_state[key]
189
- return len(store)
190
253
 
191
254
 
192
255
  # ---------------------------------------------------------------------------
@@ -345,14 +408,11 @@ def render_trial_annotations(
345
408
  note=note,
346
409
  screen_id=annotation_screen,
347
410
  )
348
- # UX-76: no 💾 Open Session button under this. It was a shortcut to a nav
349
- # entry that is one click away in the header (#UX-63), and it sat at the
350
- # foot of a panel about *this trial* pointing at a session-wide one. The
351
- # caption names where the annotations go instead — and no longer says
352
- # "the sidebar", which has not existed since #UX-38.
411
+ # UX-76: no shortcut button under this — the caption names where the
412
+ # whole dataset's annotations are listed instead.
353
413
  st.caption(
354
- "Annotations can be downloaded or restored from **💾 Session** in "
355
- "the top menu."
414
+ "Every annotation on this dataset is listed on 🗂️ **Data → "
415
+ "Annotations**, to export, import or delete."
356
416
  )
357
417
 
358
418
 
@@ -402,3 +462,172 @@ def filter_keys(
402
462
  required_tags=required_tags,
403
463
  excluded_tags=excluded_tags,
404
464
  )
465
+
466
+
467
+ # ---------------------------------------------------------------------------
468
+ # 🗂️ Data → Annotations (UX-174 r2)
469
+ # ---------------------------------------------------------------------------
470
+
471
+ #: The dataset tab's widget keys. The nonce is bumped after an import or a
472
+ #: delete, which gives the uploader and the table fresh keys: an uploader keeps
473
+ #: its file (and would import it again on every rerun), and a table's row
474
+ #: selection is by position, so it would point at other rows once some are gone.
475
+ _DATASET_NONCE_KEY = "_dataset_annotations_nonce"
476
+ _DATASET_NOTE_KEY = "_dataset_annotations_note"
477
+
478
+
479
+ def _dataset_widget_key(name: str) -> str:
480
+ return f"dataset_annotations_{name}_{st.session_state.get(_DATASET_NONCE_KEY, 0)}"
481
+
482
+
483
+ def _refresh_dataset_widgets(note: str) -> None:
484
+ st.session_state[_DATASET_NONCE_KEY] = (
485
+ int(st.session_state.get(_DATASET_NONCE_KEY, 0)) + 1
486
+ )
487
+ st.session_state[_DATASET_NOTE_KEY] = note
488
+ _reseed_trial_editors()
489
+
490
+
491
+ def _plural(count: int, noun: str) -> str:
492
+ return f"{count:,} {noun}{'' if count == 1 else 's'}"
493
+
494
+
495
+ def _import_dataset_annotations(uploader_key: str, trials: frozenset) -> None:
496
+ upload = st.session_state.get(uploader_key)
497
+ if upload is None:
498
+ return
499
+ try:
500
+ records = store_to_records(deserialize(upload.getvalue().decode("utf-8")))
501
+ except (UnicodeDecodeError, ValueError):
502
+ st.session_state[_DATASET_NOTE_KEY] = (
503
+ "error:That file is not an annotations JSON file."
504
+ )
505
+ st.session_state[_DATASET_NONCE_KEY] = (
506
+ int(st.session_state.get(_DATASET_NONCE_KEY, 0)) + 1
507
+ )
508
+ return
509
+ applied, skipped = merge_records(_store(), records, trials)
510
+ note = f"Imported {_plural(applied, 'annotation')}."
511
+ if skipped:
512
+ note += (
513
+ f" Skipped {_plural(skipped, 'annotation')} on trials this dataset "
514
+ "doesn't have."
515
+ )
516
+ _refresh_dataset_widgets(note)
517
+
518
+
519
+ def _delete_dataset_annotations(records: list[dict]) -> None:
520
+ removed = drop_records(_store(), records)
521
+ _refresh_dataset_widgets(f"Deleted {_plural(removed, 'annotation')}.")
522
+
523
+
524
+ def _annotations_frame(records: list[dict]) -> pd.DataFrame:
525
+ frame = pd.DataFrame(
526
+ {
527
+ "Participant": [r["participant_id"] for r in records],
528
+ "Trial": [r["trial_id"] for r in records],
529
+ "Screen": [r.get("screen_id", "") for r in records],
530
+ "Favorite": [r["star"] for r in records],
531
+ "Tags": [r["tags"] for r in records],
532
+ "Note": [r["note"] for r in records],
533
+ }
534
+ )
535
+ if not frame["Screen"].astype(bool).any():
536
+ frame = frame.drop(columns="Screen")
537
+ return frame
538
+
539
+
540
+ def render_dataset_annotations(trials, *, dataset_name: str) -> None:
541
+ """🗂️ Data → **Annotations**: every annotation on the open dataset's trials.
542
+
543
+ One table — participant, trial, favorite, tags, note — with **Export** (this
544
+ dataset's annotations as JSON), **Import** (the same file, or the
545
+ ``annotations.json`` of an Export bundle: entries on trials the dataset has
546
+ are added, the rest are skipped)
547
+ and **Delete**, for the rows ticked in the table. Annotations are still made
548
+ per trial, on 🗺️ Scanpath → Annotations; this is where they are seen whole.
549
+ """
550
+ trials = _trial_set(trials)
551
+ records = records_in(_store(), trials)
552
+ note = st.session_state.pop(_DATASET_NOTE_KEY, None)
553
+ if note and note.startswith("error:"):
554
+ st.error(note.removeprefix("error:"), icon=ICONS["error"])
555
+ elif note:
556
+ st.success(note, icon=ICONS["confirm"])
557
+
558
+ toolbar = st.container(
559
+ key="dataset_annotations_toolbar",
560
+ horizontal=True,
561
+ vertical_alignment="center",
562
+ gap="small",
563
+ )
564
+ toolbar.download_button(
565
+ "Export",
566
+ icon=ICONS["download"],
567
+ data=serialize(records_to_store(records)),
568
+ file_name=f"{_file_slug(dataset_name)}_annotations.json",
569
+ mime="application/json",
570
+ key="dataset_annotations_export",
571
+ disabled=not records,
572
+ help="Download this dataset's annotations as a JSON file.",
573
+ )
574
+ with toolbar.popover("Import", icon=ICONS["upload"]):
575
+ uploader_key = _dataset_widget_key("import")
576
+ st.file_uploader(
577
+ "Annotations file (JSON)",
578
+ type=["json"],
579
+ key=uploader_key,
580
+ on_change=_import_dataset_annotations,
581
+ args=(uploader_key, trials),
582
+ max_upload_size=upload_limit_mb(),
583
+ )
584
+ st.caption(
585
+ "A file exported here or in an Export bundle. Annotations on trials "
586
+ "this dataset doesn't have are skipped, and an imported one replaces "
587
+ "what its trial already had."
588
+ )
589
+ delete_slot = toolbar.container(width="content")
590
+
591
+ if not records:
592
+ st.caption(
593
+ "No annotations on this dataset yet. Star, tag or note a trial on "
594
+ "🗺️ **Scanpath → Annotations**."
595
+ )
596
+ return
597
+ event = st.dataframe(
598
+ _annotations_frame(records),
599
+ hide_index=True,
600
+ width="stretch",
601
+ on_select="rerun",
602
+ selection_mode="multi-row",
603
+ key=_dataset_widget_key("table"),
604
+ column_config={
605
+ "Favorite": st.column_config.CheckboxColumn("Favorite", width="small"),
606
+ "Tags": st.column_config.ListColumn("Tags"),
607
+ "Note": st.column_config.TextColumn("Note", width="large"),
608
+ },
609
+ )
610
+ picked = [records[i] for i in event.selection.rows if i < len(records)]
611
+ with delete_slot.popover(
612
+ f"Delete ({len(picked)})" if picked else "Delete",
613
+ icon=ICONS["delete"],
614
+ disabled=not picked,
615
+ help="Tick rows in the table to delete them.",
616
+ ):
617
+ st.write(
618
+ f"Delete {_plural(len(picked), 'annotation')}? This cannot be undone "
619
+ "— **Export** first to keep a copy."
620
+ )
621
+ st.button(
622
+ "Delete",
623
+ icon=ICONS["delete"],
624
+ key="dataset_annotations_delete",
625
+ type="primary",
626
+ on_click=_delete_dataset_annotations,
627
+ args=(picked,),
628
+ )
629
+
630
+
631
+ def _file_slug(name: str) -> str:
632
+ slug = "".join(ch if ch.isalnum() else "_" for ch in str(name)).strip("_")
633
+ return slug.lower() or "dataset"
@@ -477,7 +477,9 @@ def _schema_error(
477
477
  if not explicit:
478
478
  lines.append(
479
479
  "Matching ignores case and separators (IA_LEFT == ia_left == 'Ia Left') "
480
- "and takes the first candidate that matches."
480
+ "and takes the first candidate that matches; failing that, a vendor "
481
+ "prefix or suffix on a known name (AOI_LEFT, LEFT_px) is tried next, "
482
+ "accepted only when exactly one column qualifies."
481
483
  )
482
484
  hint = (
483
485
  f"An explicit {param} replaces auto-detection wholesale, so it needs every "
@@ -1662,6 +1664,7 @@ def animate_scanpath(
1662
1664
  playback_speed: float = 1.0,
1663
1665
  autoplay: bool = True,
1664
1666
  fix_index_range: tuple[int, int] | None = None,
1667
+ fix_index_range_b: tuple[int, int] | None = None,
1665
1668
  illustration_label: str = "auto",
1666
1669
  title: str = "",
1667
1670
  caption: str = "",
@@ -1695,6 +1698,11 @@ def animate_scanpath(
1695
1698
  the replay timing was changed. ``illustration_label`` accepts ``"auto"``,
1696
1699
  ``"show"``, or ``"hide"`` like [`plot_scanpath`][scanpath_studio.api.plot_scanpath].
1697
1700
 
1701
+ CMP-24: in a co-animation ``fix_index_range`` windows A only (the app's
1702
+ rule — A's slider never cuts B), ``fix_index_range_b`` windows B, and
1703
+ ``fixation_flags_b`` gives B flags of its own (``None``: A's
1704
+ ``fixation_flags``).
1705
+
1698
1706
  ``trial_b=(participant, trial)`` co-animates a second reading on the same
1699
1707
  clock, like the app's Animate + Compare. It is looked up in ``words_b`` /
1700
1708
  ``fixations_b`` when given, else in ``words`` / ``fixations`` — the way
@@ -1790,6 +1798,20 @@ def animate_scanpath(
1790
1798
  fixations_b,
1791
1799
  a_inferred=setup is None and canvas_size is None,
1792
1800
  )
1801
+ full_fix_range_b = None
1802
+ if (
1803
+ fixations_b is not None
1804
+ and not fixations_b.empty
1805
+ and "order_in_trial" in fixations_b.columns
1806
+ ):
1807
+ order_b = pd.to_numeric(fixations_b["order_in_trial"], errors="coerce").dropna()
1808
+ if not order_b.empty:
1809
+ full_fix_range_b = (int(order_b.min()), int(order_b.max()))
1810
+ if fix_index_range_b is not None and fixations_b is not None:
1811
+ pid_b, tid_b = (str(v) for v in (trial_b or ("B", "B")))
1812
+ fixations_b = _apply_fix_index_range(
1813
+ fixations_b, fix_index_range_b, pid_b, tid_b
1814
+ )
1793
1815
  if dataset_b is not None:
1794
1816
  # As `compare_scanpaths` and the app do: B's readers carry their
1795
1817
  # dataset's name, so a hover says whose reader it is.
@@ -1809,6 +1831,12 @@ def animate_scanpath(
1809
1831
  {**animation_overrides, "playback_speed": playback_speed},
1810
1832
  fix_index_range=fix_index_range,
1811
1833
  full_fixation_range=full_fix_range,
1834
+ # CMP-24: B's own flags and window, when it co-animates.
1835
+ fixation_flags_b=animation_overrides.get("fixation_flags_b")
1836
+ if fixations_b is not None
1837
+ else None,
1838
+ fix_index_range_b=fix_index_range_b,
1839
+ full_fixation_range_b=full_fix_range_b,
1812
1840
  )
1813
1841
  animation_overrides["illustration_reasons"] = resolve_label_reasons(
1814
1842
  label_mode, reasons
@@ -2096,6 +2124,7 @@ def compare_scanpaths(
2096
2124
  base_font_size: int = 16,
2097
2125
  font_family: str = FONT_FAMILY,
2098
2126
  fix_index_range: tuple[int, int] | None = None,
2127
+ fix_index_range_b: tuple[int, int] | None = None,
2099
2128
  drift_correction: str | None = None,
2100
2129
  title: str = "",
2101
2130
  caption: str = "",
@@ -2129,6 +2158,16 @@ def compare_scanpaths(
2129
2158
  when the text is identical. Split layouts ignore it; each panel owns its own
2130
2159
  stimulus.
2131
2160
 
2161
+ **Filters, per scanpath (CMP-24).** ``fixation_flags`` and
2162
+ ``saccade_classes`` filter both scanpaths, as they filter
2163
+ [`plot_scanpath`][scanpath_studio.api.plot_scanpath]'s one; the same two keys
2164
+ in ``style_a`` / ``style_b`` give that scanpath its own, overriding them —
2165
+ e.g. ``style_b={"fixation_flags": {"short": {"mode": "Discard",
2166
+ "threshold_ms": 80}}, "saccade_classes": ["regression"]}``. The app's
2167
+ Compare mode draws A under the rail's filters and B under B's own.
2168
+ ``fix_index_range`` windows both scanpaths; ``fix_index_range_b`` gives B a
2169
+ window of its own (the app's B slider).
2170
+
2132
2171
  Remaining keywords are forwarded to `plots.make_comparison_figure`
2133
2172
  (e.g. ``show_words=False``, ``color_by="duration_ms"``); an unknown one
2134
2173
  raises ``TypeError`` naming the closest valid options;
@@ -2217,7 +2256,12 @@ def compare_scanpaths(
2217
2256
  else pid_b
2218
2257
  )
2219
2258
  trial_fix_a = _apply_fix_index_range(trial_fix_a, fix_index_range, pid_a, tid_a)
2220
- trial_fix_b = _apply_fix_index_range(trial_fix_b, fix_index_range, pid_b, tid_b)
2259
+ trial_fix_b = _apply_fix_index_range(
2260
+ trial_fix_b,
2261
+ fix_index_range if fix_index_range_b is None else fix_index_range_b,
2262
+ pid_b,
2263
+ tid_b,
2264
+ )
2221
2265
  if drift_correction:
2222
2266
  # PRE-21: same contract as plot_scanpath — raise, don't silently skip.
2223
2267
  if not drift_correction_enabled():
@@ -2522,7 +2566,8 @@ def cache_status() -> dict:
2522
2566
  ``settings``, ``bytes``, ``saved_at``, plus ``exists`` / ``readable`` for a
2523
2567
  missing or unreadable manifest. Delete it with
2524
2568
  [`clear_cache`][scanpath_studio.api.clear_cache]; the same information is in the
2525
- app's 💾 Session → 🗄️ Automatic recovery panel and in ``scanpath-studio cache``."""
2569
+ app's 🗂️ Data → *Saved on this computer* section and in
2570
+ ``scanpath-studio cache``."""
2526
2571
  from .persistence import cache_status as _cache_status
2527
2572
 
2528
2573
  return _cache_status(url="http://localhost")
@@ -2533,10 +2578,9 @@ def clear_cache() -> dict:
2533
2578
 
2534
2579
  Removes only the files this app wrote (``manifest.json`` and the dataset
2535
2580
  Parquet files); anything else in the folder is left alone. A *running* local
2536
- app writes its session back out at the end of its next run — turn off **Save
2537
- changes automatically** in its 💾 Session → 🗄️ Automatic recovery panel (the
2538
- panel's **Clear recovery cache** button, like this function, leaves saving on),
2539
- or set ``SCANPATH_STUDIO_PERSIST=0``, to stop that."""
2581
+ app writes its session back out at the end of its next change — start it
2582
+ with ``scanpath-studio run --no-persist`` or ``SCANPATH_STUDIO_PERSIST=0`` to
2583
+ stop that."""
2540
2584
  from .persistence import clear_local_state
2541
2585
 
2542
2586
  clear_local_state()