Add plotting enhancements#454
Conversation
Set conditional based on values for different float formats going into original DataFrame Set number of significant values in adf_web.py to catch unnecessary trailing zeros in decimals. Also move most imports to beginning of script
This reverts commit 0dd3910.
nusbaume
left a comment
There was a problem hiding this comment.
Thanks for all of the code cleanup and plot enhancements @justin-richling! I realize it looks like I have a bazillion comments, but the vast majority should hopefully be easy or trivial to implement. Of course if you do have any questions or concerns with anything I wrote just let me know!
| @@ -0,0 +1,649 @@ | |||
| name: adf_npl-2024a | |||
There was a problem hiding this comment.
I personally don't think we should have an NPL environment file in the ADF (or LDF). In general it's best to only have the minimum number of packages you need to run all the ADF scripts, which I am guessing is significantly less than what is contained here.
If you need help creating a more up-to-date environment file for the ADF then please let me know, as I would be happy to help work on it. Thanks!
| !.vscode/extensions.json | ||
| .history | ||
|
|
||
| #NCL RGB files |
There was a problem hiding this comment.
Total nit-pick, but I might add a space in the comment here to match the format of all the other comments in this file:
| #NCL RGB files | |
| # NCL RGB files |
| vres["season"] = plot['season'] | ||
| vres["hemi"] = plot['hemi'] |
There was a problem hiding this comment.
Another nit-pick, but I might use the same quotation marks on both sides of the equation here (in case someone who doesn't know python well looks at the code and assumes the behavior is different):
| vres["season"] = plot['season'] | |
| vres["hemi"] = plot['hemi'] | |
| vres['season'] = plot['season'] | |
| vres['hemi'] = plot['hemi'] |
| vres["umdlfld_nowrap"] = umseasons[s] | ||
| vres["vmdlfld_nowrap"] = vmseasons[s] | ||
| vres["uobsfld_nowrap"] = uoseasons[s] | ||
| vres["vobsfld_nowrap"] = voseasons[s] | ||
| vres["udiffld_nowrap"] = udseasons[s] | ||
| vres["vdiffld_nowrap"] = vdseasons[s] | ||
| vres["upctdiffld_nowrap"] = upseasons[s] | ||
| vres["vpctdiffld_nowrap"] = vpseasons[s] |
There was a problem hiding this comment.
I realize this is purely an opinion, but in general I feel that "kwarg"-like dictionary inputs into python functions should only contain metadata (e.g. plot_type), and not required data like the fields being plotted. Given this, I think I would bring in the extra vector data fields as optional function arguments instead of keyword arguments. Of course feel free to push back here if you disagree!
| pf.plot_map_and_save(adfobj, plot_name, case_nickname, base_nickname, | ||
| [syear_cases[case_idx],eyear_cases[case_idx]], | ||
| [syear_baseline,eyear_baseline], | ||
| umseasons[s], uoseasons[s], | ||
| udseasons[s], upseasons[s], | ||
| obs=obs, **vres) |
There was a problem hiding this comment.
If you do accept my suggestion on passing the main data fields as optional arguments, then this function call would look something like this instead:
| pf.plot_map_and_save(adfobj, plot_name, case_nickname, base_nickname, | |
| [syear_cases[case_idx],eyear_cases[case_idx]], | |
| [syear_baseline,eyear_baseline], | |
| umseasons[s], uoseasons[s], | |
| udseasons[s], upseasons[s], | |
| obs=obs, **vres) | |
| pf.plot_map_and_save(adfobj, plot_name, case_nickname, base_nickname, | |
| [syear_cases[case_idx],eyear_cases[case_idx]], | |
| [syear_baseline,eyear_baseline], | |
| umseasons[s], uoseasons[s], | |
| udseasons[s], upseasons[s], | |
| vm=vmseasons[s], vo=voseasons[s], | |
| vd=vdseasons[s], vp=vpseasons[s], | |
| obs=obs, **vres) |
Then in the plot_map_and_save function itself all of the vx variables would be None in the function argument list.
|
|
||
| fig, ax = plt.subplots(nrows=3) | ||
| ax = [ax[0],ax[1],ax[2]] | ||
| fig, ax = plt.subplots(nrows=3)#figsize=(6,8), |
There was a problem hiding this comment.
Remove commented-out code?
| plot_kwargs = kwargs.copy() | ||
| plot_kwargs["colormap_2d"] = use_cmap | ||
| diff_kwargs = plot_kwargs.copy() | ||
| diff_kwargs["type"] = "diff" | ||
| diff_kwargs.pop("norm", None) |
There was a problem hiding this comment.
Instead of making multiple copies, why not just make one copy that is then modified for the diff and pctdiff plots? The only reason you would need multiple copies if you needed to save diff_kwargs after creating the zonal plot for the difference field.
| levs = np.unique(np.array(cp_info['levels1'])) | ||
| levs_diff = np.unique(np.array(cp_info['levelsdiff'])) | ||
| levs_pctdiff = np.unique(np.array(cp_info['levelspctdiff'])) | ||
| # Generate zonal plot: |
There was a problem hiding this comment.
Meridional plot?
| # Generate zonal plot: | |
| # Generate meridional plot: |
| fig, ax = plt.subplots(figsize=(8,10),nrows=4, constrained_layout=True, | ||
| sharey=True, **cp_info['subplots_opt']) | ||
|
|
||
| levs = np.unique(np.array(levels_sim)) |
There was a problem hiding this comment.
Just noting again that naming this variable to something like contour_levs might help make it obvious that this is not related to vertical levels.
| use_cmap = kwargs.get("colormap_2d", False) | ||
| plot_kwargs = kwargs.copy() | ||
| plot_kwargs["colormap_2d"] = use_cmap | ||
| diff_kwargs = plot_kwargs.copy() | ||
| diff_kwargs["type"] = "diff" | ||
| diff_kwargs.pop("norm", None) |
There was a problem hiding this comment.
Can we get away with only one copy of kwargs here as well?
| height="90%", # height : 90% | ||
| loc='lower left', | ||
| bbox_to_anchor=(1.05, 0.05, 1, 1), | ||
| bbox_transform=ax2.transAxes, |
There was a problem hiding this comment.
I think this ax2 needs to be pointed at an axs element
| height="90%", # height : 90% | ||
| loc='lower left', | ||
| bbox_to_anchor=(1.05, 0.05, 1, 1), | ||
| bbox_transform=ax3.transAxes, |
| height="90%", # height : 90% | ||
| loc='lower left', | ||
| bbox_to_anchor=(1.05, 0.05, 1, 1), | ||
| bbox_transform=ax4.transAxes, |
| a.set_boundary(circle, transform=a.transAxes) | ||
| a.gridlines(draw_labels=False, crs=ccrs.PlateCarree(), | ||
| lw=1, color="gray",y_inline=True, | ||
| xlocs=range(-180,180,90), ylocs=range(0,90,10)) |
There was a problem hiding this comment.
Is range(0,90,10)going to work for southern hemisphere?
| loc='lower left', | ||
| bbox_to_anchor=(1.05, 0, 1, 1), | ||
| bbox_to_anchor=(1.02, 0, 1, 1), | ||
| bbox_transform=ax2.transAxes, |
| height="100%", | ||
| loc='lower left', | ||
| bbox_to_anchor=(1.02, 0, 1, 1), | ||
| bbox_transform=ax3.transAxes, |
| msg += f" Trying if this an NCL color map" | ||
|
|
||
| url = guess_ncl_url(cmap_pctdiff) | ||
| locfil = "." / f"{cmap_pctdiff}.rgb" |
There was a problem hiding this comment.
I think you have to use some pathlib thing here, probably: locfil = Path.cwd() / f"{cmap_pctdiff}.rgb"
| pctdiff_mag = diff_mag / np.abs(obs_mag) * 100.0 | ||
| #pctdiff_mag = diff_mag / np.abs((mdl_mag + obs_mag)/2) * 100.0 | ||
| pctdiff_mag = pctdiff_mag.where(np.isfinite(pctdiff_mag), np.nan) | ||
| pctdiff_mag = pctdiff_mag.fillna(0.0) |
There was a problem hiding this comment.
Is this what we actually want? Divide by zero is replaced by np.nan, but then get set to 0.0. Won't that mean that any nan value ends up looking like a valid result in the plot?
| ax[i].quiver(lons[skip], lats[skip], umdlfld[skip], vmdlfld[skip], mdl_mag.values[skip], | ||
| transform=ccrs.PlateCarree(), cmap='Reds') | ||
| ax[i].quiver(lons[skip], lats[skip], uobsfld[skip], vobsfld[skip], obs_mag.values[skip], | ||
| transform=ccrs.PlateCarree(), cmap='Reds') |
There was a problem hiding this comment.
Both quiver calls are unconditional inside the if i in [0,1] block — so for both i=0 (model panel) and i=1 (baseline panel), it draws model vectors first and then baseline vectors on top. Both panels end up with overlaid arrows.
It seems like the intent is i=0 → model only, i=1 → baseline only. The fix should be:
if i == 0:
ax[i].quiver(..., umdlfld[skip], vmdlfld[skip], mdl_mag.values[skip], ...)
elif i == 1:
ax[i].quiver(..., uobsfld[skip], vobsfld[skip], obs_mag.values[skip], ...)
| scale_factor: 1 | ||
| add_offset: 0 | ||
| new_unit: "" | ||
| pct_diff_contour_levels: [-100,-75,-50,-40,-30,-20,-10,-8,-6,-4,-2,0,2,4,6,8,10,20,30,40,50,75,100] |
There was a problem hiding this comment.
Before this PR, pct_diff_colormap and pct_diff_contour_levels were at the top level of a variable's YAML entry and were found directly in kwargs.
Now:
- If the user has a
global_latlon_map(or similar) sub-block, they must putpct_diff_colormapinside that block
which means that the previous behavior of top-level placement is silently ignored. - If
plot_typeis not passed into the function at all,plot_type_dict = {}and there is no way for the user to override hard-coded values that are given inlib/plotting_utils.py: - Line 863:
cmap_pctdiff = "PuOr_r" - Line 882:
cmap_pctdiff = 'PuOr_r' - Line 896:
levels_pctdiff = [-100,-75,-50,-40,-30,-20,-10,-8,-6,-4,-2,0,2,4,6,8,10,20,30,40,50,75,100]
So the PR description's claim that users can still supply these args is only partially true. The configurability is preserved when nested under a plot-type block but silently lost in the flat/top-level case that was standard before.
This probably doesn't matter much, but it complicates customizing plots from the YAML file. Needs documentation at minimum.
| vmax = kwargs.get("vmax", None) | ||
| if use_cmap: | ||
| if kwargs["type"] == "pctdiff": | ||
| cmap = kwargs.get("pct_diff_colormap", "PuOr") |
There was a problem hiding this comment.
The hard-coded yaml default was PuOr_r right? Should that be used here?
brianpm
left a comment
There was a problem hiding this comment.
I left a few comments where I saw some inconsistent changes. Looks good overall. My first thought is that we could probably reduce total code, but no need for now.
New feature type
New plot and/or plot enhancement
What is this new feature?
Add flexibility for plotting (contour levels, colormap, etc.) based on pressure level and/or plot type (polar vs global)
Update plots for more professional look, ie Zonal and Meridional are very bare boned, lat/lon were missing units, some variables were not getting proper scaling, fit colorbar better to axes and extend if necessary, etc.
colormap_2d)Combine Lat/Lon Vector into Lat/Lon code since it was a lot of reused code.
Add option in variable defaults config for variable nickname; remove all percent diff args in defaults yaml and hard code in plotting script. The user will still have the option to supply these args in the yaml file if default is not desired.
Add option for old NCL color maps
Add units cleaning method to ensure units are always available, this was not enforced throughout the code base.
Add code to resolve levels for hemisphere and resolve colormap in
prep_contour_plotCore new features:
Different contours based on vertical levels
closes #452
closes #425
closes #373