diff --git a/src/e3sm_siteview/analysis/column_heatmap.py b/src/e3sm_siteview/analysis/column_heatmap.py
index 290c8e2..b92c179 100644
--- a/src/e3sm_siteview/analysis/column_heatmap.py
+++ b/src/e3sm_siteview/analysis/column_heatmap.py
@@ -1,5 +1,6 @@
import json
+import netCDF4
import numpy as np
import plotly.graph_objects as go
from trame.app import TrameComponent
@@ -28,6 +29,7 @@ def __init__(self, server, column_reader):
self._build_ui()
self.bind_reactivity()
+ self._compute_heatmap()
@property
def name(self):
@@ -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
@@ -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)
@@ -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}}
lev: %{{y}}
{field}: %{{z}}"
+ ),
)
)
+ # 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(" ", "
") 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)
diff --git a/src/e3sm_siteview/components/site_selection.py b/src/e3sm_siteview/components/site_selection.py
index 177e4d4..636cc28 100644
--- a/src/e3sm_siteview/components/site_selection.py
+++ b/src/e3sm_siteview/components/site_selection.py
@@ -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=(
@@ -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")
diff --git a/src/e3sm_siteview/data_models.py b/src/e3sm_siteview/data_models.py
index d5a9c26..9fbe063 100644
--- a/src/e3sm_siteview/data_models.py
+++ b/src/e3sm_siteview/data_models.py
@@ -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
@@ -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
diff --git a/src/e3sm_siteview/io/eam_filters.py b/src/e3sm_siteview/io/eam_filters.py
index b0455a6..3130766 100644
--- a/src/e3sm_siteview/io/eam_filters.py
+++ b/src/e3sm_siteview/io/eam_filters.py
@@ -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 ------------------------------------------------------------
diff --git a/src/e3sm_siteview/io/eam_reader.py b/src/e3sm_siteview/io/eam_reader.py
index b3fdf4f..e3d6db0 100644
--- a/src/e3sm_siteview/io/eam_reader.py
+++ b/src/e3sm_siteview/io/eam_reader.py
@@ -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):
@@ -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:
@@ -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}')"
diff --git a/src/e3sm_siteview/viewer.py b/src/e3sm_siteview/viewer.py
index 868f138..54029fb 100644
--- a/src/e3sm_siteview/viewer.py
+++ b/src/e3sm_siteview/viewer.py
@@ -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
]