diff --git a/js/celldega.js b/js/celldega.js index 99c19e1e3..7b86e2521 100644 --- a/js/celldega.js +++ b/js/celldega.js @@ -32,6 +32,7 @@ const render_landscape_ist = async ({ model, el }) => { const nbhd = model.get('nbhd_geojson'); const max_tiles_to_view = model.get('max_tiles_to_view'); const nbhd_edit = model.get('nbhd_edit'); + const cell_centroid_size = model.get('cell_centroid_size') ?? 5; const scale_bar_microns_per_pixel = model.get('scale_bar_microns_per_pixel'); let meta_cell_data = { result: {}, attr: [] }; @@ -90,6 +91,7 @@ const render_landscape_ist = async ({ model, el }) => { rotation_x, rotate, max_tiles_to_view, + cell_centroid_size, scale_bar_microns_per_pixel, base_urls, cell_name_prefix diff --git a/js/deck-gl/core/deck_ist.js b/js/deck-gl/core/deck_ist.js index ad30f31ee..084a6bb04 100644 --- a/js/deck-gl/core/deck_ist.js +++ b/js/deck-gl/core/deck_ist.js @@ -23,6 +23,9 @@ export const ini_deck = (root, width, height, technology = '') => { getCursor, width, height, + glOptions: { + preserveDrawingBuffer: true, + }, }); return deck_ist; diff --git a/js/deck-gl/layers/cell_layer.js b/js/deck-gl/layers/cell_layer.js index f13ff294a..8696260f3 100644 --- a/js/deck-gl/layers/cell_layer.js +++ b/js/deck-gl/layers/cell_layer.js @@ -391,10 +391,11 @@ export const ini_cell_layer = async (base_url, viz_state) => { ...getModelMatrixProps(viz_state.rotation), }); } else { + const initial_cell_radius = viz_state.genes?.cell_ini_radius ?? 5; cell_layer = new ScatterplotLayer({ id: 'cell-layer', - radiusMinPixels: 1, - getRadius: 5.0, + radiusMinPixels: Math.max(1, initial_cell_radius), + getRadius: initial_cell_radius, pickable: true, getFillColor: (i, d) => get_cell_color(viz_state.cats, viz_state.highlighted_cells, i, d), @@ -435,6 +436,9 @@ export const update_cell_layer_radius = (layers_obj, radius, viz_state) => { }); } else { layers_obj.cell_layer = layers_obj.cell_layer.clone({ + // Keep marker size adjustable when zoomed out by tying the + // minimum on-screen pixel size to the slider radius. + radiusMinPixels: Math.max(1, radius), getRadius: radius, }); } diff --git a/js/deck-gl/matrix/deck_mat.js b/js/deck-gl/matrix/deck_mat.js index 596f39bdd..a3ef4d37e 100644 --- a/js/deck-gl/matrix/deck_mat.js +++ b/js/deck-gl/matrix/deck_mat.js @@ -20,6 +20,9 @@ export const ini_deck = (root, width, height) => { getCursor, width, height, + glOptions: { + preserveDrawingBuffer: true, + }, }); return deck_ist; diff --git a/js/deck-gl/matrix/views.js b/js/deck-gl/matrix/views.js index cafdc3563..54b8b34f8 100644 --- a/js/deck-gl/matrix/views.js +++ b/js/deck-gl/matrix/views.js @@ -78,7 +78,7 @@ export const ini_views = (viz_state) => { id: 'col_attr_labels', x: `${viz_state.viz.row_region + viz_state.viz.label_buffer + viz_state.viz.mat_width}px`, y: '0px', - width: `${viz_state.viz.dendrogram_width + 60}px`, + width: `${viz_state.viz.dendrogram_width + viz_state.viz.col_attr_label_width}px`, height: `${viz_state.viz.col_region}px`, controller: false, }), diff --git a/js/matrix/set_constants.js b/js/matrix/set_constants.js index 6b2d8ae35..49232d48e 100644 --- a/js/matrix/set_constants.js +++ b/js/matrix/set_constants.js @@ -50,6 +50,20 @@ const parseEntitySpec = (value) => { return { entity: 'custom', attr: 'name' }; }; +const estimate_row_label_width = (row_nodes = []) => { + const min_width = 75; + const max_width = 260; + const avg_char_width_px = 7; + + const max_label_length = row_nodes.reduce((max_len, node) => { + const name = String(node?.name ?? ''); + return Math.max(max_len, name.length); + }, 0); + + const estimated_width = Math.ceil(max_label_length * avg_char_width_px); + return Math.max(min_width, Math.min(max_width, estimated_width)); +}; + export const set_mat_constants = ( model, network, @@ -141,7 +155,7 @@ export const set_mat_constants = ( viz_state.viz.base_font_size = 125; viz_state.viz.col_label = 75; // 40 - viz_state.viz.row_label = 75; // 35 + viz_state.viz.row_label = estimate_row_label_width(network.row_nodes); viz_state.viz.extra_space = {}; viz_state.viz.extra_space.row = 5; // 10; @@ -164,6 +178,7 @@ export const set_mat_constants = ( viz_state.viz.cat_shift_row = 30; viz_state.viz.label_buffer = 1; + viz_state.viz.col_attr_label_width = 100; viz_state.animate = {}; viz_state.animate.duration = 2500; diff --git a/js/ui/bar_plot.js b/js/ui/bar_plot.js index 5d48b0b00..dbc066694 100644 --- a/js/ui/bar_plot.js +++ b/js/ui/bar_plot.js @@ -127,6 +127,9 @@ export const bar_callback_nbhd = ( _layers_obj, _viz_state ) => { + const is_multi_select = + _event?.type === 'contextmenu' || _event?.button === 2; + if (_viz_state.nbhd.edit) { _viz_state.obs_store.viz_edit_layer.set(true); @@ -143,37 +146,46 @@ export const bar_callback_nbhd = ( toggle_slider(_viz_state.sliders.nbhd, true); const prev_selected_nbhds = _viz_state.obs_store.selected_nbhds.get(); - if ( - prev_selected_nbhds[0] === _d.name && - prev_selected_nbhds.length === 1 + let next_selected_nbhds; + if (is_multi_select) { + if (prev_selected_nbhds.includes(_d.name)) { + next_selected_nbhds = prev_selected_nbhds.filter((n) => n !== _d.name); + } else { + next_selected_nbhds = [...prev_selected_nbhds, _d.name]; + } + } else if ( + prev_selected_nbhds.length === 1 && + prev_selected_nbhds[0] === _d.name ) { - _viz_state.obs_store.selected_nbhds.set([]); - _layers_obj.edit_layer = _layers_obj.edit_layer.clone({ - selectedFeatureIndexes: [], - }); + next_selected_nbhds = []; } else { - _viz_state.obs_store.selected_nbhds.set([_d.name]); - // Use edit.feature_collection for edit mode - const features = _viz_state.edit?.feature_collection?.features || []; - const featureIndex = features.findIndex( - (f) => f.properties.name === _d.name || f.properties.cat === _d.name - ); - if (featureIndex >= 0) { - _layers_obj.edit_layer = _layers_obj.edit_layer.clone({ - selectedFeatureIndexes: [featureIndex], - }); - } + next_selected_nbhds = [_d.name]; } + _viz_state.obs_store.selected_nbhds.set(next_selected_nbhds); + + // Use edit.feature_collection for edit mode + const features = _viz_state.edit?.feature_collection?.features || []; + const selectedFeatureIndexes = features + .map((f, idx) => + next_selected_nbhds.includes(f.properties.name) || + next_selected_nbhds.includes(f.properties.cat) + ? idx + : -1 + ) + .filter((idx) => idx >= 0); + _layers_obj.edit_layer = _layers_obj.edit_layer.clone({ + selectedFeatureIndexes, + }); refresh_layer(_viz_state, _layers_obj, 'edit_layer'); if (_viz_state.obs_store.selected_nbhds.get().length > 0) { + const selected_nbhds = _viz_state.obs_store.selected_nbhds.get(); _viz_state.nbhd.svg_bar_nbhd.selectAll('rect').style('opacity', (d) => { - if (d.name === _d.name) { + if (selected_nbhds.includes(d.name)) { return 1.0; - } else { - return 0.2; } + return 0.2; }); } else { _viz_state.nbhd.svg_bar_nbhd.selectAll('rect').style('opacity', 1.0); @@ -194,7 +206,15 @@ export const bar_callback_nbhd = ( } const prev_selected_nbhds = _viz_state.obs_store.selected_nbhds.get(); - if ( + if (is_multi_select) { + if (prev_selected_nbhds.includes(_d.name)) { + _viz_state.obs_store.selected_nbhds.set( + prev_selected_nbhds.filter((n) => n !== _d.name) + ); + } else { + _viz_state.obs_store.selected_nbhds.set([...prev_selected_nbhds, _d.name]); + } + } else if ( prev_selected_nbhds[0] === _d.name && prev_selected_nbhds.length === 1 ) { @@ -206,12 +226,12 @@ export const bar_callback_nbhd = ( refresh_layer(_viz_state, _layers_obj, 'nbhd_layer'); if (_viz_state.obs_store.selected_nbhds.get().length > 0) { + const selected_nbhds = _viz_state.obs_store.selected_nbhds.get(); _viz_state.nbhd.svg_bar_nbhd.selectAll('rect').style('opacity', (d) => { - if (d.name === _d.name) { + if (selected_nbhds.includes(d.name)) { return 1.0; - } else { - return 0.2; } + return 0.2; }); } else { _viz_state.nbhd.svg_bar_nbhd.selectAll('rect').style('opacity', 1.0); @@ -279,7 +299,14 @@ export const make_bar_graph = ( .attr('transform', (d, i) => `translate(2,${y_new(i) + 2})`) .on('click', (event, d) => click_callback(event, d, deck_ist, layers_obj, viz_state) - ); + ) + .on('contextmenu', (event, d) => { + // Right-click multi-select for neighborhood bar controls. + if (click_callback === bar_callback_nbhd) { + event.preventDefault(); + click_callback(event, d, deck_ist, layers_obj, viz_state); + } + }); bar .append('rect') diff --git a/js/ui/sliders.js b/js/ui/sliders.js index 59ecb9e43..ba676d50c 100644 --- a/js/ui/sliders.js +++ b/js/ui/sliders.js @@ -5,6 +5,9 @@ import { square_scatter_layer_opacity } from '../deck-gl/layers/square_scatter_l import { update_trx_layer_radius } from '../deck-gl/layers/trx_layer'; import { refresh_layer } from '../utils/refresh_layer'; +const CELL_RADIUS_SLIDER_SCALE = 5; +const TRX_RADIUS_SLIDER_SCALE = 100; + const clamp_to_byte = (value) => { return Math.max(0, Math.min(255, Math.round(value))); }; @@ -208,11 +211,9 @@ const tile_slider_callback = async (deck_sst, viz_state, layers_sst) => { }; const cell_slider_callback = async (deck_ist, layers_obj, viz_state) => { - const scale_down_cell_radius = 5; - update_cell_layer_radius( layers_obj, - viz_state.sliders.cell.value / scale_down_cell_radius, + viz_state.sliders.cell.value / CELL_RADIUS_SLIDER_SCALE, viz_state ); @@ -220,11 +221,9 @@ const cell_slider_callback = async (deck_ist, layers_obj, viz_state) => { }; const trx_slider_callback = async (deck_ist, layers_obj, viz_state) => { - const scale_down_trx_radius = 100; - update_trx_layer_radius( layers_obj, - viz_state.sliders.trx.value / scale_down_trx_radius + viz_state.sliders.trx.value / TRX_RADIUS_SLIDER_SCALE ); refresh_layer(viz_state, layers_obj, 'trx_layer'); @@ -302,7 +301,7 @@ export const ini_slider = (slider_type, inst_deck, layers_obj, viz_state) => { callback = () => tile_slider_callback(inst_deck, viz_state, layers_obj); break; case 'cell': - ini_value = viz_state.genes.trx_ini_raidus * 100; + ini_value = viz_state.genes.cell_ini_radius * CELL_RADIUS_SLIDER_SCALE; callback = () => cell_slider_callback(inst_deck, layers_obj, viz_state); break; case 'trx': diff --git a/js/ui/text_buttons.js b/js/ui/text_buttons.js index 2b2bf4d34..28d311138 100644 --- a/js/ui/text_buttons.js +++ b/js/ui/text_buttons.js @@ -251,14 +251,8 @@ const cell_button_callback = async (event, deck_ist, layers_obj, viz_state) => { toggle_path_layer_visibility(layers_obj, is_visible); if (is_visible) { - toggle_nbhd_layer_visibility(layers_obj, false); - viz_state.obs_store.viz_nbhd_layer.set(false); viz_state.obs_store.viz_edit_layer.set(false); - if (viz_state.nbhd.is_nbhd) { - viz_state.buttons?.buttons?.nbhd?.style?.('color', 'gray'); - } - viz_state.cats.svg_bar_cluster.selectAll('rect').style('opacity', 1.0); } else { viz_state.cats.svg_bar_cluster.selectAll('rect').style('opacity', 0.2); @@ -286,6 +280,24 @@ const nbhd_button_callback = async (event, deck_ist, layers_obj, viz_state) => { toggle_slider(viz_state.sliders.nbhd, is_visible); + if (is_visible) { + const selected_nbhds = viz_state.obs_store.selected_nbhds.get(); + const available_nbhds = new Set( + (viz_state.nbhd?.feature_collection?.features || []).map( + (feature) => feature.properties.cat + ) + ); + const has_matching_selection = selected_nbhds.some((nbhd) => + available_nbhds.has(nbhd) + ); + + // If a stale neighborhood selection is active, clear it so polygons render. + if (selected_nbhds.length > 0 && !has_matching_selection) { + viz_state.obs_store.selected_nbhds.set([]); + viz_state.nbhd?.svg_bar_nbhd?.selectAll?.('rect')?.style?.('opacity', 1.0); + } + } + toggle_nbhd_layer_visibility(layers_obj, is_visible); viz_state.obs_store.viz_nbhd_layer.set(is_visible); diff --git a/js/ui/ui_containers.js b/js/ui/ui_containers.js index f57382308..f5f267336 100644 --- a/js/ui/ui_containers.js +++ b/js/ui/ui_containers.js @@ -14,6 +14,7 @@ import { update_path_pickable_state } from '../deck-gl/layers/path_layer'; import { update_trx_pickable_state } from '../deck-gl/layers/trx_layer'; import { update_dendro_layer_data } from '../deck-gl/matrix/dendro_layers'; import { get_mat_layers_list } from '../deck-gl/matrix/matrix_layers'; +import { ini_views } from '../deck-gl/matrix/views'; import { get_layers_list } from '../deck-gl/utils/layers_ist'; import { uniprot_data, @@ -26,6 +27,7 @@ import { } from '../matrix/dendro'; import { debounce } from '../utils/debounce'; import { refresh_layer } from '../utils/refresh_layer'; +import { capture_deck_screenshot } from '../utils/screenshot'; import { make_bar_graph, @@ -76,6 +78,85 @@ export const make_ctrl_container = () => { return ctrl_container; }; +const append_screenshot_button = (container, deck, prefix) => { + const button = document.createElement('button'); + button.textContent = '📷 PNG'; + button.title = 'Download high-resolution screenshot'; + button.style.marginTop = '6px'; + button.style.marginLeft = '8px'; + button.style.height = '22px'; + button.style.fontSize = '11px'; + button.style.cursor = 'pointer'; + button.style.borderRadius = '6px'; + button.style.border = '1px solid #d3d3d3'; + button.style.backgroundColor = '#f8f9fa'; + button.style.color = '#47515b'; + button.style.padding = '0 8px'; + button.addEventListener('click', () => + capture_deck_screenshot(deck, prefix, 3) + ); + container.appendChild(button); +}; + +const append_size_controls = (container, deck, viz_state) => { + if (!viz_state?.root) { + return; + } + + const controls = document.createElement('div'); + controls.style.display = 'flex'; + controls.style.flexDirection = 'column'; + controls.style.marginLeft = '8px'; + controls.style.marginTop = '4px'; + controls.style.gap = '4px'; + + const width_slider = document.createElement('input'); + const height_slider = document.createElement('input'); + + const current_width = Math.round(viz_state.root.clientWidth || 900); + const current_height = Math.round(viz_state.root.clientHeight || 700); + + const setup_slider = (slider, initial_value) => { + ini_slider_params(slider, initial_value, () => {}); + slider.min = '300'; + slider.max = '2000'; + slider.value = String(initial_value); + slider.style.width = '85px'; + }; + + setup_slider(width_slider, current_width); + setup_slider(height_slider, current_height); + + const apply_size = () => { + const new_width = Number(width_slider.value) || current_width; + const new_height = Number(height_slider.value) || current_height; + viz_state.root.style.width = `${new_width}px`; + viz_state.root.style.height = `${new_height}px`; + deck.setProps({ + width: new_width, + height: new_height, + }); + }; + + width_slider.addEventListener('input', apply_size); + height_slider.addEventListener('input', apply_size); + + const w_label = document.createElement('div'); + w_label.textContent = 'W'; + w_label.style.fontSize = '10px'; + w_label.style.color = '#47515b'; + const h_label = document.createElement('div'); + h_label.textContent = 'H'; + h_label.style.fontSize = '10px'; + h_label.style.color = '#47515b'; + + controls.appendChild(w_label); + controls.appendChild(width_slider); + controls.appendChild(h_label); + controls.appendChild(height_slider); + container.appendChild(controls); +}; + export const flex_container = (class_name, flex_direction, height = null) => { const container = document.createElement('div'); container.className = class_name; @@ -230,6 +311,37 @@ export const make_matrix_ui_container = (deck_mat, layers_mat, viz_state) => { viz_state.dendro.sliders.col.style.marginTop = '3px'; viz_state.dendro.sliders.row.style.marginTop = '10px'; + const row_label_width_slider = document.createElement('input'); + const min_row_label_width = 60; + const max_row_label_width = 320; + const initial_row_label_width = Math.max( + min_row_label_width, + Math.min(max_row_label_width, viz_state.viz.row_label || 75) + ); + + ini_slider_params( + row_label_width_slider, + ((initial_row_label_width - min_row_label_width) * 100) / + (max_row_label_width - min_row_label_width), + (event) => { + const slider_value = Number(event.target.value) || 0; + const width_value = + min_row_label_width + + ((max_row_label_width - min_row_label_width) * slider_value) / 100; + + viz_state.viz.row_label = Math.round(width_value); + viz_state.viz.row_region = + (viz_state.viz.row_cat_width + viz_state.viz.extra_space.row) * + viz_state.attr.num.row + + viz_state.viz.row_label; + ini_views(viz_state); + deck_mat.setProps({ + views: viz_state.views.views_list, + }); + } + ); + row_label_width_slider.style.marginTop = '6px'; + d3.select(slider_container) .append('div') .text('Dendro') @@ -253,8 +365,28 @@ export const make_matrix_ui_container = (deck_mat, layers_mat, viz_state) => { '-apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue", Helvetica, Arial, sans-serif' ); + d3.select(slider_container) + .append('div') + .text('Row W') + .style('width', '40px') + .style('height', '14px') + .style('display', 'inline-flex') + .style('align-items', 'center') + .style('justify-content', 'center') + .style('text-align', 'center') + .style('font-size', '9px') + .style('font-weight', 'bold') + .style('color', '#47515b') + .style('border-color', 'white') + .style('margin-left', '10px') + .style( + 'font-family', + '-apple-system, BlinkMacSystemFont, "San Francisco", "Helvetica Neue", Helvetica, Arial, sans-serif' + ); + slider_container.appendChild(viz_state.dendro.sliders.col); slider_container.appendChild(viz_state.dendro.sliders.row); + slider_container.appendChild(row_label_width_slider); // add top margin to ctrl_container and slider_container ctrl_container.style.marginTop = '10px'; @@ -348,6 +480,8 @@ export const make_sst_ui_container = (deck_sst, layers_sst, viz_state) => { ctrl_container.appendChild(image_container); ctrl_container.appendChild(tile_container); ctrl_container.appendChild(viz_state.genes.gene_search); + append_size_controls(ctrl_container, deck_sst, viz_state); + append_screenshot_button(ctrl_container, deck_sst, 'landscape'); return ui_container; }; @@ -922,6 +1056,8 @@ export const make_ist_ui_container = ( } ctrl_container.appendChild(cell_container); ctrl_container.appendChild(gene_container); + append_size_controls(ctrl_container, deck_ist, viz_state); + append_screenshot_button(ctrl_container, deck_ist, 'landscape'); viz_state.genes.gene_search.style.width = '160px'; viz_state.genes.gene_search.style.marginLeft = '5px'; diff --git a/js/utils/screenshot.js b/js/utils/screenshot.js new file mode 100644 index 000000000..ac4bfa48c --- /dev/null +++ b/js/utils/screenshot.js @@ -0,0 +1,44 @@ +export const capture_deck_screenshot = ( + deck, + file_prefix = 'celldega', + scale = 3 +) => { + const canvas = deck?.canvas; + if (!canvas) { + return; + } + + const safe_scale = Math.max(1, Number(scale) || 1); + const export_canvas = document.createElement('canvas'); + export_canvas.width = Math.max(1, Math.floor(canvas.width * safe_scale)); + export_canvas.height = Math.max(1, Math.floor(canvas.height * safe_scale)); + + const ctx = export_canvas.getContext('2d'); + if (!ctx) { + return; + } + + deck.redraw(true); + requestAnimationFrame(() => { + requestAnimationFrame(() => { + ctx.setTransform(safe_scale, 0, 0, safe_scale, 0, 0); + ctx.drawImage(canvas, 0, 0); + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `${file_prefix}-${timestamp}.png`; + + export_canvas.toBlob((blob) => { + if (!blob) { + return; + } + + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + }, 'image/png'); + }); + }); +}; diff --git a/js/viz/landscape_ist.js b/js/viz/landscape_ist.js index 13cd34875..8ee9a1ea0 100644 --- a/js/viz/landscape_ist.js +++ b/js/viz/landscape_ist.js @@ -14,7 +14,6 @@ import { set_views } from '../deck-gl/core/views'; import { ini_background_layer } from '../deck-gl/layers/background_layer'; import { ini_cell_layer, - new_toggle_cell_layer_visibility, set_cell_layer_onclick, toggle_spatial_umap, update_cell_pickable_state, @@ -207,6 +206,7 @@ export const landscape_ist = async ( rotation_x = 0, rotate = 0, max_tiles_to_view = 50, + cell_centroid_size = 5, scale_bar_microns_per_pixel = null, base_urls = [], cell_name_prefix = false @@ -402,6 +402,7 @@ export const landscape_ist = async ( viz_state.genes.gene_counts = []; viz_state.genes.selected_genes = []; viz_state.genes.trx_ini_radius = trx_radius; + viz_state.genes.cell_ini_radius = cell_centroid_size; viz_state.genes.trx_names_array = []; viz_state.genes.trx_data = []; viz_state.genes.gene_text_box = ''; @@ -451,6 +452,10 @@ export const landscape_ist = async ( const root = document.createElement('div'); root.style.position = 'relative'; root.style.height = `${height}px`; + root.style.resize = 'both'; + root.style.overflow = 'auto'; + root.style.minWidth = '500px'; + root.style.minHeight = '400px'; root.style.border = '1px solid #d3d3d3'; const userMicronsPerPixel = @@ -657,35 +662,25 @@ export const landscape_ist = async ( viz_state.obs_store.viz_nbhd_layer.subscribe( (visible) => { if (visible) { - // set cell layer to not visible - new_toggle_cell_layer_visibility(viz_state.layers_obj, false); - + // keep CELL layer independent from NBHD visibility // set gene/cat bars to disabled color viz_state.genes.svg_bar_gene.selectAll('rect').style('opacity', 0.2); - viz_state.cats.svg_bar_cluster.selectAll('rect').style('opacity', 0.2); viz_state.nbhd.svg_bar_nbhd.selectAll('rect').style('opacity', 1.0); - viz_state.buttons.buttons.cell.style('color', 'gray'); viz_state.buttons.buttons.trx.style('color', 'gray'); viz_state.buttons.buttons.nbhd?.style('color', 'blue'); - toggle_slider(viz_state.sliders.cell, false); toggle_slider(viz_state.sliders.trx, false); if (viz_state.nbhd.is_nbhd) { toggle_slider(viz_state.sliders.nbhd, true); } } else { - new_toggle_cell_layer_visibility(viz_state.layers_obj, true); - viz_state.genes.svg_bar_gene.selectAll('rect').style('opacity', 1.0); - viz_state.cats.svg_bar_cluster.selectAll('rect').style('opacity', 1.0); viz_state.nbhd.svg_bar_nbhd.selectAll('rect').style('opacity', 0.2); - viz_state.buttons.buttons.cell.style('color', 'blue'); viz_state.buttons.buttons.trx.style('color', 'blue'); viz_state.buttons.buttons.nbhd?.style('color', 'gray'); - toggle_slider(viz_state.sliders.cell, true); toggle_slider(viz_state.sliders.trx, true); if (viz_state.nbhd.is_nbhd) { toggle_slider(viz_state.sliders.nbhd, false); diff --git a/src/celldega/viz/widget.py b/src/celldega/viz/widget.py index ca5a627f9..8126b6d8f 100644 --- a/src/celldega/viz/widget.py +++ b/src/celldega/viz/widget.py @@ -93,6 +93,7 @@ class Landscape(anywidget.AnyWidget): scale_bar_microns_per_pixel = traitlets.Float(default_value=None, allow_none=True).tag( sync=True ) + cell_centroid_size = traitlets.Float(5.0).tag(sync=True) nbhd = traitlets.Instance(gpd.GeoDataFrame, allow_none=True) nbhd_geojson = traitlets.Dict({}).tag(sync=True)