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
46 changes: 41 additions & 5 deletions src/e3sm_siteview/analysis/column_heatmap.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json

import netCDF4
import numpy as np
import plotly.graph_objects as go
from trame.app import TrameComponent
Expand Down Expand Up @@ -28,6 +29,7 @@ def __init__(self, server, column_reader):

self._build_ui()
self.bind_reactivity()
self._compute_heatmap()

@property
def name(self):
Expand All @@ -52,6 +54,22 @@ def unbind_reactivity(self):
def _sync_color_by(self, color_by):
self.ctx.setup.surface_chart.color_by = color_by

def _time_labels(self):
tdim = self.single_column_reader.GetDimensions().get("time")
if tdim is None:
return [0]

if tdim.data is not None and tdim.units:
try:
dates = netCDF4.num2date(
tdim.data, tdim.units, calendar=tdim.calendar or "standard"
)
return [d.strftime("%Y-%m-%d %H:%M") for d in dates]
except (ValueError, TypeError):
pass

return list(range(tdim.size))

def _compute_heatmap(self, *_):
field = self.ctx.setup.surface_chart.color_by
col_id = self.ctx.setup.surface_chart.column
Expand All @@ -67,9 +85,10 @@ def _compute_heatmap(self, *_):
select_arrays.DisableAllArrays()
select_arrays.EnableArray(field)

time_labels = self._time_labels()
series = []
levels = None
for t in range(self.ctx.setup.time_index_max):
for t in range(len(time_labels)):
col.SetSlicing(json.dumps({"time": t}))
col.Update()
table = col.GetOutputDataObject(0)
Expand All @@ -92,17 +111,34 @@ def _compute_heatmap(self, *_):
fig = go.Figure(
data=go.Heatmap(
z=series.T,
x=time_labels,
y=levels,
colorscale="Viridis",
hovertemplate=(
f"time: %{{x}}<br>lev: %{{y}}<br>{field}: %{{z}}<extra></extra>"
),
)
)
# Only label a handful of time steps to keep the axis readable
tick_step = max(1, len(time_labels) // 6)
tick_vals = time_labels[::tick_step]
tick_text = [str(v).replace(" ", "<br>") for v in tick_vals]

fig.update_layout(
xaxis_title="time",
yaxis={"title": field, "autorange": "reversed"},
title={"text": field, "x": 0.5, "xanchor": "center"},
xaxis={
"title": "time",
"side": "bottom",
"type": "category",
"tickmode": "array",
"tickvals": tick_vals,
"ticktext": tick_text,
"tickangle": 0,
},
yaxis={"autorange": "reversed"},
showlegend=False,
margin={"b": 0, "l": 0, "r": 0, "t": 0},
margin={"b": 60, "l": 0, "r": 0, "t": 30},
)
fig.update_xaxes(side="top")

with self.state:
self.update_figure(fig)
Expand Down
6 changes: 3 additions & 3 deletions src/e3sm_siteview/components/site_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def __init__(self, **_):
"`left: ${region[0]}%;top:${region[1]}%;width:${region[2]}%;height:${region[3]}%;`",
),
)
html.Div(classes="coord-equator")
html.Div(classes="coord-meridian")
# html.Div(classes="coord-equator")
# html.Div(classes="coord-meridian")
html.Div(
classes="coord-radius",
style=(
Expand Down Expand Up @@ -178,7 +178,7 @@ def _on_radius(self, site_radius, site_radius_unit, **_):
site_radius = 1

self.ctx.setup.radius_deg = site_radius * (
1.0 if site_radius_unit == "deg" else 111.111
1.0 if site_radius_unit == "deg" else 1.0 / 111.111
)

@change("site_lat", "site_lon")
Expand Down
8 changes: 8 additions & 0 deletions src/e3sm_siteview/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ def __init__(self, server, **defaults):
self.locator.SetDataSet(loop.GetOutput())
self.locator.BuildLocator()

# Bind volume color_by to surface chart
self.volume.watch(
["color_by"], self.push_volume_color_by_to_surface_chart, sync=True
)

@property
def ctrl(self):
return self.server.controller
Expand All @@ -177,6 +182,9 @@ def ctrl(self):
def ctx(self):
return self.server.context

def push_volume_color_by_to_surface_chart(self, *_):
self.surface_chart.color_by = self.volume.color_by

@dataclass.watch("time_values", sync=True)
def _on_time_values(self, values):
self.time_index_max = len(values) - 1
Expand Down
6 changes: 4 additions & 2 deletions src/e3sm_siteview/io/eam_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,18 +100,20 @@ def _interface_pressures(cls, table):

Prefers the ilev field array; if only lev is present, derives interfaces
by averaging adjacent midpoints and extrapolating the top/bottom.
The result is returned in reversed order (bottom first, top last).
"""
fd = table.GetFieldData()
ilev = fd.GetAbstractArray(cls.ILEV)
if ilev is not None:
return numpy_support.vtk_to_numpy(ilev).reshape(-1).astype(np.float64)
iface = numpy_support.vtk_to_numpy(ilev).reshape(-1).astype(np.float64)
return iface[::-1].copy()
lev = fd.GetAbstractArray(cls.LEV)
if lev is not None:
levp = numpy_support.vtk_to_numpy(lev).reshape(-1).astype(np.float64)
mids = 0.5 * (levp[:-1] + levp[1:])
top = levp[0] - (mids[0] - levp[0])
bot = levp[-1] + (levp[-1] - mids[-1])
return np.concatenate([[top], mids, [bot]])
return np.concatenate([[top], mids, [bot]])[::-1].copy()
return None

# -- execution ------------------------------------------------------------
Expand Down
8 changes: 7 additions & 1 deletion src/e3sm_siteview/io/eam_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def __init__(self, name, size, data=None):
self.size = size
self.long_name = None
self.units = None
self.calendar = None
self.data = data # Store the actual dimension coordinate values

def __getitem__(self, key):
Expand All @@ -50,7 +51,7 @@ def __setitem__(self, key, value):
setattr(self, key, value)

def update_from_variable(self, var_info):
"""Update metadata from netCDF variable info - only long_name and units."""
"""Update metadata from netCDF variable info - long_name, units and calendar."""
try:
self.long_name = var_info.getncattr("long_name")
except AttributeError:
Expand All @@ -61,6 +62,11 @@ def update_from_variable(self, var_info):
except AttributeError:
pass

try:
self.calendar = var_info.getncattr("calendar")
except AttributeError:
pass

def __repr__(self):
return f"DimMeta(name='{self.name}', size={self.size}, long_name='{self.long_name}')"

Expand Down
11 changes: 11 additions & 0 deletions src/e3sm_siteview/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ def create_viewers(server):
w = (lon_max - lon_min) / 360
x_percent.append((0.5 + o, w))

# make sure we go left to right
# x_percent = [(origin_percent, width_percent), ...]
x_percent.sort()
if len(x_percent) == 2 and x_percent[0][0] + x_percent[0][1] == x_percent[1][0]:
# remove split, just make 1 region
x_percent = [(x_percent[0][0], x_percent[0][1] + x_percent[1][1])]

if len(x_percent) == 1 and x_percent[0][1] == 1:
# Full region => no highlight
x_percent = []

server.state.data_regions = [
(lon_o * 100, lat_o, lon_w * 100, lat_h) for lon_o, lon_w in x_percent
]
Expand Down
Loading