# dimension_selection.py
from dash import dcc, html, callback_context
from dash.dependencies import Input, Output, State, ALL, MATCH
import pyproj
import json
import numpy as np
[docs]
class DimensionSelection:
def __init__(self, app, ds_getter):
self.app = app
self.ds_getter = ds_getter
self.dim_selections = {}
[docs]
def setup_callbacks(self):
@self.app.callback(
Output('dimension-checklist-container', 'children'),
Input('variable-dropdown', 'value')
)
def update_dimension_checklist(selected_var):
ds = self.ds_getter()
if selected_var and ds is not None:
return self.generate_dimension_checklist(ds, selected_var)
return ""
@self.app.callback(
Output('dimension-dropdowns-container', 'children'),
Input('dimension-checklist', 'value'),
State('variable-dropdown', 'value')
)
def update_dimension_controls(selected_dims, selected_var):
ds = self.ds_getter()
if selected_var and selected_dims and ds is not None:
return self.generate_dimension_controls(ds, selected_dims, selected_var)
return ""
@self.app.callback(
Output('selected-dimensions-store', 'data'),
Input({'type': 'dimension-slider', 'index': ALL}, 'value'),
Input({'type': 'dimension-dropdown', 'index': ALL}, 'value'),
Input('dimension-checklist', 'value'),
State('variable-dropdown', 'value'),
State({'type': 'dimension-slider', 'index': ALL}, 'id'),
State({'type': 'dimension-dropdown', 'index': ALL}, 'id'),
prevent_initial_call=True
)
def store_selected_dimensions(slider_values, dropdown_values,
checked_dims, selected_var,
slider_ids, dropdown_ids):
ds = self.ds_getter()
if not checked_dims or ds is None:
return {}
selected_dims = {}
# Process slider values (range selections)
for i, slider_val in enumerate(slider_values):
if slider_val is not None and i < len(slider_ids):
dim = slider_ids[i]['index']
if dim in checked_dims:
# Convert slider indices to actual dimension values
dim_values = ds[selected_var][dim].values
if (isinstance(slider_val, list) and
len(slider_val) == 2):
# Range selection - return tuple of (start, end) values
start_idx, end_idx = slider_val
start_val = dim_values[start_idx]
end_val = dim_values[end_idx]
# Convert to proper data type if needed
if isinstance(start_val, np.datetime64):
start_val = start_val.item()
if isinstance(end_val, np.datetime64):
end_val = end_val.item()
selected_dims[dim] = (start_val, end_val)
elif isinstance(slider_val, (int, float)):
# Single value selection - return tuple with single value
idx = int(slider_val)
val = dim_values[idx]
# Convert to proper data type if needed
if isinstance(val, np.datetime64):
val = val.item()
selected_dims[dim] = (val,)
# Process dropdown values (single selections)
for i, dropdown_val in enumerate(dropdown_values):
if dropdown_val is not None and i < len(dropdown_ids):
dim = dropdown_ids[i]['index']
if (dim in checked_dims and
dim not in selected_dims): # Don't override slider selections
# Convert dropdown index to actual dimension value
dim_values = ds[selected_var][dim].values
idx = dropdown_val
val = dim_values[idx]
# Convert to proper data type if needed
if isinstance(val, np.datetime64):
val = val.item()
# Single selection - return tuple with single value
selected_dims[dim] = (val,)
return selected_dims
@self.app.callback(
Output({'type': 'dim-control-widget', 'index': MATCH}, 'children'),
Input({'type': 'dim-control-type', 'index': MATCH}, 'value'),
State('variable-dropdown', 'value'),
State('dimension-checklist', 'value'),
State({'type': 'dim-control-type', 'index': MATCH}, 'id'),
)
def render_dim_control(control_type, selected_var, checked_dims, id_dict):
ds = self.ds_getter()
dim = id_dict['index']
if not selected_var or not checked_dims or ds is None:
return None
if control_type == 'slider':
return self.create_range_slider(ds, dim, selected_var)
else:
return self.create_dropdown(ds, dim, selected_var)
[docs]
def generate_dimension_checklist(self, ds, selected_var):
if selected_var is None:
return []
# Use .sizes for mapping from dimension name to length (future-proof)
dimensions = list(ds[selected_var].sizes.keys())
return html.Div([
html.Label("Select dimensions to filter:", className="mb-2"),
dcc.Checklist(
id='dimension-checklist',
options=[{'label': dim, 'value': dim} for dim in dimensions],
value=dimensions,
className="mb-3"
)
])
[docs]
def generate_dimension_controls(self, ds, selected_dims, selected_var):
if selected_var is None or selected_dims is None:
return []
dimension_controls = []
for dim in selected_dims:
dim_lower = dim.lower()
# Determine default control type based on dimension characteristics
default_control = self._get_default_control_type(
ds, selected_var, dim)
controls = [
html.Label(f"{dim} control type:", className="mb-2"),
dcc.RadioItems(
id={'type': 'dim-control-type', 'index': dim},
options=[
{'label': 'Slider (Range)', 'value': 'slider'},
{'label': 'Dropdown (Single)', 'value': 'dropdown'}
],
value=default_control,
inline=True,
className="mb-3"
),
html.Div(id={'type': 'dim-control-widget', 'index': dim})
]
dimension_controls.append(
html.Div(controls, className="mb-4 p-3 border rounded")
)
return dimension_controls
def _get_default_control_type(self, ds, selected_var, dim):
"""Determine the default control type for a dimension"""
dim_lower = dim.lower()
dim_size = ds[selected_var][dim].size
# Spatial dimensions typically work better with sliders
if any(key in dim_lower for key in ['lat', 'lon', 'x', 'y']):
return 'slider'
# Time dimensions often work better with sliders
elif any(key in dim_lower for key in ['time', 'date']):
return 'slider'
# Elevation/depth can work with either, default to slider for large ranges
elif any(key in dim_lower for key in ['depth', 'elevation', 'height', 'level']):
return 'slider' if dim_size > 5 else 'dropdown'
# For other dimensions, use dropdown if small, slider if large
else:
return 'slider' if dim_size > 10 else 'dropdown'
[docs]
def create_range_slider(self, ds, dim, selected_var):
dim_values = ds[selected_var][dim].values
sorted_dim_values = sorted(dim_values)
min_idx = 0
max_idx = len(sorted_dim_values) - 1
# Set default range to middle 50% of the data
range_25 = int(0.25 * max_idx)
range_75 = int(0.75 * max_idx)
step = max(1, len(sorted_dim_values) // 20) # More granular steps
# Create marks for better visualization
marks = self._create_slider_marks(
ds, selected_var, dim, sorted_dim_values, step)
return html.Div([
html.Label(f'Select {dim} range', className="mb-2"),
dcc.RangeSlider(
id={'type': 'dimension-slider', 'index': dim},
min=min_idx,
max=max_idx,
value=[range_25, range_75],
marks=marks,
step=1,
tooltip={"placement": "bottom", "always_visible": True},
className="mb-2"
),
html.Div([
html.Small(f"Range: {self._format_dim_value(sorted_dim_values[range_25])} to {self._format_dim_value(sorted_dim_values[range_75])}",
className="text-muted")
], id={'type': 'slider-output', 'index': dim})
])
[docs]
def create_dropdown(self, ds, dim, selected_var):
dim_values = ds[selected_var][dim].values
# For large dimensions, show a subset of values to avoid overwhelming the dropdown
if len(dim_values) > 100:
# Sample every nth value for display
step = len(dim_values) // 100
display_values = dim_values[::step]
display_indices = list(range(0, len(dim_values), step))
else:
display_values = dim_values
display_indices = list(range(len(dim_values)))
# Create better labels for the dropdown
options = []
for idx, val in zip(display_indices, display_values):
if isinstance(val, (np.datetime64, np.timedelta64)):
label = str(val)
else:
label = f"{val:.4g}" if isinstance(
val, (int, float)) else str(val)
options.append({'label': label, 'value': idx})
return html.Div([
html.Label(f'Select {dim}', className="mb-2"),
dcc.Dropdown(
id={'type': 'dimension-dropdown', 'index': dim},
options=options,
placeholder=f"Select {dim}",
className="mb-2"
),
html.Div([
html.Small(f"Available: {len(dim_values)} values",
className="text-muted")
])
])
def _create_slider_marks(self, ds, selected_var, dim, sorted_dim_values, step):
"""Create marks for the slider with proper formatting"""
marks = {}
# Try to get coordinate reference system for spatial dimensions
crs = self._get_crs(ds, selected_var)
dim_lower = dim.lower()
# Create marks with appropriate formatting
for i in range(0, len(sorted_dim_values), step):
val = sorted_dim_values[i]
if dim_lower in ['x', 'lon'] and crs is not None:
try:
# Convert to degrees for display
transformer = pyproj.Transformer.from_crs(
crs, 4326, always_xy=True)
lon, _ = transformer.transform(val, 0)
marks[i] = f"{lon:.2f}°"
except Exception:
marks[i] = self._format_dim_value(val)
elif dim_lower in ['y', 'lat'] and crs is not None:
try:
# Convert to degrees for display
transformer = pyproj.Transformer.from_crs(
crs, 4326, always_xy=True)
_, lat = transformer.transform(0, val)
marks[i] = f"{lat:.2f}°"
except Exception:
marks[i] = self._format_dim_value(val)
elif isinstance(val, (np.datetime64, np.timedelta64)):
marks[i] = str(val)[:10] # Truncate long datetime strings
else:
marks[i] = self._format_dim_value(val)
return marks
def _get_crs(self, ds, selected_var):
"""Get coordinate reference system from dataset"""
try:
grid_mapping = ds[selected_var].attrs.get('grid_mapping')
if grid_mapping and grid_mapping in ds:
gm_var = ds[grid_mapping]
return pyproj.CRS.from_cf(gm_var.attrs)
except Exception:
pass
return None
def _format_dim_value(self, val):
"""Format dimension values for display with proper type handling"""
if isinstance(val, (np.datetime64, np.timedelta64)):
return str(val)[:10] # Truncate long datetime strings
elif isinstance(val, (int, float)):
return f"{val:.4g}"
else:
return str(val)