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 ]