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.
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/MANIFEST.in +1 -1
- {scanpath_studio-0.31.2/scanpath_studio.egg-info → scanpath_studio-0.32.0}/PKG-INFO +5 -2
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/README.md +4 -1
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/pyproject.toml +1 -1
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/__init__.py +1 -1
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/annotations.py +250 -21
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/api.py +51 -7
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/app.py +1818 -1394
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/cli.py +89 -15
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/code_snippet.py +85 -4
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/compare_source.py +6 -2
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/computations.py +12 -12
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/constants.py +41 -11
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/controls.py +561 -87
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/data.py +646 -26
- scanpath_studio-0.32.0/scanpath_studio/dataset_table.py +252 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/datasets.py +160 -43
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/debug_log.py +59 -18
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/export.py +165 -4
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/eyegenbench.py +9 -4
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/illustration.py +22 -7
- scanpath_studio-0.32.0/scanpath_studio/loading.py +926 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/menu.py +45 -114
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/metadata.py +471 -6
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/persistence.py +166 -51
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/plots.py +196 -32
- scanpath_studio-0.32.0/scanpath_studio/progress.py +351 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/session_keys.py +92 -27
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/styles.py +424 -108
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/tabs.py +823 -258
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/tour.py +35 -26
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/url_state.py +179 -97
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/utils.py +37 -12
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/wizard.py +260 -25
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/wizard_shell.py +14 -4
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0/scanpath_studio.egg-info}/PKG-INFO +5 -2
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio.egg-info/SOURCES.txt +6 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/LICENSE +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/NOTICE +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/__main__.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/aggregation.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/alignment.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/animation_export.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/assets/scanpath_studio_title_logo.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/authoring.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/authoring_component.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/easter_egg.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/experimental_setup.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/export_status.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/eyegenbench_geometry.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/fields.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/html_embed.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/measures.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/model_scanpaths.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/multipart.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/onestop_shard.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/preprocessing.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/fixations.csv +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/fixations.parquet +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/ia.csv +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/ia.parquet +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_a.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_b.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_c.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_d.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_1_Ele__paragraph.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_1_Ele__question.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_a.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_b.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_c.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_d.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_2_Ele__paragraph.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_2_Ele__question.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_a.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_b.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_c.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_d.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_3_Adv__paragraph.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_3_Adv__question.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_a.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_b.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_c.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_d.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_4_Adv__paragraph.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_4_Adv__question.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_a.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_b.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_c.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_d.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_5_Adv__paragraph.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_5_Adv__question.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_a.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_b.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_c.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_d.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_6_Ele__paragraph.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_1_6_Ele__question.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_a.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_b.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_c.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_d.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_1_Adv__paragraph.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_1_Adv__question.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_a.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_b.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_c.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_d.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_2_Adv__paragraph.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_2_Adv__question.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_a.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_b.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_c.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_d.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_3_Adv__paragraph.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_3_Adv__question.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_a.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_b.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_c.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_d.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_4_Ele__paragraph.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_4_Ele__question.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_a.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_b.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_c.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_d.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_5_Ele__paragraph.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_5_Ele__question.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_a.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_b.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_c.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_d.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_6_Ele__paragraph.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/2_2_6_Ele__question.png +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/images/manifest.csv +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/raw_gaze.csv +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/sample_data/raw_gaze.parquet +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/similarity.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/synthetic.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio/update_sample_data.py +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio.egg-info/dependency_links.txt +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio.egg-info/entry_points.txt +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio.egg-info/requires.txt +0 -0
- {scanpath_studio-0.31.2 → scanpath_studio-0.32.0}/scanpath_studio.egg-info/top_level.txt +0 -0
- {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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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;
|
|
6
|
-
|
|
7
|
-
(
|
|
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 `
|
|
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
|
|
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
|
|
180
|
-
"""
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
|
349
|
-
#
|
|
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
|
-
"
|
|
355
|
-
"
|
|
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(
|
|
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
|
|
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
|
|
2537
|
-
|
|
2538
|
-
|
|
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()
|