Skip to content
Merged
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
23 changes: 17 additions & 6 deletions js/deck-gl/layers/image_layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ import {

import { make_simple_image_layer } from './simple_image_layer';

const make_image_layer = (viz_state, info) => {
const make_image_layer = (viz_state, info, datasetIndex = 0, cacheKey = '') => {
const { max_pyramid_zoom } = viz_state.img.landscape_parameters;

const opacity = 5;

// Include dataset index and cache key in ID to force complete layer recreation
const layerId = `${info.button_name}-ds${datasetIndex}${cacheKey ? `-${cacheKey}` : ''}`;

const image_layer = new TileLayer({
id: info.button_name,
id: layerId,
tileSize: viz_state.dimensions.tileSize,
refinementStrategy: 'no-overlap',
minZoom: -7,
maxZoom: 0,
maxCacheSize: 20,
maxCacheSize: 0, // Disable internal tile caching
extent: [0, 0, viz_state.dimensions.width, viz_state.dimensions.height],
getTileData: create_get_tile_data(
viz_state.global_base_url,
Expand All @@ -40,19 +43,27 @@ const make_image_layer = (viz_state, info) => {
return image_layer;
};

export const make_image_layers = async (viz_state) => {
export const make_image_layers = async (viz_state, datasetIndex = 0) => {
const { image_info } = viz_state.img;

// Generate a unique cache key to force complete layer recreation
const cacheKey = Date.now().toString(36);

if (
image_info.length === 1 &&
(image_info[0].name === 'h_and_e' || image_info[0].name === 'h&e')
) {
const layer = await make_simple_image_layer(viz_state, image_info[0]);
const layer = await make_simple_image_layer(
viz_state,
image_info[0],
datasetIndex,
cacheKey
);
return [layer];
}

const image_layers = image_info.map((info) =>
make_image_layer(viz_state, info)
make_image_layer(viz_state, info, datasetIndex, cacheKey)
);
return image_layers;
};
Expand Down
14 changes: 11 additions & 3 deletions js/deck-gl/layers/simple_image_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,27 @@ import {
create_get_tile_data,
} from '../utils/tiles';

export const make_simple_image_layer = async (viz_state, info) => {
export const make_simple_image_layer = async (
viz_state,
info,
datasetIndex = 0,
cacheKey = ''
) => {
const { global_base_url } = viz_state;
const { dimensions } = viz_state;
const { landscape_parameters } = viz_state.img;
const { image_format } = viz_state.img.landscape_parameters;

// Include dataset index and cache key in ID to force complete layer recreation
const layerId = `global-simple-image-layer-ds${datasetIndex}${cacheKey ? `-${cacheKey}` : ''}`;

const simple_image_layer = new TileLayer({
id: 'global-simple-image-layer',
id: layerId,
tileSize: dimensions.tileSize,
refinementStrategy: 'no-overlap',
minZoom: -7,
maxZoom: 0,
maxCacheSize: 20,
maxCacheSize: 0, // Disable internal tile caching
extent: [0, 0, dimensions.width, dimensions.height],
getTileData: create_get_tile_data(
global_base_url,
Expand Down
12 changes: 12 additions & 0 deletions js/obs_store/obs_store.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ export const create_obs_store = () => {
landscape_view: Observable('spatial'),
umap_state: Observable(false),
scale_bar_view_state: Observable(null),
// Dataset switching observables
current_dataset_index: Observable(0),
dataset_switching: Observable(false),
// Persistent state across dataset switches
// This allows users to compare the same cluster/gene across datasets
persistent_state: Observable({
selected_cats: [],
selected_genes: [],
cat: 'cluster', // 'cluster' or gene name
viz_image_layers: true,
landscape_view: 'spatial', // 'spatial' or 'umap'
}),
// to do utilize for setProps
deck_check: Observable({
background_layer: true,
Expand Down
116 changes: 116 additions & 0 deletions js/ui/dataset_dropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { switch_dataset } from './switch_dataset';

/**
* Creates a small dataset dropdown selector for switching between datasets.
* @param {Object} viz_state - The visualization state object
* @param {Object} deck_ist - The deck.gl instance
* @param {Object} layers_obj - The layers object
* @returns {HTMLElement} The dropdown container element
*/
export const make_dataset_dropdown = (viz_state, deck_ist, layers_obj) => {
const base_urls = viz_state.base_urls || [];

// Don't create dropdown if there's only one or no datasets
if (base_urls.length <= 1) {
return null;
}

const container = document.createElement('div');
container.className = 'dataset-dropdown-container';
container.style.display = 'inline-flex';
container.style.alignItems = 'center';
container.style.marginLeft = '5px';
container.style.marginRight = '5px';
container.style.position = 'relative';

const select = document.createElement('select');
select.className = 'dataset-dropdown';
select.style.width = '55px';
select.style.height = '18px';
select.style.fontSize = '9px';
select.style.padding = '1px 2px';
select.style.border = '1px solid #d3d3d3';
select.style.borderRadius = '3px';
select.style.backgroundColor = 'white';
select.style.cursor = 'pointer';
select.style.outline = 'none';
select.style.fontFamily =
'-apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue", Helvetica, Arial, sans-serif';
select.style.transition = 'width 0.15s ease';
select.title = 'Switch dataset';

// Calculate max width needed for full labels
const max_label_length = Math.max(
...base_urls.map((d) => (d.label || '').length)
);
const expanded_width = Math.min(Math.max(max_label_length * 7 + 20, 80), 150);

// Add focus/hover styling - expand on focus/mousedown
select.addEventListener('mousedown', () => {
select.style.width = `${expanded_width}px`;
});
select.addEventListener('focus', () => {
select.style.borderColor = '#8797ff';
select.style.width = `${expanded_width}px`;
});
select.addEventListener('blur', () => {
select.style.borderColor = '#d3d3d3';
select.style.width = '55px';
});
select.addEventListener('change', () => {
// Collapse after selection
setTimeout(() => {
select.style.width = '55px';
}, 100);
});

// Add options for each dataset
base_urls.forEach((dataset, index) => {
const option = document.createElement('option');
option.value = index;
// Use short_label if available, otherwise use label, otherwise default
const label = dataset.short_label || dataset.label || `DS-${index + 1}`;
const full_label = dataset.label || `Dataset ${index + 1}`;
option.textContent = label;
option.title = full_label; // Full label on hover
select.appendChild(option);
});

// Set initial value from obs_store
select.value = viz_state.obs_store.current_dataset_index.get();

// Handle change event
select.addEventListener('change', async (event) => {
const new_index = parseInt(event.target.value, 10);
const current_index = viz_state.obs_store.current_dataset_index.get();

if (
new_index !== current_index &&
!viz_state.obs_store.dataset_switching.get()
) {
// Show loading state
select.disabled = true;
select.style.opacity = '0.5';

try {
await switch_dataset(new_index, viz_state, deck_ist, layers_obj);
} catch (error) {
// Revert selection on error
select.value = current_index;
void error;
} finally {
select.disabled = false;
select.style.opacity = '1';
}
}
});

// Subscribe to dataset index changes to keep dropdown in sync
viz_state.obs_store.current_dataset_index.subscribe((index) => {
select.value = index;
});

container.appendChild(select);

return container;
};
Loading
Loading