From e88af1a52865b78453ea94641d635f22ecd3791d Mon Sep 17 00:00:00 2001 From: Luke Schreiber <131496683+Luke-Schreiber@users.noreply.github.com> Date: Sun, 21 Dec 2025 12:04:49 -0700 Subject: [PATCH 1/3] Condition selector picks smallest default number of plots --- .../componentStores/conditionSelectorStore.ts | 9 ++-- apps/client/src/util/axisOptimizer.ts | 46 +++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 apps/client/src/util/axisOptimizer.ts diff --git a/apps/client/src/stores/componentStores/conditionSelectorStore.ts b/apps/client/src/stores/componentStores/conditionSelectorStore.ts index 2a097d19..6e089745 100644 --- a/apps/client/src/stores/componentStores/conditionSelectorStore.ts +++ b/apps/client/src/stores/componentStores/conditionSelectorStore.ts @@ -8,6 +8,7 @@ import { import { keysToString, stringToKeys } from '@/util/conChartStringFunctions'; import { useNotificationStore } from '../misc/notificationStore'; import { isEmpty } from 'lodash-es'; +import { findOptimalAxes } from '@/util/axisOptimizer'; export type Axis = 'x-axis' | 'y-axis'; @@ -110,10 +111,10 @@ export const useConditionSelectorStore = defineStore( watch( currentExperimentTags, (newExperimentTags) => { - const tagKeys = Object.keys(newExperimentTags); - if (tagKeys.length > 1) { - selectedXTag.value = tagKeys[0]; - selectedYTag.value = tagKeys[1]; + const optimalAxes = findOptimalAxes(newExperimentTags); + if (optimalAxes) { + selectedXTag.value = optimalAxes[0]; + selectedYTag.value = optimalAxes[1]; } }, { immediate: true } diff --git a/apps/client/src/util/axisOptimizer.ts b/apps/client/src/util/axisOptimizer.ts new file mode 100644 index 00000000..d18e7d91 --- /dev/null +++ b/apps/client/src/util/axisOptimizer.ts @@ -0,0 +1,46 @@ +/** + * Finds the optimal X and Y axes keys from a set of tags to minimize the total number of charts. + * The total number of charts is the product of the number of unique values in the X and Y axes. + * We prioritize pairs where the product is at least 2. + * If no pair has a product >= 2, we default to the first two available keys. + * + * @param tags A record where keys are tag names and values are arrays of tag options. + * @returns A tuple [xKey, yKey] representing the selected axes. Returns null if fewer than 2 keys exist. + */ +export function findOptimalAxes(tags: Record): [string, string] | null { + const keys = Object.keys(tags); + if (keys.length < 2) { + return null; + } + + let bestPair: [string, string] | null = null; + let minProduct = Infinity; + + // Iterate through all unique pairs. + for (let i = 0; i < keys.length; i++) { + for (let j = i + 1; j < keys.length; j++) { + const key1 = keys[i]; + const key2 = keys[j]; + + const count1 = tags[key1].length; + const count2 = tags[key2].length; + + const product = count1 * count2; + + if (product >= 2) { + if (product < minProduct) { + minProduct = product; + bestPair = [key1, key2]; + } + } + } + } + + // If we found a pair with product >= 2, return it. + if (bestPair) { + return bestPair; + } + + // Fallback + return [keys[0], keys[1]]; +} From 3002faa2f1352c242ecc15eaf90f9a5c752d8a4b Mon Sep 17 00:00:00 2001 From: Luke Schreiber <131496683+Luke-Schreiber@users.noreply.github.com> Date: Sun, 21 Dec 2025 12:39:24 -0700 Subject: [PATCH 2/3] Only 15 exemplar tracks visible at a time --- .../components/exemplarView/ExemplarView.vue | 28 +++++++++++++++---- .../componentStores/ExemplarViewStore.ts | 24 ++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/apps/client/src/components/exemplarView/ExemplarView.vue b/apps/client/src/components/exemplarView/ExemplarView.vue index 3f8154f8..a419c5b7 100644 --- a/apps/client/src/components/exemplarView/ExemplarView.vue +++ b/apps/client/src/components/exemplarView/ExemplarView.vue @@ -145,7 +145,10 @@ const { selectedVar1, horizonChartSettings, histogramYAxisLabel, + visibleExemplarTracks, + visibleConditionGroupsCount, } = storeToRefs(exemplarViewStore); +const { loadMoreConditionGroups } = exemplarViewStore; const imageViewerStore = useImageViewerStore(); const selectedAttributeName = computed(() => exemplarViewStore.getAttributeName()); @@ -297,6 +300,15 @@ function handleScroll(delta: number) { scrollExtent.value.max ); + // Check if we are near the bottom of the scroll extent + const buffer = 500; // pixels + if (viewStateMirror.value.target[1] >= scrollExtent.value.max - buffer) { + // Trigger load more + loadMoreConditionGroups(); + // Since visible tracks changed, we need to recalculate Y offsets + // This will happen in renderDeckGL + } + const newViewState = cloneDeep(viewStateMirror.value); deckgl.value.setProps({ initialViewState: newViewState, @@ -448,6 +460,12 @@ watch( { immediate: false } // We don't need to run this immediately on mount ); +// Watch for changes in visible tracks (pagination) to re-render +watch(visibleExemplarTracks, () => { + safeRenderDeckGL(); +}); + + // Main rendering function for DeckGL ------------------------------------------------------------------- let deckGLLayers: any[] = []; @@ -528,7 +546,7 @@ const exemplarRenderInfo = ref(new Map()); // Finds the exemplar tracks that are currently on screen. const exemplarTracksOnScreen = computed(() => { - return exemplarTracks.value.filter((exemplar: ExemplarTrack) => { + return visibleExemplarTracks.value.filter((exemplar: ExemplarTrack) => { const renderInfo = exemplarRenderInfo.value.get( uniqueExemplarKey(exemplar) ); @@ -574,9 +592,9 @@ const bottomYOffset = computed(() => { function recalculateExemplarYOffsets(): void { exemplarRenderInfo.value.clear(); let yOffset = 0; - let lastExemplar = exemplarTracks.value[0]; - for (let i = 0; i < exemplarTracks.value.length; i++) { - const exemplar = exemplarTracks.value[i]; + let lastExemplar = visibleExemplarTracks.value[0]; + for (let i = 0; i < visibleExemplarTracks.value.length; i++) { + const exemplar = visibleExemplarTracks.value[i]; yOffset += exemplarHeight.value; if (i !== 0) { if (isEqual(exemplar.tags, lastExemplar.tags)) { @@ -678,7 +696,7 @@ watch( // Populate cellSegmentationData with all segmentation data for all cells in exemplar tracks ONCE. async function getCellSegmentationData() { // For every cell from every exemplar track, get its segmentation and location and push that to the cellSegmentationData - for (const exemplar of exemplarTracks.value) { + for (const exemplar of visibleExemplarTracks.value) { if (!exemplar.data || exemplar.data.length === 0) continue; // For every exemplar track, iterate through its cells to get segmentation and location for (const cell of exemplar.data) { diff --git a/apps/client/src/stores/componentStores/ExemplarViewStore.ts b/apps/client/src/stores/componentStores/ExemplarViewStore.ts index 4cfe50e4..fc0c8b3c 100644 --- a/apps/client/src/stores/componentStores/ExemplarViewStore.ts +++ b/apps/client/src/stores/componentStores/ExemplarViewStore.ts @@ -342,6 +342,8 @@ export const useExemplarViewStore = defineStore('ExemplarViewStore', () => { } finally { // After fetching, we've finished loading. exemplarDataLoaded.value = true; + // Reset pagination + visibleConditionGroupsCount.value = 15; } } @@ -934,6 +936,25 @@ export const useExemplarViewStore = defineStore('ExemplarViewStore', () => { } } + // Pagination logic + const visibleConditionGroupsCount = ref(15); + const LOAD_INCREMENT = 5; + + function loadMoreConditionGroups() { + const sortedGroups = sortExemplarsByCondition(exemplarTracks.value); + if (visibleConditionGroupsCount.value < sortedGroups.length) { + visibleConditionGroupsCount.value += LOAD_INCREMENT; + } + } + + // This is the subset of tracks that should actually be rendered + const visibleExemplarTracks = computed(() => { + const sortedGroups = sortExemplarsByCondition(exemplarTracks.value); + const visibleGroups = sortedGroups.slice(0, visibleConditionGroupsCount.value); + // Flatten array of arrays + return visibleGroups.flatMap(group => group); + }); + // const getHistogramDataComputed = computed(() => histogramData.value); const conditionHistogramsComputed = computed( () => conditionHistograms.value @@ -944,6 +965,8 @@ export const useExemplarViewStore = defineStore('ExemplarViewStore', () => { getExemplarTracks, getExemplarImageUrls, exemplarTracks, + visibleExemplarTracks, // Export the visible tracks + loadMoreConditionGroups, // Export the load more action viewConfiguration, snippetZoom, exemplarHeight, @@ -965,5 +988,6 @@ export const useExemplarViewStore = defineStore('ExemplarViewStore', () => { histogramDomains: histogramDomainsComputed, exemplarDataLoaded, // export the loading state horizonChartScheme, + visibleConditionGroupsCount, // for debugging if needed }; }); From e2bab6a341d5f599c2e27edde04c36c903a09611 Mon Sep 17 00:00:00 2001 From: Luke Schreiber <131496683+Luke-Schreiber@users.noreply.github.com> Date: Sun, 21 Dec 2025 13:05:10 -0700 Subject: [PATCH 3/3] Cache sorted groups --- .../src/stores/componentStores/ExemplarViewStore.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/client/src/stores/componentStores/ExemplarViewStore.ts b/apps/client/src/stores/componentStores/ExemplarViewStore.ts index fc0c8b3c..450f14e2 100644 --- a/apps/client/src/stores/componentStores/ExemplarViewStore.ts +++ b/apps/client/src/stores/componentStores/ExemplarViewStore.ts @@ -940,17 +940,20 @@ export const useExemplarViewStore = defineStore('ExemplarViewStore', () => { const visibleConditionGroupsCount = ref(15); const LOAD_INCREMENT = 5; + // Cache the sorted groups to avoid re-sorting on every render/scroll + const sortedExemplarGroups = computed(() => { + return sortExemplarsByCondition(exemplarTracks.value); + }); + function loadMoreConditionGroups() { - const sortedGroups = sortExemplarsByCondition(exemplarTracks.value); - if (visibleConditionGroupsCount.value < sortedGroups.length) { + if (visibleConditionGroupsCount.value < sortedExemplarGroups.value.length) { visibleConditionGroupsCount.value += LOAD_INCREMENT; } } // This is the subset of tracks that should actually be rendered const visibleExemplarTracks = computed(() => { - const sortedGroups = sortExemplarsByCondition(exemplarTracks.value); - const visibleGroups = sortedGroups.slice(0, visibleConditionGroupsCount.value); + const visibleGroups = sortedExemplarGroups.value.slice(0, visibleConditionGroupsCount.value); // Flatten array of arrays return visibleGroups.flatMap(group => group); });