From 90967755e007fdd38cafcf2fa2aa44459341b098 Mon Sep 17 00:00:00 2001 From: ikondrat Date: Tue, 21 Jul 2026 11:16:00 +0200 Subject: [PATCH] Render sample tag distribution series --- .../DatasetDistributionPanel.svelte | 43 +++++++++++++++++-- .../ExpandDialog/ExpandDialog.svelte | 20 ++++++++- .../PanelHeader/PanelHeader.svelte | 6 +++ 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/lightly_studio_view/src/lib/components/DatasetDistributionPanel/DatasetDistributionPanel.svelte b/lightly_studio_view/src/lib/components/DatasetDistributionPanel/DatasetDistributionPanel.svelte index d4532218d..65ea3639c 100644 --- a/lightly_studio_view/src/lib/components/DatasetDistributionPanel/DatasetDistributionPanel.svelte +++ b/lightly_studio_view/src/lib/components/DatasetDistributionPanel/DatasetDistributionPanel.svelte @@ -3,7 +3,11 @@ import { Button } from '$lib/components'; import Typography from '$lib/components/Typography/Typography.svelte'; import { Select, type SelectItem } from '$lib/components/Select'; - import { BarChart, type CategoryCount } from '$lib/components/BarChart'; + import { + BarChart, + type CategoryCount, + type CategoryCountSeries + } from '$lib/components/BarChart'; import { Histogram, type HistogramRange } from '$lib/components/Histogram'; import { formatFloat, formatInteger } from '$lib/utils'; import DistributionConfigDialog from './DistributionConfigDialog/DistributionConfigDialog.svelte'; @@ -115,7 +119,30 @@ activeSource.groups?.find(groupHasContent) ?? activeSource.groups?.[0] ); - const activeData = $derived(activeGroup?.data ?? activeSource.data ?? []); + const activeSingleSeriesData = $derived( + activeGroup?.data ?? activeSource.data ?? [] + ); + const activeComparisonData = $derived( + activeGroup?.comparisonData ?? activeSource.comparisonData ?? [] + ); + const activeSeries = $derived( + activeComparisonData.map((tag) => ({ + id: tag.sample_tag_id, + label: tag.sample_tag_name, + data: tag.counts.map((item) => ({ label: item.label_name, count: item.count })) + })) + ); + // Rank the shared axis by the aggregate across tags; individual series stay independent. + const activeData = $derived.by(() => { + if (activeSeries.length === 0) return activeSingleSeriesData; + const totals = new Map(); + for (const series of activeSeries) { + for (const item of series.data) { + totals.set(item.label, (totals.get(item.label) ?? 0) + item.count); + } + } + return [...totals].map(([label, count]) => ({ label, count })); + }); // A group/source carrying bins renders as a histogram instead of a bar // chart; the categorical controls (sort, top-N, orientation) don't apply. const activeHistogram = $derived(activeGroup?.histogram ?? activeSource.histogram ?? null); @@ -164,6 +191,13 @@ ); const visible = $derived(selectVisibleCounts(activeData, config)); + const visibleLabels = $derived(new Set(visible.map((item) => item.label))); + const visibleSeries = $derived( + activeSeries.map((series) => ({ + ...series, + data: series.data.filter((item) => visibleLabels.has(item.label)) + })) + ); const totalCount = $derived(activeData.reduce((sum, item) => sum + item.count, 0)); function applyConfig(next: DistributionConfig) { @@ -284,7 +318,8 @@ {config} classCount={activeData.length} visibleClassCount={visible.length} - totalCount={showTotalCount ? totalCount : undefined} + totalCount={showTotalCount && activeSeries.length === 0 ? totalCount : undefined} + seriesCount={activeSeries.length || undefined} {valueNoun} onConfigure={() => (configDialogOpen = true)} onShowAll={() => (config = { ...config, mode: 'topN', n: activeData.length })} @@ -316,6 +351,7 @@ maxHeightPx={chartHeight || undefined} maxWidthPx={clientWidth || undefined} {totalCount} + series={visibleSeries} {onBarClick} /> {/if} @@ -330,6 +366,7 @@ import * as Dialog from '$lib/components/ui/dialog'; - import { BarChart, type CategoryCount } from '$lib/components/BarChart'; + import { + BarChart, + type CategoryCount, + type CategoryCountSeries + } from '$lib/components/BarChart'; import DistributionConfigDialog from '../DistributionConfigDialog/DistributionConfigDialog.svelte'; import PanelHeader from '../PanelHeader/PanelHeader.svelte'; import { selectVisibleCounts } from '../selectVisibleCounts'; @@ -11,6 +15,8 @@ open: boolean; /** Full class counts; the dialog applies `config` itself. */ data: CategoryCount[]; + /** Optional comparison series rendered on the shared class axis. */ + series?: CategoryCountSeries[]; /** The applied view config, shared with the panel. */ config: DistributionConfig; /** Noun for the header summary (e.g. 'annotations', 'samples'). */ @@ -23,6 +29,7 @@ let { open = $bindable(), data, + series = [], config, valueNoun = 'annotations', onConfigChange, @@ -36,6 +43,13 @@ let clientWidth = $state(0); const visible = $derived(selectVisibleCounts(data, config)); + const visibleLabels = $derived(new Set(visible.map((item) => item.label))); + const visibleSeries = $derived( + series.map((item) => ({ + ...item, + data: item.data.filter((count) => visibleLabels.has(count.label)) + })) + ); const totalCount = $derived(data.reduce((sum, item) => sum + item.count, 0)); @@ -49,7 +63,8 @@ {config} classCount={data.length} visibleClassCount={visible.length} - {totalCount} + totalCount={series.length === 0 ? totalCount : undefined} + seriesCount={series.length || undefined} {valueNoun} onConfigure={() => (configDialogOpen = true)} onShowAll={() => onConfigChange({ ...config, mode: 'topN', n: data.length })} @@ -70,6 +85,7 @@ maxHeightPx={chartHeight || undefined} maxWidthPx={clientWidth || undefined} {totalCount} + series={visibleSeries} {onBarClick} /> diff --git a/lightly_studio_view/src/lib/components/DatasetDistributionPanel/PanelHeader/PanelHeader.svelte b/lightly_studio_view/src/lib/components/DatasetDistributionPanel/PanelHeader/PanelHeader.svelte index 67b6f9f3b..368d44694 100644 --- a/lightly_studio_view/src/lib/components/DatasetDistributionPanel/PanelHeader/PanelHeader.svelte +++ b/lightly_studio_view/src/lib/components/DatasetDistributionPanel/PanelHeader/PanelHeader.svelte @@ -17,6 +17,8 @@ visibleClassCount: number; /** Sum of counts across all classes, for the summary line. Omit to hide the count. */ totalCount?: number; + /** Number of compared sample-tag series; shown instead of a combined count. */ + seriesCount?: number; /** Noun for the total count summary (e.g. 'annotations', 'samples'). */ valueNoun?: string; /** Opens the view-config dialog (top-N and sort order). */ @@ -36,6 +38,7 @@ classCount, visibleClassCount, totalCount, + seriesCount, valueNoun = 'annotations', onConfigure, onShowAll, @@ -59,6 +62,9 @@ · {totalCount.toLocaleString('en-US')} {valueNoun} {/if} + {#if seriesCount !== undefined} + · {seriesCount} sample {seriesCount === 1 ? 'tag' : 'tags'} + {/if} {#if onShowAll && visibleClassCount < classCount} ·