scanpath-studio 0.34.0__tar.gz → 0.35.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.34.0/scanpath_studio.egg-info → scanpath_studio-0.35.0}/PKG-INFO +24 -21
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/README.md +21 -18
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/pyproject.toml +2 -2
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/__init__.py +17 -9
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/aggregation.py +17 -23
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/analysis_recipe.py +3 -3
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/animation_export.py +12 -12
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/annotations.py +25 -12
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/api.py +384 -139
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/app.py +706 -217
- scanpath_studio-0.35.0/scanpath_studio/assets/scanpath_studio_title_logo.png +0 -0
- scanpath_studio-0.35.0/scanpath_studio/assets/scanpath_studio_title_logo_dark.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/authoring.py +12 -7
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/authoring_component.py +1 -1
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/cli.py +531 -229
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/code_snippet.py +163 -64
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/column_names.py +110 -13
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/compare_source.py +2 -2
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/computations.py +117 -119
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/constants.py +28 -3
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/controls.py +892 -371
- scanpath_studio-0.35.0/scanpath_studio/crash_report.py +97 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/data.py +523 -89
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/data_health.py +5 -5
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/dataset_table.py +7 -6
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/datasets.py +16 -13
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/debug_log.py +22 -19
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/experimental_setup.py +9 -9
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/export.py +129 -35
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/export_status.py +2 -1
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/fields.py +21 -3
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/html_embed.py +14 -2
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/illustration.py +10 -3
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/measures.py +15 -38
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/menu.py +4 -9
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/multipart.py +28 -14
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/persistence.py +34 -5
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/plots.py +491 -97
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/session_keys.py +71 -4
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/similarity.py +13 -22
- scanpath_studio-0.35.0/scanpath_studio/streamlit_entry.py +12 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/styles.py +153 -44
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/synthetic.py +8 -9
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/tabs.py +1444 -458
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/tour.py +228 -189
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/url_state.py +309 -58
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/utils.py +74 -20
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/wizard.py +186 -83
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/wizard_shell.py +5 -7
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0/scanpath_studio.egg-info}/PKG-INFO +24 -21
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio.egg-info/SOURCES.txt +6 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio.egg-info/requires.txt +1 -1
- scanpath_studio-0.34.0/scanpath_studio/assets/scanpath_studio_title_logo.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/LICENSE +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/MANIFEST.in +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/NOTICE +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/__main__.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/alignment.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/easter_egg.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/eyegenbench.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/eyegenbench_geometry.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/loading.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/metadata.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/model_scanpaths.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/onestop_shard.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/preprocessing.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/progress.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/fixations.csv +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/fixations.parquet +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/ia.csv +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/ia.parquet +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_a.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_b.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_c.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_1_Ele__answer_d.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_1_Ele__paragraph.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_1_Ele__question.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_a.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_b.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_c.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_2_Ele__answer_d.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_2_Ele__paragraph.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_2_Ele__question.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_a.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_b.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_c.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_3_Adv__answer_d.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_3_Adv__paragraph.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_3_Adv__question.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_a.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_b.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_c.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_4_Adv__answer_d.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_4_Adv__paragraph.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_4_Adv__question.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_a.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_b.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_c.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_5_Adv__answer_d.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_5_Adv__paragraph.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_5_Adv__question.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_a.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_b.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_c.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_6_Ele__answer_d.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_6_Ele__paragraph.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_1_6_Ele__question.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_a.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_b.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_c.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_1_Adv__answer_d.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_1_Adv__paragraph.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_1_Adv__question.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_a.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_b.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_c.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_2_Adv__answer_d.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_2_Adv__paragraph.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_2_Adv__question.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_a.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_b.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_c.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_3_Adv__answer_d.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_3_Adv__paragraph.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_3_Adv__question.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_a.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_b.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_c.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_4_Ele__answer_d.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_4_Ele__paragraph.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_4_Ele__question.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_a.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_b.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_c.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_5_Ele__answer_d.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_5_Ele__paragraph.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_5_Ele__question.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_a.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_b.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_c.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_6_Ele__answer_d.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_6_Ele__paragraph.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/2_2_6_Ele__question.png +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/images/manifest.csv +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/raw_gaze.csv +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/sample_data/raw_gaze.parquet +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/truncation_tooltip.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio/update_sample_data.py +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio.egg-info/dependency_links.txt +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio.egg-info/entry_points.txt +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/scanpath_studio.egg-info/top_level.txt +0 -0
- {scanpath_studio-0.34.0 → scanpath_studio-0.35.0}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scanpath-studio
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Interactive
|
|
3
|
+
Version: 0.35.0
|
|
4
|
+
Summary: Interactive workbench for eye-tracking-while-reading data: draw, replay and compare scanpaths 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>
|
|
7
7
|
License-Expression: MIT
|
|
@@ -23,7 +23,7 @@ Requires-Python: >=3.11
|
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
25
|
License-File: NOTICE
|
|
26
|
-
Requires-Dist: streamlit>=1.
|
|
26
|
+
Requires-Dist: streamlit>=1.65.0
|
|
27
27
|
Requires-Dist: pandas>=3.0
|
|
28
28
|
Requires-Dist: plotly>=7.1
|
|
29
29
|
Requires-Dist: numpy>=2.4
|
|
@@ -65,16 +65,18 @@ Dynamic: license-file
|
|
|
65
65
|
[](https://doi.org/10.5281/zenodo.22933884)
|
|
66
66
|
|
|
67
67
|
**Scanpath Studio shows you how people read.** Load eye-tracking-while-reading
|
|
68
|
-
data and watch each
|
|
69
|
-
screen — then compare
|
|
70
|
-
paper.
|
|
68
|
+
data and watch each trial unfold over the text, exactly where it sat on the
|
|
69
|
+
screen — then compare participants, analyze a corpus, and export figures ready
|
|
70
|
+
for a paper.
|
|
71
71
|
|
|
72
|
-

|
|
73
73
|
|
|
74
74
|
## Get started
|
|
75
75
|
|
|
76
|
-
- **
|
|
77
|
-
|
|
76
|
+
- **As a desktop app**, the easiest way to work with your own data:
|
|
77
|
+
[Windows](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-windows-x86_64.zip) ·
|
|
78
|
+
[macOS (Apple silicon)](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-macos-arm64.dmg) ·
|
|
79
|
+
[Linux](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-linux-x86_64.tar.gz).
|
|
78
80
|
- **With pip** (Python 3.11–3.14):
|
|
79
81
|
|
|
80
82
|
```bash
|
|
@@ -82,30 +84,31 @@ paper.
|
|
|
82
84
|
scanpath-studio # opens the app in your browser
|
|
83
85
|
```
|
|
84
86
|
|
|
85
|
-
- **
|
|
86
|
-
|
|
87
|
-
[Linux](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-linux-x86_64.tar.gz).
|
|
87
|
+
- **In the browser**, to try it on the bundled demo data: the live demo at
|
|
88
|
+
<https://scanpath-studio.streamlit.app>.
|
|
88
89
|
|
|
89
|
-
The
|
|
90
|
-
|
|
90
|
+
The desktop and pip installs keep your data on your own machine, handle large
|
|
91
|
+
datasets, and download the public corpora (PoTeC, OneStop) in one click. The
|
|
92
|
+
hosted demo runs on Streamlit Community Cloud, with limited memory and no
|
|
93
|
+
corpus downloads.
|
|
91
94
|
|
|
92
95
|
## What you can do
|
|
93
96
|
|
|
94
97
|
- **See the reading:** fixations, saccades, heatmaps and raw gaze over the text
|
|
95
98
|
at its true on-screen position, with fixations colored by any column.
|
|
96
99
|
- **Replay it** in real time or faster, and export it as HTML, GIF or MP4.
|
|
97
|
-
- **Compare
|
|
100
|
+
- **Compare participants:** overlay two trials or place them side by side — even
|
|
98
101
|
from two different datasets.
|
|
99
|
-
- **
|
|
100
|
-
|
|
102
|
+
- **Analyze a corpus** per text, participant or group, from the reading
|
|
103
|
+
measures your data brings, each defined in the
|
|
101
104
|
[computation register](https://lacclab.github.io/scanpath-studio/computations/).
|
|
102
105
|
- **Triage, export and share:** tag and filter trials, export one figure or a
|
|
103
106
|
zip for every trial, and share a link that reopens the exact view.
|
|
104
107
|
|
|
105
108
|
| | |
|
|
106
109
|
|:---:|:---:|
|
|
107
|
-
|  |  |
|
|
111
|
+
| A trial, replayed fixation by fixation | Two participants on one paragraph, overlaid ([animated](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/docs/assets/demo_dual_scanpath.gif)) |
|
|
109
112
|
|
|
110
113
|
The app has three views: 🗺️ **Scanpath** for one trial at a time, 📊 **Corpus
|
|
111
114
|
Analysis** for the whole dataset, and 🗂️ **Data Management** for loading and configuring
|
|
@@ -143,8 +146,8 @@ sps.save_figure(fig, "scanpath.html")
|
|
|
143
146
|
|
|
144
147
|
For your own files, pass `--words ia.csv --fixations fix.csv` to `render`, or
|
|
145
148
|
use `sps.load_scanpath_data("ia.csv", "fix.csv")`. HTML output needs nothing
|
|
146
|
-
else; PNG, SVG
|
|
147
|
-
`plotly_get_chrome -y
|
|
149
|
+
else; PNG, SVG and PDF (and GIF/MP4 replays) go through Kaleido, which needs
|
|
150
|
+
Chrome, Chromium or Edge, or run `plotly_get_chrome -y` once. The
|
|
148
151
|
[CLI reference](https://lacclab.github.io/scanpath-studio/cli/) and the
|
|
149
152
|
[Python API reference](https://lacclab.github.io/scanpath-studio/api/) list
|
|
150
153
|
every flag and parameter.
|
|
@@ -11,16 +11,18 @@
|
|
|
11
11
|
[](https://doi.org/10.5281/zenodo.22933884)
|
|
12
12
|
|
|
13
13
|
**Scanpath Studio shows you how people read.** Load eye-tracking-while-reading
|
|
14
|
-
data and watch each
|
|
15
|
-
screen — then compare
|
|
16
|
-
paper.
|
|
14
|
+
data and watch each trial unfold over the text, exactly where it sat on the
|
|
15
|
+
screen — then compare participants, analyze a corpus, and export figures ready
|
|
16
|
+
for a paper.
|
|
17
17
|
|
|
18
|
-

|
|
19
19
|
|
|
20
20
|
## Get started
|
|
21
21
|
|
|
22
|
-
- **
|
|
23
|
-
|
|
22
|
+
- **As a desktop app**, the easiest way to work with your own data:
|
|
23
|
+
[Windows](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-windows-x86_64.zip) ·
|
|
24
|
+
[macOS (Apple silicon)](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-macos-arm64.dmg) ·
|
|
25
|
+
[Linux](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-linux-x86_64.tar.gz).
|
|
24
26
|
- **With pip** (Python 3.11–3.14):
|
|
25
27
|
|
|
26
28
|
```bash
|
|
@@ -28,30 +30,31 @@ paper.
|
|
|
28
30
|
scanpath-studio # opens the app in your browser
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
- **
|
|
32
|
-
|
|
33
|
-
[Linux](https://github.com/lacclab/scanpath-studio/releases/latest/download/ScanpathStudio-linux-x86_64.tar.gz).
|
|
33
|
+
- **In the browser**, to try it on the bundled demo data: the live demo at
|
|
34
|
+
<https://scanpath-studio.streamlit.app>.
|
|
34
35
|
|
|
35
|
-
The
|
|
36
|
-
|
|
36
|
+
The desktop and pip installs keep your data on your own machine, handle large
|
|
37
|
+
datasets, and download the public corpora (PoTeC, OneStop) in one click. The
|
|
38
|
+
hosted demo runs on Streamlit Community Cloud, with limited memory and no
|
|
39
|
+
corpus downloads.
|
|
37
40
|
|
|
38
41
|
## What you can do
|
|
39
42
|
|
|
40
43
|
- **See the reading:** fixations, saccades, heatmaps and raw gaze over the text
|
|
41
44
|
at its true on-screen position, with fixations colored by any column.
|
|
42
45
|
- **Replay it** in real time or faster, and export it as HTML, GIF or MP4.
|
|
43
|
-
- **Compare
|
|
46
|
+
- **Compare participants:** overlay two trials or place them side by side — even
|
|
44
47
|
from two different datasets.
|
|
45
|
-
- **
|
|
46
|
-
|
|
48
|
+
- **Analyze a corpus** per text, participant or group, from the reading
|
|
49
|
+
measures your data brings, each defined in the
|
|
47
50
|
[computation register](https://lacclab.github.io/scanpath-studio/computations/).
|
|
48
51
|
- **Triage, export and share:** tag and filter trials, export one figure or a
|
|
49
52
|
zip for every trial, and share a link that reopens the exact view.
|
|
50
53
|
|
|
51
54
|
| | |
|
|
52
55
|
|:---:|:---:|
|
|
53
|
-
|  |  |
|
|
57
|
+
| A trial, replayed fixation by fixation | Two participants on one paragraph, overlaid ([animated](https://raw.githubusercontent.com/lacclab/scanpath-studio/main/docs/assets/demo_dual_scanpath.gif)) |
|
|
55
58
|
|
|
56
59
|
The app has three views: 🗺️ **Scanpath** for one trial at a time, 📊 **Corpus
|
|
57
60
|
Analysis** for the whole dataset, and 🗂️ **Data Management** for loading and configuring
|
|
@@ -89,8 +92,8 @@ sps.save_figure(fig, "scanpath.html")
|
|
|
89
92
|
|
|
90
93
|
For your own files, pass `--words ia.csv --fixations fix.csv` to `render`, or
|
|
91
94
|
use `sps.load_scanpath_data("ia.csv", "fix.csv")`. HTML output needs nothing
|
|
92
|
-
else; PNG, SVG
|
|
93
|
-
`plotly_get_chrome -y
|
|
95
|
+
else; PNG, SVG and PDF (and GIF/MP4 replays) go through Kaleido, which needs
|
|
96
|
+
Chrome, Chromium or Edge, or run `plotly_get_chrome -y` once. The
|
|
94
97
|
[CLI reference](https://lacclab.github.io/scanpath-studio/cli/) and the
|
|
95
98
|
[Python API reference](https://lacclab.github.io/scanpath-studio/api/) list
|
|
96
99
|
every flag and parameter.
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "scanpath-studio"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "Interactive
|
|
8
|
+
description = "Interactive workbench for eye-tracking-while-reading data: draw, replay and compare scanpaths over the text, and export figures and tables."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
authors = [
|
|
@@ -34,7 +34,7 @@ classifiers = [
|
|
|
34
34
|
"Topic :: Scientific/Engineering :: Visualization",
|
|
35
35
|
]
|
|
36
36
|
dependencies = [
|
|
37
|
-
"streamlit>=1.
|
|
37
|
+
"streamlit>=1.65.0",
|
|
38
38
|
"pandas>=3.0",
|
|
39
39
|
"plotly>=7.1",
|
|
40
40
|
"numpy>=2.4",
|
|
@@ -5,15 +5,12 @@ from __future__ import annotations
|
|
|
5
5
|
__all__ = [
|
|
6
6
|
"ScanpathData",
|
|
7
7
|
"__version__",
|
|
8
|
-
"alignment_sensitivity",
|
|
9
|
-
"analysis_tables",
|
|
10
8
|
"animate_scanpath",
|
|
11
9
|
"build_authored_scanpath",
|
|
12
10
|
"cache_status",
|
|
13
11
|
"check_data_health",
|
|
14
12
|
"clear_cache",
|
|
15
13
|
"compare_scanpaths",
|
|
16
|
-
"compute_word_metrics",
|
|
17
14
|
"figure_code",
|
|
18
15
|
"figure_options",
|
|
19
16
|
"list_parts",
|
|
@@ -31,15 +28,12 @@ __all__ = [
|
|
|
31
28
|
"main",
|
|
32
29
|
"plot_corpus_figure",
|
|
33
30
|
"plot_scanpath",
|
|
34
|
-
"preprocess_data",
|
|
35
31
|
"propose_schema",
|
|
36
|
-
"reader_summary",
|
|
37
32
|
"render_parent_trial",
|
|
38
33
|
"save_figure",
|
|
39
34
|
"save_figure_layers",
|
|
40
|
-
"trial_summary",
|
|
41
35
|
]
|
|
42
|
-
__version__ = "0.
|
|
36
|
+
__version__ = "0.35.0"
|
|
43
37
|
|
|
44
38
|
# Public headless API (see api.py / datasets.py / eyegenbench.py). Resolved lazily so
|
|
45
39
|
# `import scanpath_studio` stays cheap and doesn't pull in pandas/plotly/
|
|
@@ -47,10 +41,23 @@ __version__ = "0.34.0"
|
|
|
47
41
|
# The MultiplEYE and benchmark-corpus loaders stay importable but are left out
|
|
48
42
|
# of `__all__`: those corpora are held back from this release
|
|
49
43
|
# (`constants.multipleye_enabled` / `benchmark_corpora_enabled`).
|
|
44
|
+
# The app's own computed measures are held back the same way
|
|
45
|
+
# (`constants.computed_measures_enabled`): importable, but not advertised —
|
|
46
|
+
# they raise without SCANPATH_EXPERIMENTAL=1 (#374).
|
|
47
|
+
_HELD_BACK_EXPORTS = frozenset(
|
|
48
|
+
{
|
|
49
|
+
"alignment_sensitivity",
|
|
50
|
+
"analysis_tables",
|
|
51
|
+
"compute_word_metrics",
|
|
52
|
+
"preprocess_data",
|
|
53
|
+
"reader_summary",
|
|
54
|
+
"trial_summary",
|
|
55
|
+
}
|
|
56
|
+
)
|
|
50
57
|
_DATASET_EXPORTS = frozenset({"load_potec", "load_multipleye", "load_onestop"})
|
|
51
58
|
_EYEGENBENCH_EXPORTS = frozenset({"load_eyegenbench", "eyegenbench_datasets"})
|
|
52
59
|
_API_EXPORTS = (
|
|
53
|
-
frozenset(__all__)
|
|
60
|
+
(frozenset(__all__) | _HELD_BACK_EXPORTS)
|
|
54
61
|
- {"__version__", "main"}
|
|
55
62
|
- _DATASET_EXPORTS
|
|
56
63
|
- _EYEGENBENCH_EXPORTS
|
|
@@ -78,7 +85,8 @@ def __dir__() -> list:
|
|
|
78
85
|
|
|
79
86
|
|
|
80
87
|
def main() -> None:
|
|
81
|
-
"""
|
|
88
|
+
"""The app's Streamlit script body. To open the app, run `scanpath-studio`;
|
|
89
|
+
from Python, use the headless API instead."""
|
|
82
90
|
from .app import main as _main
|
|
83
91
|
|
|
84
92
|
_main()
|
|
@@ -127,7 +127,7 @@ MEASURES: dict[str, Measure] = {
|
|
|
127
127
|
),
|
|
128
128
|
Measure(
|
|
129
129
|
"fprt",
|
|
130
|
-
"First-pass
|
|
130
|
+
"First-pass reading time — FPRT",
|
|
131
131
|
"words",
|
|
132
132
|
"first_pass_gaze_duration_ms",
|
|
133
133
|
"ms",
|
|
@@ -171,7 +171,7 @@ MEASURES: dict[str, Measure] = {
|
|
|
171
171
|
),
|
|
172
172
|
Measure(
|
|
173
173
|
"landing_distance",
|
|
174
|
-
"
|
|
174
|
+
"Centered landing distance",
|
|
175
175
|
"words",
|
|
176
176
|
"initial_landing_distance",
|
|
177
177
|
"letters",
|
|
@@ -737,7 +737,7 @@ def reader_vs_cohort_values(
|
|
|
737
737
|
*,
|
|
738
738
|
normalize: bool = False,
|
|
739
739
|
) -> dict[str, np.ndarray]:
|
|
740
|
-
"""``{"This
|
|
740
|
+
"""``{"This participant": …, "Cohort": …}`` value arrays for a measure (AN-7)."""
|
|
741
741
|
if frame is None or frame.empty or "participant_id" not in frame.columns:
|
|
742
742
|
return {}
|
|
743
743
|
work = frame.copy()
|
|
@@ -749,7 +749,7 @@ def reader_vs_cohort_values(
|
|
|
749
749
|
me = work.loc[is_target, "_m"].dropna().to_numpy()
|
|
750
750
|
others = work.loc[~is_target, "_m"].dropna().to_numpy()
|
|
751
751
|
if me.size:
|
|
752
|
-
out["This
|
|
752
|
+
out["This participant"] = me
|
|
753
753
|
if others.size:
|
|
754
754
|
out["Cohort"] = others
|
|
755
755
|
return out
|
|
@@ -1286,7 +1286,7 @@ def landing_positions(
|
|
|
1286
1286
|
way the letter position is: counted from where the glyphs end.
|
|
1287
1287
|
|
|
1288
1288
|
Not clipped: a first fixation the word got although it lies outside the box
|
|
1289
|
-
horizontally (
|
|
1289
|
+
horizontally (an imported ``word_id``)
|
|
1290
1290
|
reads below 0 or above 1, rather than piling onto an edge it did not land on.
|
|
1291
1291
|
"""
|
|
1292
1292
|
from .measures import word_glyph_span
|
|
@@ -1578,18 +1578,21 @@ def paired_group_summary(
|
|
|
1578
1578
|
spec_b: Mapping,
|
|
1579
1579
|
*,
|
|
1580
1580
|
agg: str = "mean",
|
|
1581
|
-
spread: str = "SEM",
|
|
1582
1581
|
label_a: str = "Group A",
|
|
1583
1582
|
label_b: str = "Group B",
|
|
1584
1583
|
words: pd.DataFrame | None = None,
|
|
1585
1584
|
fixations: pd.DataFrame | None = None,
|
|
1586
1585
|
normalize: bool = False,
|
|
1587
1586
|
) -> pd.DataFrame:
|
|
1588
|
-
"""Per-measure group
|
|
1589
|
-
|
|
1590
|
-
Returns ``[measure, group, value,
|
|
1591
|
-
word
|
|
1592
|
-
|
|
1587
|
+
"""Per-measure group summaries for the paired bars (AN-20).
|
|
1588
|
+
|
|
1589
|
+
Returns ``[measure, group, value, n_observations]``: ``value`` is ``agg``
|
|
1590
|
+
over every word or fixation of the group pooled across participants, and
|
|
1591
|
+
``n_observations`` counts those pooled values. No error bars this release
|
|
1592
|
+
(#374): a spread over pooled words from the same few participants reads as
|
|
1593
|
+
a precision the data does not have. ``measures`` may mix word- and
|
|
1594
|
+
fixation-level measures; pass ``words``/``fixations`` so each reads its
|
|
1595
|
+
backing frame (``frame`` is the fallback).
|
|
1593
1596
|
"""
|
|
1594
1597
|
label_a, label_b = distinct_group_labels(label_a, label_b)
|
|
1595
1598
|
rows = []
|
|
@@ -1603,24 +1606,15 @@ def paired_group_summary(
|
|
|
1603
1606
|
continue
|
|
1604
1607
|
for spec, label in ((spec_a, label_a), (spec_b, label_b)):
|
|
1605
1608
|
vals = measure_values(apply_group(src, spec), m, normalize=normalize)
|
|
1606
|
-
center = aggregate_value(vals, agg)
|
|
1607
|
-
lo, hi = spread_bounds(vals, center, spread, agg=agg)
|
|
1608
1609
|
rows.append(
|
|
1609
1610
|
{
|
|
1610
1611
|
"measure": m.label,
|
|
1611
1612
|
"group": label,
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
# Plotly error-bar length renders in the wrong direction.
|
|
1615
|
-
"value": center,
|
|
1616
|
-
"err_lo": max(center - lo, 0.0),
|
|
1617
|
-
"err_hi": max(hi - center, 0.0),
|
|
1618
|
-
"n": int(vals.size),
|
|
1613
|
+
"value": aggregate_value(vals, agg),
|
|
1614
|
+
"n_observations": int(vals.size),
|
|
1619
1615
|
}
|
|
1620
1616
|
)
|
|
1621
|
-
return pd.DataFrame(
|
|
1622
|
-
rows, columns=["measure", "group", "value", "err_lo", "err_hi", "n"]
|
|
1623
|
-
)
|
|
1617
|
+
return pd.DataFrame(rows, columns=["measure", "group", "value", "n_observations"])
|
|
1624
1618
|
|
|
1625
1619
|
|
|
1626
1620
|
def group_mean_difference(
|
|
@@ -35,8 +35,8 @@ RECIPE_VERSION = 1
|
|
|
35
35
|
#: What the recipe deliberately leaves out, written into every one.
|
|
36
36
|
EXCLUDES = {
|
|
37
37
|
"figure_settings": (
|
|
38
|
-
"Palette, canvas and fonts
|
|
39
|
-
"Share → File, which holds no trial filters."
|
|
38
|
+
"Palette, canvas and fonts live in the settings file (Scanpath → "
|
|
39
|
+
"Share → File), which holds no trial filters."
|
|
40
40
|
),
|
|
41
41
|
"data": (
|
|
42
42
|
"No table rows and no annotation notes. The dataset is referenced by "
|
|
@@ -133,7 +133,7 @@ def analysis_choices(
|
|
|
133
133
|
if normalize is not None:
|
|
134
134
|
rate = bool(getattr(measure, "is_rate", False))
|
|
135
135
|
out["normalization"] = (
|
|
136
|
-
"z-score
|
|
136
|
+
"z-score per participant" if normalize and not rate else "none"
|
|
137
137
|
)
|
|
138
138
|
if spread:
|
|
139
139
|
out["spread"] = spread
|
|
@@ -121,7 +121,7 @@ class AnimationBudgetError(AnimationExportError):
|
|
|
121
121
|
# `chromium_browser_path` finds an installed Chrome, Chromium or Edge there too.
|
|
122
122
|
CHROME_INSTALL_HINT = (
|
|
123
123
|
"Image export needs Chrome, Chromium or Edge, and none was found. Install one "
|
|
124
|
-
"of them and try again — a pip install
|
|
124
|
+
"of them and try again — or, with a pip install, run `plotly_get_chrome -y`. "
|
|
125
125
|
"The **HTML** export needs no browser."
|
|
126
126
|
)
|
|
127
127
|
|
|
@@ -130,7 +130,7 @@ def chromium_browser_path() -> str | None:
|
|
|
130
130
|
"""Return the browser path Kaleido would use, without launching it.
|
|
131
131
|
|
|
132
132
|
``Chromium.find_browser`` is Choreographer's complete discovery path: it
|
|
133
|
-
|
|
133
|
+
honors ``BROWSER_PATH``, searches ``PATH``, checks platform-specific
|
|
134
134
|
locations for Chrome, Chromium, Edge, Brave, and Vivaldi, and can fall back
|
|
135
135
|
to Choreographer's managed Chrome download. We prefer a system browser: a
|
|
136
136
|
stale managed download must not shadow a working installed Edge/Chrome.
|
|
@@ -187,7 +187,7 @@ def _static_base(fig: go.Figure) -> go.Figure:
|
|
|
187
187
|
truncate the existing entries), so we assign the attributes directly. The
|
|
188
188
|
reserved control band is then reclaimed so the clip isn't topped by an empty
|
|
189
189
|
strip; a slim margin remains for the "Elapsed" annotation. Only the control
|
|
190
|
-
band is reclaimed: the other margins hold a title, a caption, a
|
|
190
|
+
band is reclaimed: the other margins hold a title, a caption, a color bar or
|
|
191
191
|
the coordinate-grid ticks, and are kept as they are. The replay's clock
|
|
192
192
|
on ``layout.meta`` (BUG-93) goes too — only the live player reads it, and it
|
|
193
193
|
would otherwise ride into every frame Kaleido renders.
|
|
@@ -286,14 +286,14 @@ def check_gif_budget(
|
|
|
286
286
|
if total <= budget:
|
|
287
287
|
return
|
|
288
288
|
fits = int(budget) // per_frame
|
|
289
|
-
shorter = f"cap it at {fits} frames or fewer, " if fits >= 1 else ""
|
|
289
|
+
shorter = f"cap it at {fits} frames or fewer, or " if fits >= 1 else ""
|
|
290
290
|
raise AnimationBudgetError(
|
|
291
291
|
f"a {n_frames}-frame GIF at {width}×{height} px and {scale:g}× comes to "
|
|
292
292
|
f"{total / 1e6:,.0f} megapixels of frames, over the "
|
|
293
293
|
f"{budget / 1e6:,.0f}-megapixel limit for one GIF"
|
|
294
294
|
f"{' on this shared server' if shared else ''}. Export **MP4** instead — "
|
|
295
295
|
f"its frames stream straight to the encoder and the file stays small — or "
|
|
296
|
-
f"{shorter}
|
|
296
|
+
f"{shorter}lower the resolution."
|
|
297
297
|
)
|
|
298
298
|
|
|
299
299
|
|
|
@@ -406,7 +406,8 @@ def render_png_frames(
|
|
|
406
406
|
raise AnimationExportError(
|
|
407
407
|
CHROME_INSTALL_HINT
|
|
408
408
|
if not chrome_available()
|
|
409
|
-
else f"
|
|
409
|
+
else f"Frame {k + 1} of {len(frames)} couldn't be drawn "
|
|
410
|
+
f"({exc}). Try again, or export the HTML replay."
|
|
410
411
|
) from exc
|
|
411
412
|
pngs.append(bytes(png))
|
|
412
413
|
if progress_callback is not None:
|
|
@@ -432,7 +433,7 @@ def encode_gif(
|
|
|
432
433
|
) -> bytes:
|
|
433
434
|
"""Encode PNG frames into an animated GIF with a uniform per-frame delay.
|
|
434
435
|
|
|
435
|
-
**Streams** (SEC1 / BUG-74): each PNG is decoded, quantized to a 256-
|
|
436
|
+
**Streams** (SEC1 / BUG-74): each PNG is decoded, quantized to a 256-color
|
|
436
437
|
palette and written before the next is read, so memory holds one decoded frame
|
|
437
438
|
and one palette frame, not the whole clip. Pillow's ``save(save_all=True)``
|
|
438
439
|
cannot do that — it keeps every normalized frame until the end to diff them —
|
|
@@ -556,8 +557,7 @@ def encode_mp4(pngs: list[bytes], frame_duration_ms: float) -> bytes:
|
|
|
556
557
|
raise
|
|
557
558
|
except Exception as exc:
|
|
558
559
|
raise AnimationExportError(
|
|
559
|
-
f"MP4
|
|
560
|
-
"imageio-ffmpeg is installed."
|
|
560
|
+
f"The MP4 couldn't be made ({exc}). Try GIF, or the HTML replay."
|
|
561
561
|
) from exc
|
|
562
562
|
finally:
|
|
563
563
|
try:
|
|
@@ -580,12 +580,12 @@ def export_animation(
|
|
|
580
580
|
"""Render a scanpath-animation figure to GIF or MP4 bytes.
|
|
581
581
|
|
|
582
582
|
Args:
|
|
583
|
-
fig:
|
|
583
|
+
fig: a replay from ``scanpath_studio.animate_scanpath`` (it must have frames).
|
|
584
584
|
fmt: ``"gif"`` or ``"mp4"``.
|
|
585
585
|
frame_duration_ms: uniform per-frame duration. By default the replay's
|
|
586
586
|
own (:func:`plots.animation_clip_frame_ms`), so the clip lasts what
|
|
587
587
|
the on-screen replay does — ``reading span / playback speed``.
|
|
588
|
-
Required for a figure
|
|
588
|
+
Required for a figure ``animate_scanpath`` didn't build.
|
|
589
589
|
scale: Kaleido render scale (1.0 = on-screen px; <1 is faster/smaller,
|
|
590
590
|
>1 is crisper/larger).
|
|
591
591
|
show_elapsed: draw the "Elapsed: X.Xs" readout in the top margin.
|
|
@@ -679,7 +679,7 @@ def export_animation(
|
|
|
679
679
|
emit_status(
|
|
680
680
|
status_callback,
|
|
681
681
|
ExportStage.FINALIZING,
|
|
682
|
-
"
|
|
682
|
+
"Finishing the animation…",
|
|
683
683
|
started_at=started,
|
|
684
684
|
)
|
|
685
685
|
result = bytes(result)
|
|
@@ -172,7 +172,7 @@ def _check_record(index: int, record: object) -> None:
|
|
|
172
172
|
not isinstance(tags, list)
|
|
173
173
|
or any(isinstance(t, bool) or not isinstance(t, _ID_TYPES) for t in tags)
|
|
174
174
|
):
|
|
175
|
-
raise AnnotationsFileError(f"{where}: tags must be a list of
|
|
175
|
+
raise AnnotationsFileError(f"{where}: tags must be a list of text labels")
|
|
176
176
|
note = record.get("note", "")
|
|
177
177
|
if note is not None and not isinstance(note, str):
|
|
178
178
|
raise AnnotationsFileError(f"{where}: note must be text")
|
|
@@ -851,10 +851,14 @@ def render_trial_annotations(
|
|
|
851
851
|
"radio",
|
|
852
852
|
"Annotation scope",
|
|
853
853
|
options=["Parent trial", "This screen"],
|
|
854
|
+
# #374: the stored option keeps its name; "parent" is internal.
|
|
855
|
+
format_func=lambda option: (
|
|
856
|
+
"Whole trial" if option == "Parent trial" else option
|
|
857
|
+
),
|
|
854
858
|
key=scope_key,
|
|
855
859
|
horizontal=True,
|
|
856
|
-
help="
|
|
857
|
-
"
|
|
860
|
+
help="Whole trial: every screen of this trial. This screen: only the "
|
|
861
|
+
"screen on view.",
|
|
858
862
|
)
|
|
859
863
|
if scope == "This screen":
|
|
860
864
|
annotation_screen = str(screen_id)
|
|
@@ -897,7 +901,7 @@ def render_trial_annotations(
|
|
|
897
901
|
f"{ICONS['favorite']} Favorite (star this trial)",
|
|
898
902
|
display=f"{ICONS['favorite']} Favorite",
|
|
899
903
|
key=star_key,
|
|
900
|
-
help="
|
|
904
|
+
help="Star this trial (with scope *This screen*, this screen only).",
|
|
901
905
|
on_change=_save_entry_callback,
|
|
902
906
|
args=save_args,
|
|
903
907
|
)
|
|
@@ -1137,11 +1141,18 @@ def _delete_dataset_annotations(records: list[dict]) -> None:
|
|
|
1137
1141
|
_refresh_dataset_widgets(f"Deleted {_plural(removed, 'annotation')}.")
|
|
1138
1142
|
|
|
1139
1143
|
|
|
1140
|
-
def _annotations_frame(
|
|
1144
|
+
def _annotations_frame(
|
|
1145
|
+
records: list[dict], trials: frozenset, trial_labels=None
|
|
1146
|
+
) -> pd.DataFrame:
|
|
1147
|
+
"""The Annotations table. ``trial_labels`` maps a trial id to the trial
|
|
1148
|
+
picker's label for it (#374 F5: one trial label everywhere)."""
|
|
1149
|
+
shown = trial_labels or {}
|
|
1141
1150
|
frame = pd.DataFrame(
|
|
1142
1151
|
{
|
|
1143
1152
|
"Participant": [r["participant_id"] for r in records],
|
|
1144
|
-
"Trial": [
|
|
1153
|
+
"Trial": [
|
|
1154
|
+
shown.get(str(r["trial_id"]), str(r["trial_id"])) for r in records
|
|
1155
|
+
],
|
|
1145
1156
|
"Screen": [r.get("screen_id", "") for r in records],
|
|
1146
1157
|
"Favorite": [r["star"] for r in records],
|
|
1147
1158
|
"Tags": [r["tags"] for r in records],
|
|
@@ -1159,7 +1170,9 @@ def _annotations_frame(records: list[dict], trials: frozenset) -> pd.DataFrame:
|
|
|
1159
1170
|
return frame
|
|
1160
1171
|
|
|
1161
1172
|
|
|
1162
|
-
def render_dataset_annotations(
|
|
1173
|
+
def render_dataset_annotations(
|
|
1174
|
+
trials, *, dataset_name: str, open_trials=None, trial_labels=None
|
|
1175
|
+
) -> None:
|
|
1163
1176
|
"""🗂️ Data → **Annotations**: every annotation the open dataset holds.
|
|
1164
1177
|
|
|
1165
1178
|
One table — participant, trial, favorite, tags, note — with **Export** (this
|
|
@@ -1178,7 +1191,8 @@ def render_dataset_annotations(trials, *, dataset_name: str, open_trials=None) -
|
|
|
1178
1191
|
|
|
1179
1192
|
``open_trials`` — the trials the Scanpath picker can show, after the trial
|
|
1180
1193
|
filters — gives each row an **Open** button (:func:`_open_annotation`);
|
|
1181
|
-
without it the table has none.
|
|
1194
|
+
without it the table has none. ``trial_labels`` writes each trial as
|
|
1195
|
+
the trial picker does.
|
|
1182
1196
|
"""
|
|
1183
1197
|
trials = _trial_set(trials)
|
|
1184
1198
|
records = store_to_records(_store())
|
|
@@ -1232,12 +1246,11 @@ def render_dataset_annotations(trials, *, dataset_name: str, open_trials=None) -
|
|
|
1232
1246
|
st.caption(
|
|
1233
1247
|
f"{_plural(elsewhere, 'annotation')} here "
|
|
1234
1248
|
f"{'is' if elsewhere == 1 else 'are'} on trials this dataset hasn't "
|
|
1235
|
-
"loaded (*In dataset* unticked)
|
|
1236
|
-
"saved before annotations were kept per dataset. They stay with this "
|
|
1249
|
+
"loaded (*In dataset* unticked). They stay with this "
|
|
1237
1250
|
"dataset; to move them to another, **Export** them here and "
|
|
1238
1251
|
"**Import** them there."
|
|
1239
1252
|
)
|
|
1240
|
-
frame = _annotations_frame(records, trials)
|
|
1253
|
+
frame = _annotations_frame(records, trials, trial_labels)
|
|
1241
1254
|
column_config = {}
|
|
1242
1255
|
if open_trials is not None:
|
|
1243
1256
|
frame.insert(0, "Open", _OPEN_LABEL)
|
|
@@ -1246,7 +1259,7 @@ def render_dataset_annotations(trials, *, dataset_name: str, open_trials=None) -
|
|
|
1246
1259
|
"",
|
|
1247
1260
|
type="tertiary",
|
|
1248
1261
|
width="small",
|
|
1249
|
-
help="Show this
|
|
1262
|
+
help="Show this trial — and its screen, for a screen annotation — "
|
|
1250
1263
|
"in the Scanpath view.",
|
|
1251
1264
|
on_click=_open_annotation,
|
|
1252
1265
|
args=(open_key, records, trials, _trial_set(open_trials)),
|