Source code for main
import dash
from dash import Dash, html, dcc, Input, Output, State, callback_context
import dash_bootstrap_components as dbc
# Setup module for initialization tasks
from setup import setup_cartopy_data
from variables import VariableSelection
from dimension import DimensionSelection
from data import DataManager, DatasetLoader
[docs]
class ViewerApp:
def __init__(self):
self.app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
self.app.config.suppress_callback_exceptions = True
self.ds = None
self.dataset_engine = None
self.dataseturl = None
self.dataset_loader = DatasetLoader()
setup_cartopy_data()
self.app.layout = dbc.Container([
dbc.Row([
dbc.Col(html.H1("NCZarr Viewer",
className="text-center mb-4"), width=12)
]),
# Top row: Load Dataset and Variable Selection side by side
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("Load Dataset"),
dbc.CardBody([
html.Div([
html.Label("Choose from predefined datasets:",
className="mb-2",
style={"fontWeight": "bold"}),
dcc.Dropdown(
id='dataset-dropdown',
options=[
{
"label": ("https://s3.waw3-1.cloudferro.com/"
"emodnet/emodnet_seabed_habitats/12548/"
"EUSeaMap_2023.zarr"),
"value": ("https://s3.waw3-1.cloudferro.com/"
"emodnet/emodnet_seabed_habitats/12548/"
"EUSeaMap_2023.zarr")
},
{
"label": ("https://s3.waw3-1.cloudferro.com/"
"emodnet/emodnet_arco/emodnet_chemistry/"
"water_body_dissolved_inorganic_nitrogen/"
"water_body_dissolved_inorganic_nitrogen_"
"masked_using_relative_error_threshold_0.5_"
"baltic_sea/Water_body_dissolved_inorganic_"
"nitrogen.4Danl.zarr"),
"value": ("https://s3.waw3-1.cloudferro.com/"
"emodnet/emodnet_arco/emodnet_chemistry/"
"water_body_dissolved_inorganic_nitrogen/"
"water_body_dissolved_inorganic_nitrogen_"
"masked_using_relative_error_threshold_0.5_"
"baltic_sea/Water_body_dissolved_inorganic_"
"nitrogen.4Danl.zarr")
},
{
"label": ("https://s3.waw3-1.cloudferro.com/"
"emodnet/emodnet_geology/12495/"
"EMODnet_Seabed_Substrate_1M.zarr"),
"value": ("https://s3.waw3-1.cloudferro.com/"
"emodnet/emodnet_geology/12495/"
"EMODnet_Seabed_Substrate_1M.zarr")
},
{
"label": ("https://s3.waw3-1.cloudferro.com/mdl-arco-geo-001/"
"arco/ARCTIC_MULTIYEAR_BGC_002_005/"
"cmems_mod_arc_bgc_my_ecosmo_P1M_202105/"
"geoChunked.zarr"),
"value": ("https://s3.waw3-1.cloudferro.com/mdl-arco-geo-001/"
"arco/ARCTIC_MULTIYEAR_BGC_002_005/"
"cmems_mod_arc_bgc_my_ecosmo_P1M_202105/"
"geoChunked.zarr")
}
],
placeholder='Select a dataset from the list',
style={'width': '100%'},
clearable=True,
searchable=True
),
html.Br(),
html.Label("Or enter a custom URL:",
className="mb-2",
style={"fontWeight": "bold"}),
dcc.Input(
id='dataset-url-input',
type='text',
placeholder='Enter custom dataset URL or path',
style={'width': '100%'}
),
html.Div([
html.Small([
html.I("💡 Tip: For netcdf files on s3 storage, add ",
style={"color": "#6c757d"}),
html.Code("#mode=bytes", style={
"backgroundColor": "#f8f9fa", "padding": "2px 4px", "borderRadius": "3px"}),
html.I(" at the end of the URL to ensure proper data access.",
style={"color": "#6c757d"})
], style={"marginTop": "5px", "fontSize": "12px"})
]),
html.Br(),
html.Br(),
html.Label("Backend Selection:",
className="mb-2",
style={"fontWeight": "bold"}),
dcc.Dropdown(
id='backend-dropdown',
options=[
{"label": "Auto-detect", "value": "auto"},
{"label": "xarray", "value": "xarray"},
{"label": "Copernicus Marine",
"value": "copernicusmarine"},
],
value="auto",
style={'width': '100%'}
),
html.Br(),
html.Label("Backend Configuration (JSON):",
className="mb-2",
style={"fontWeight": "bold"}),
html.Div([
html.A("How to use kwargs",
href="#",
id="help-link",
style={"color": "#007bff", "textDecoration": "underline"}),
html.Span(" - Click for examples",
className="text-muted",
style={"fontSize": "12px"})
], className="mb-2"),
dcc.Textarea(
id='additional-params-input',
placeholder='{"backend": "xarray", "engine": "zarr", "chunks": {"time": 1}}',
style={'width': '100%', 'height': '80px'}
),
# Help modal content (hidden by default)
dbc.Modal([
dbc.ModalHeader(dbc.ModalTitle(
"How to use Backend Configuration")),
dbc.ModalBody([
html.H6("xarray Backend Examples:"),
html.Pre('''
{
"backend": "xarray",
"engine": "zarr",
"chunks": {"time": 1},
"decode_timedelta": true
}
{
"backend": "xarray",
"engine": "netcdf4",
"chunks": {"time": 1, "lat": 100, "lon": 100},
"decode_timedelta": false
}
''', style={"backgroundColor": "#f8f9fa", "padding": "10px", "borderRadius": "4px"}),
html.H6(
"Copernicus Marine Examples:"),
html.Pre('''
{
"backend": "copernicusmarine",
"engine": "copernicusmarinetoolbox",
"username": "your_username",
"password": "your_password",
"dataset_id": "dataset_identifier"
}
''', style={"backgroundColor": "#f8f9fa", "padding": "10px", "borderRadius": "4px"}),
html.H6("Notes:"),
html.Ul([
html.Li(
"The 'backend' field in JSON overrides the dropdown selection"),
html.Li(
"For xarray: engine can be 'zarr', 'netcdf4'"),
html.Li(
"For Copernicus Marine: engine can be 'copernicusmarinetoolbox' or 'custom_open_zarr.open_zarr'"),
html.Li(
"Additional parameters like chunks, decode_timedelta, s3 credentials can be included"),
html.Li(
"All xarray parameters (chunks, decode_timedelta, etc.) can be passed through JSON"),
html.Li(
"Common xarray params: chunks, decode_cf, decode_timedelta, mask_and_scale, storage_options")
])
]),
dbc.ModalFooter(
dbc.Button(
"Close", id="close-help-modal", className="ms-auto")
)
], id="help-modal", is_open=False)
]),
html.Br(),
html.Br(),
dbc.Button('Load Dataset', id='load-dataset-button',
color='primary', n_clicks=0, className='mb-2'),
dcc.Loading(html.Div(id='load-status'),
type='default'),
])
], className='mb-3'),
], width=10),
dbc.Col([
dbc.Card([
dbc.CardHeader("Variable Selection"),
dbc.CardBody([
dcc.Dropdown(id='variable-dropdown',
style={'width': '100%'}),
])
], className='mb-3'),
], width=6),
]),
# Second row: Dimension Selection
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("Dimension Selection"),
dbc.CardBody([
html.Div(id='dimension-checklist-container'),
html.Div(id='dimension-dropdowns-container'),
])
], className='mb-3'),
], width=12),
]),
# Third row: Select Data (filter and button)
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("Select Data"),
dbc.CardBody([
html.Div([
html.Label("Data Filter (Min/Max):",
className="mb-1"),
dbc.Input(id='data-filter-min', type='number', placeholder='Min value', style={
'width': '45%', 'display': 'inline-block', 'marginRight': '10px'}),
dbc.Input(id='data-filter-max', type='number', placeholder='Max value', style={
'width': '45%', 'display': 'inline-block'}),
], className='mb-2'),
dbc.Button('Show Data Quick Stats (Max/Min/Med/Stdev)',
id='show-data-button', color='info', n_clicks=0, className='mt-2'),
])
], className='mb-3'),
], width=12),
]),
# Fourth row: Quick Stats
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("Quick Stats (Max/Min/Mean/Med/STDEV)"),
dbc.CardBody([
dcc.Loading(
html.Div(id='data-array-display'), type='circle'),
])
], className='mb-3'),
], width=12),
]),
# Fifth row: Plot Selected Data (full width)
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("Plot Selected Data"),
dbc.CardBody([
dcc.Loading(html.Div([
html.Div(
id='map-container', children=[html.Img(id='map', style={'width': '100%', 'height': 'auto'})]),
]), type='circle'),
dbc.Button('Extract Image', id='extract-plot-button',
color='success', n_clicks=0, className='mt-2'),
])
]),
], width=12),
]),
# Sixth row: Raster Image Display (full width)
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("Generated Raster Image"),
dbc.CardBody([
html.Div(id='raster-container', children=[
html.P("Click 'Extract Image' to generate a raster image",
className="text-muted text-center")
])
])
]),
], width=12),
]),
# Metadata row: full width at the bottom
dbc.Row([
dbc.Col([
html.Div(id='dataset-info-container', className='mt-3'),
], width=12),
]),
# Add a card for variable metadata display
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader("Variable Metadata"),
dbc.CardBody([
html.Div(id='variable-metadata-container')
])
], className='mb-3'),
], width=12),
]),
# Debug and logging row
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader([
"Debug Output & Logs",
dbc.Button("Clear Logs", id="clear-logs-button",
color="warning", size="sm", className="float-end")
]),
dbc.CardBody([
html.Div(id='debug-output-container'),
html.Div(id='logs-container', style={
'backgroundColor': '#f8f9fa',
'border': '1px solid #dee2e6',
'borderRadius': '4px',
'padding': '10px',
'fontFamily': 'monospace',
'fontSize': '12px',
'maxHeight': '300px',
'overflowY': 'auto',
'whiteSpace': 'pre-wrap'
})
])
], className='mb-3'),
], width=12),
]),
dcc.Store(id='selected-dimensions-store'),
html.Div(id='debug-output')
], fluid=True)
# Register all callbacks at startup, using a getter for self.ds
self.variable_selection = VariableSelection(self.app, lambda: self.ds)
self.variable_selection.setup_callbacks()
self.dimension_selection = DimensionSelection(
self.app, lambda: self.ds)
self.dimension_selection.setup_callbacks()
# Use the new unified DataManager for all data operations
self.data_manager = DataManager(self.app, lambda: self.ds)
self.data_manager.setup_callbacks()
self.setup_callbacks()
self.update_variable_dropdown()
self.setup_variable_metadata_callback()
self.setup_help_modal_callback()
[docs]
def update_variable_dropdown(self):
# Update the variable dropdown to list all data variables
if self.ds is not None:
from dash import callback_context
options = [
{"label": f"{var} ({self.ds[var].attrs.get('long_name', var)})",
"value": var}
for var in self.ds.data_vars.keys()
]
self.app.callback_map['variable-dropdown.value']['inputs'][0]['options'] = options
[docs]
def get_metadata_summary(self):
if self.ds is None:
return "No dataset loaded."
try:
vars = self.ds.data_vars.keys()
dims = self.ds.dims.items()
attrs = self.ds.attrs.items()
# Build a table of variables with their dimensions and CF attributes
var_rows = []
for var in vars:
v = self.ds[var]
dims_str = ', '.join([f"{d} ({v.sizes[d]})" for d in v.dims])
long_name = v.attrs.get('long_name', '-')
standard_name = v.attrs.get('standard_name', '-')
grid_mapping = v.attrs.get('grid_mapping', '-')
var_rows.append(html.Tr([
html.Td(var),
html.Td(dims_str),
html.Td(long_name),
html.Td(standard_name),
html.Td(grid_mapping)
]))
# If grid mapping variable exists, show its attributes
grid_mapping_info = []
for var in vars:
v = self.ds[var]
grid_mapping_name = v.attrs.get('grid_mapping')
if grid_mapping_name and grid_mapping_name in self.ds:
gm_var = self.ds[grid_mapping_name]
grid_mapping_info.append(html.Div([
html.H6(f"Grid Mapping: {grid_mapping_name}"),
html.Ul([
html.Li(f"{k}: {v}") for k, v in gm_var.attrs.items()
])
]))
# Make dataset metadata div much neater and more readable
return html.Div([
html.H5("Dataset Metadata (CF Conventions)", style={
"marginTop": "10px", "color": "#222"}),
html.Table([
html.Thead(html.Tr([
html.Th("Variable"),
html.Th("Dimensions"),
html.Th("long_name"),
html.Th("standard_name"),
html.Th("grid_mapping")
])),
html.Tbody(var_rows)
], id="dataset-info-table", style={"width": "100%", "fontSize": "13px", "color": "#222", "background": "#fff", "borderRadius": "6px", "padding": "8px", "marginBottom": "16px"}),
html.H6("Global Attributes", style={
"color": "#222", "marginTop": "12px"}),
html.Table([
html.Tbody([
html.Tr([
html.Td(html.B(str(k)), style={
"paddingRight": "8px"}),
html.Td(str(v))
]) for k, v in attrs
])
], style={"fontSize": "13px", "background": "#f8f9fa", "borderRadius": "6px", "padding": "8px", "width": "auto"}),
*grid_mapping_info
], className="dataset-info-container", style={"background": "#fff", "color": "#222", "borderRadius": "8px", "padding": "16px", "marginBottom": "16px", "boxShadow": "0 2px 8px rgba(0,0,0,0.04)"})
except Exception as e:
return f"Error reading metadata: {e}"
[docs]
def setup_help_modal_callback(self):
@self.app.callback(
Output("help-modal", "is_open"),
[Input("help-link", "n_clicks"),
Input("close-help-modal", "n_clicks")],
[State("help-modal", "is_open")],
prevent_initial_call=True
)
def toggle_help_modal(n1, n2, is_open):
if n1 or n2:
return not is_open
return is_open
[docs]
def setup_callbacks(self):
@self.app.callback(
[Output('load-status', 'children'),
Output('dataset-info-container', 'children'),
Output('logs-container', 'children')],
[Input('load-dataset-button', 'n_clicks'),
Input('clear-logs-button', 'n_clicks')],
[State('dataset-dropdown', 'value'),
State('dataset-url-input', 'value'),
State('backend-dropdown', 'value'),
State('additional-params-input', 'value')],
prevent_initial_call=True
)
def handle_dataset_operations(load_clicks, clear_clicks, dropdown_url, custom_url, backend, additional_params):
# Determine which button was clicked
ctx = callback_context
if ctx.triggered:
triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]
else:
triggered_id = None
if triggered_id == 'clear-logs-button':
# Clear logs operation
self.dataset_loader.clear_logs()
return dash.no_update, dash.no_update, "Logs cleared."
elif triggered_id == 'load-dataset-button':
# Load dataset operation
# Use dropdown selection if available, otherwise use custom input
url = dropdown_url if dropdown_url else custom_url
if not url:
return "Please select a dataset from the list or enter a custom URL.", dash.no_update, dash.no_update
# Parse additional parameters
kwargs = {}
if additional_params and additional_params.strip():
try:
import json
kwargs = json.loads(additional_params)
except json.JSONDecodeError as e:
return f"Invalid JSON in backend args: {e}", dash.no_update, dash.no_update
# Parse JSON configuration
config = {}
if additional_params and additional_params.strip():
try:
import json
config = json.loads(additional_params)
except json.JSONDecodeError as e:
return f"Invalid JSON in backend args: {e}", dash.no_update, dash.no_update
# If no backend specified in JSON, use dropdown selection
if 'backend' not in config and backend != "auto":
config['backend'] = backend
# Show loading spinner automatically via dcc.Loading
self.dataseturl = url
# Load dataset with enhanced error handling
result = self.dataset_loader.load_dataset(
url, **config)
if isinstance(result, tuple) and len(result) == 2:
self.ds, self.dataset_engine = result
if self.ds is None:
error_msg = f"Failed to load dataset from {url}. Error: {self.dataset_engine}"
return error_msg, dash.no_update, self.dataset_loader.get_logs()
else:
success_msg = f"Successfully loaded dataset from {url} using {self.dataset_engine}"
return success_msg, self.get_metadata_summary(), self.dataset_loader.get_logs()
else:
return f"Unexpected result from dataset loader: {result}", dash.no_update, self.dataset_loader.get_logs()
# Default case - shouldn't happen
return dash.no_update, dash.no_update, dash.no_update
[docs]
def setup_variable_metadata_callback(self):
@self.app.callback(
Output('variable-metadata-container', 'children'),
Input('variable-dropdown', 'value')
)
def update_variable_metadata(selected_var):
ds = self.ds
if ds is None or not selected_var or selected_var not in ds:
return "No variable selected."
v = ds[selected_var]
# Attributes (show all, including long_name, units, etc.)
attr_items = []
for k, vv in v.attrs.items():
attr_items.append(html.Tr([
html.Td(str(k), style={
"fontWeight": "bold", "paddingRight": "8px"}),
html.Td(str(vv))
]))
attr_table = html.Table([
html.Tbody(attr_items)
], style={"fontSize": "14px", "marginBottom": "12px", "background": "#f8f9fa", "borderRadius": "6px", "padding": "8px", "width": "auto"})
# Dimensions (use .sizes for mapping from name to length)
dims = v.dims
dim_items = [html.Li(f"{d}: {v.sizes[d]}") for d in dims]
# Coordinates (show preview values, handle loading errors, only load a small slice)
coord_items = []
for c in v.coords:
try:
arr = v.coords[c].isel({d: slice(0, 5)
for d in v.coords[c].dims}).values
preview = ', '.join([str(arr[i])
for i in range(min(5, arr.size))])
if v.coords[c].sizes and list(v.coords[c].sizes.values())[0] > 5:
preview += ', ...'
coord_items.append(html.Li([
html.B(
c), f": [{preview}] (size={v.coords[c].sizes[list(v.coords[c].sizes.keys())[0]]})"
]))
except Exception as e:
coord_items.append(html.Li([
html.B(c), f": [Error loading values: {e}]"
]))
# Grid mapping
grid_mapping_name = v.attrs.get('grid_mapping')
grid_mapping_info = None
if grid_mapping_name and grid_mapping_name in ds:
gm_var = ds[grid_mapping_name]
grid_mapping_info = html.Div([
html.H6(f"Grid Mapping: {grid_mapping_name}"),
html.Ul([
html.Li(f"{k}: {vv}") for k, vv in gm_var.attrs.items()
])
])
return html.Div([
html.H5(f"Variable: {selected_var}"),
html.H6("Attributes:"),
attr_table,
html.H6("Dimensions:"),
html.Ul(dim_items),
html.H6("Coordinates (preview):"),
html.Ul(coord_items),
grid_mapping_info if grid_mapping_info else None
], style={"background": "#fff", "color": "#222", "borderRadius": "8px", "padding": "16px", "marginBottom": "16px", "boxShadow": "0 2px 8px rgba(0,0,0,0.04)"})
if __name__ == '__main__':
app = ViewerApp()
app.run()