Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions apps/client/src/components/exemplarView/ExemplarView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ const {
selectedVar1,
horizonChartSettings,
histogramYAxisLabel,
visibleExemplarTracks,
visibleConditionGroupsCount,
} = storeToRefs(exemplarViewStore);
const { loadMoreConditionGroups } = exemplarViewStore;
const imageViewerStore = useImageViewerStore();
const selectedAttributeName = computed(() => exemplarViewStore.getAttributeName());

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -528,7 +546,7 @@ const exemplarRenderInfo = ref(new Map<string, ExemplarRenderInfo>());

// 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)
);
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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) {
Expand Down
27 changes: 27 additions & 0 deletions apps/client/src/stores/componentStores/ExemplarViewStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ export const useExemplarViewStore = defineStore('ExemplarViewStore', () => {
} finally {
// After fetching, we've finished loading.
exemplarDataLoaded.value = true;
// Reset pagination
visibleConditionGroupsCount.value = 15;
}
}

Expand Down Expand Up @@ -934,6 +936,28 @@ export const useExemplarViewStore = defineStore('ExemplarViewStore', () => {
}
}

// Pagination logic
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() {
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 visibleGroups = sortedExemplarGroups.value.slice(0, visibleConditionGroupsCount.value);
// Flatten array of arrays
return visibleGroups.flatMap(group => group);
});

// const getHistogramDataComputed = computed(() => histogramData.value);
const conditionHistogramsComputed = computed(
() => conditionHistograms.value
Expand All @@ -944,6 +968,8 @@ export const useExemplarViewStore = defineStore('ExemplarViewStore', () => {
getExemplarTracks,
getExemplarImageUrls,
exemplarTracks,
visibleExemplarTracks, // Export the visible tracks
loadMoreConditionGroups, // Export the load more action
viewConfiguration,
snippetZoom,
exemplarHeight,
Expand All @@ -965,5 +991,6 @@ export const useExemplarViewStore = defineStore('ExemplarViewStore', () => {
histogramDomains: histogramDomainsComputed,
exemplarDataLoaded, // export the loading state
horizonChartScheme,
visibleConditionGroupsCount, // for debugging if needed
};
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 }
Expand Down
46 changes: 46 additions & 0 deletions apps/client/src/util/axisOptimizer.ts
Original file line number Diff line number Diff line change
@@ -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[]>): [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]];
}