diff --git a/.github/workflows/end2end_test.yml b/.github/workflows/end2end_test.yml
index eee29c3544..cd4f40acb1 100644
--- a/.github/workflows/end2end_test.yml
+++ b/.github/workflows/end2end_test.yml
@@ -199,6 +199,14 @@ jobs:
success_key: e2e-success-evaluations
artifact_name: Playwright Report (Evaluations)
postgres: false
+ - name: Distribution
+ index_script: index_distribution.py
+ playwright_project: distribution
+ shard: "1/1"
+ success_marker: .e2e-success-distribution
+ success_key: e2e-success-distribution
+ artifact_name: Playwright Report (Distribution)
+ postgres: false
- name: General Postgres 1 of 2
index_script: index_general.py
playwright_project: general
diff --git a/lightly_studio/e2e-tests/index_distribution.py b/lightly_studio/e2e-tests/index_distribution.py
index 8a12ff3bc1..7bd950c27a 100644
--- a/lightly_studio/e2e-tests/index_distribution.py
+++ b/lightly_studio/e2e-tests/index_distribution.py
@@ -60,6 +60,10 @@
DATASET_NAME = "distribution_example_dataset"
RANDOM_SEED = 42
+COMPARISON_TAG_REVIEWED = "distribution_reviewed"
+COMPARISON_TAG_PRIORITY = "distribution_priority"
+COMPARISON_TAG_EMPTY = "distribution_empty"
+
# Distinct, weighted class pools per type so each distribution has a clear shape.
CLASSIFICATION_CLASSES = ["daytime", "night", "dawn", "dusk", "indoor"]
CLASSIFICATION_WEIGHTS = [0.45, 0.25, 0.15, 0.1, 0.05]
@@ -114,6 +118,10 @@ def main() -> None:
samples = list(dataset)
rng = random.Random(RANDOM_SEED)
+ dataset.query()[:32].add_tag(COMPARISON_TAG_REVIEWED)
+ dataset.query()[16:48].add_tag(COMPARISON_TAG_PRIORITY)
+ dataset.query()[:0].add_tag(COMPARISON_TAG_EMPTY)
+
for sample in samples:
annotations: list[CreateAnnotation] = []
diff --git a/lightly_studio_view/e2e/distribution/distribution-comparison.e2e-test.ts b/lightly_studio_view/e2e/distribution/distribution-comparison.e2e-test.ts
new file mode 100644
index 0000000000..5a22d9a63e
--- /dev/null
+++ b/lightly_studio_view/e2e/distribution/distribution-comparison.e2e-test.ts
@@ -0,0 +1,62 @@
+import { expect, test } from '../utils';
+import { distributionComparison } from './fixtures';
+
+test('compares overlapping and empty sample tags without reloading or filtering the grid', async ({
+ page,
+ samplesPage
+}) => {
+ const firstId = await samplesPage.getTagIdByName(distributionComparison.firstTag);
+ const secondId = await samplesPage.getTagIdByName(distributionComparison.secondTag);
+ const emptyId = await samplesPage.getTagIdByName(distributionComparison.emptyTag);
+ expect(firstId).toBeTruthy();
+ expect(secondId).toBeTruthy();
+ expect(emptyId).toBeTruthy();
+
+ await page.getByTestId('side-panel-tabs-distribution').click();
+ await expect(page.getByTestId('dataset-distribution-panel')).toBeVisible();
+ const urlBeforeComparison = page.url();
+ let navigationCount = 0;
+ let imageListRequestCount = 0;
+ const recordNavigation = () => navigationCount++;
+ const recordImageListRequest = (request: { url: () => string }) => {
+ if (request.url().includes('/images/list')) imageListRequestCount++;
+ };
+ page.on('framenavigated', recordNavigation);
+ page.on('request', recordImageListRequest);
+
+ const waitForGroupedCounts = (tagCount: number) =>
+ page.waitForResponse((response) => {
+ if (!response.url().includes('/annotations/count-by-sample-tags')) return false;
+ const body = response.request().postDataJSON() as { sample_tag_ids?: string[] };
+ return response.status() === 200 && body.sample_tag_ids?.length === tagCount;
+ });
+ const selectTag = async (tagId: string, expectedCount: number) => {
+ const option = page.getByTestId(`dataset-distribution-tag-option-${tagId}`);
+ if (!(await option.isVisible())) {
+ await page.getByTestId('dataset-distribution-tag-select').click();
+ }
+ const responsePromise = waitForGroupedCounts(expectedCount);
+ await option.click();
+ return responsePromise;
+ };
+
+ await selectTag(firstId!, 1);
+ await selectTag(secondId!, 2);
+ const response = await selectTag(emptyId!, 3);
+ const requestBody = response.request().postDataJSON() as { sample_tag_ids: string[] };
+ expect(requestBody.sample_tag_ids).toEqual([firstId, secondId, emptyId]);
+ const groupedCounts = (await response.json()) as {
+ sample_tag_id: string;
+ counts: { count: number }[];
+ }[];
+ const emptyCounts = groupedCounts.find((item) => item.sample_tag_id === emptyId)?.counts;
+ expect(emptyCounts?.length).toBeGreaterThan(0);
+ expect(emptyCounts?.every((item) => item.count === 0)).toBe(true);
+
+ await expect(page.getByText(/3 sample tags/)).toBeVisible();
+ expect(page.url()).toBe(urlBeforeComparison);
+ expect(navigationCount).toBe(0);
+ expect(imageListRequestCount).toBe(0);
+ page.off('framenavigated', recordNavigation);
+ page.off('request', recordImageListRequest);
+});
diff --git a/lightly_studio_view/e2e/distribution/fixtures.ts b/lightly_studio_view/e2e/distribution/fixtures.ts
new file mode 100644
index 0000000000..307aeeeea4
--- /dev/null
+++ b/lightly_studio_view/e2e/distribution/fixtures.ts
@@ -0,0 +1,5 @@
+export const distributionComparison = {
+ firstTag: 'distribution_reviewed',
+ secondTag: 'distribution_priority',
+ emptyTag: 'distribution_empty'
+} as const;
diff --git a/lightly_studio_view/playwright.config.ts b/lightly_studio_view/playwright.config.ts
index fe7cf378ef..5e6b6a3337 100644
--- a/lightly_studio_view/playwright.config.ts
+++ b/lightly_studio_view/playwright.config.ts
@@ -50,6 +50,10 @@ export default defineConfig({
{
name: 'evaluations',
testDir: './e2e/evaluations'
+ },
+ {
+ name: 'distribution',
+ testDir: './e2e/distribution'
}
],
diff --git a/lightly_studio_view/src/lib/components/DatasetDistributionPanel/TagComparisonSelect.svelte b/lightly_studio_view/src/lib/components/DatasetDistributionPanel/TagComparisonSelect.svelte
index 876c51b0a0..1f7c6303b4 100644
--- a/lightly_studio_view/src/lib/components/DatasetDistributionPanel/TagComparisonSelect.svelte
+++ b/lightly_studio_view/src/lib/components/DatasetDistributionPanel/TagComparisonSelect.svelte
@@ -49,7 +49,11 @@
No sample tag found.
{#each items as item (item.value)}
- toggle(item.value)}>
+ toggle(item.value)}
+ >