Skip to content

Add plotting enhancements#454

Open
justin-richling wants to merge 121 commits into
NCAR:mainfrom
justin-richling:plotting-enhancements
Open

Add plotting enhancements#454
justin-richling wants to merge 121 commits into
NCAR:mainfrom
justin-richling:plotting-enhancements

Conversation

@justin-richling

Copy link
Copy Markdown
Collaborator

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.

  • NOTE: There is an additional enhancement for 2d line plots for Zonal/Meridional that allows a color map for the 2d variables (See SWCF example below - variable defaults argument: colormap_2d)

Combine Lat/Lon Vector into Lat/Lon code since it was a lot of reused code.

  • NOTE: the calculation for percent difference is currently off for lat/lon vectors plots, so this map is being ignored and vector plots default back to the two cases on top and difference centered below

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_plot


Core new features:

Different contours based on vertical levels

RESTOM:
  colormap: "RdBu_r"
  contour_levels_range: [-100, 101, 5]
  diff_contour_range: [-10, 10, 0.5]
  polar_map:
    colormap:
      nh: "RdBu_r"
      sh: "terrain"
    contour_levels_range:
      #nh: [-100, 101, 5]
      sh: [-100, 0, 2.5]
   scale_factor: 1
  add_offset: 0
  new_unit: "W m$^{-2}$"
  category: "TOA energy flux"
  derivable_from: ['FLNT','FSNT']

SWCF:
  colormap: "Reds"
  polar_map:
    colormap:
      nh: "fakemap"
      sh: "Blues"
  contour_levels_range: [-150, 51, 10]
  diff_colormap: "BrBG"
  diff_contour_range: [-20, 21, 2]
  scale_factor: 1
  add_offset: 0
  new_unit: "Wm$^{-2}$"
  obs_file: "CERES_EBAF_Ed4.1_2001-2020.nc"
  obs_name: "CERES_EBAF_Ed4.1"
  obs_var_name: "toa_cre_sw_mon"
  obs_scale_factor: 1
  obs_add_offset: 0
  category: "TOA energy flux"
  colormap_2d: True

U:
  global_latlon_map:
    colormap: # if not specified, default to viridis for any missing values nested (levs, ie 850) or not
      200: "ncl_default"
      #850: "Spectral"
    contour_levels_range:
      200: [-10, 51, 2.5]
      850: [-10, 31, 2.5]
    diff_contour_range:
      200: [-15, 16, 2]
      850: [-5, 5.1, 0.5]
  polar_map:
    colormap:
      nh: "terrain"
      sh: "fake_cmap"
    contour_levels_range:
      nh:
        200: [-10, 31, 2.5]
        850: [-10, 13, 1]
      sh:
        200: [-10, 51, 5]
        850: [-10, 31, 2.5]
    diff_contour_range:
      200: [-10, 10, 1]
      850: [-5, 5.1, 0.5]
      #nh:
      #  200: [-15, 16, 2]
      #  850: [-5, 5.1, 0.5]
      #sh:
      #  200: [-15, 16, 2]
      #  850: [-5, 5.1, 0.5]
  zonal_mean:
    colormap: "ncl_default"
    contour_levels_linspace: [-10, 30, 15] # These will be applied to all zonal 3d mean plots
    #diff_contour_range: # default to figuring our the contour levels from difference from max/min
  scale_factor: 1
  add_offset: 0
  new_unit: "ms$^{-1}$"
  obs_file: "U_ERA5_monthly_climo_197901-202112.nc"
  obs_name: "ERA5"
  obs_var_name: "U"
  vector_pair: "V"
  vector_name: "Wind"
  category: "State"

Surface_Wind_Stress:
  nickname: "Surface Wind Stress"
  new_unit: "N m$^{-2}$"
  category: "Surface variables"

TAUX:
  new_unit: "N m$^{-2}$"
  vector_pair: "TAUY"
  vector_name: "Surface_Wind_Stress"
  category: "Surface variables"
  scale_factor: -1
  add_offset: 0

TAUY:
  new_unit: "N m$^{-2}$"
  vector_pair: "TAUX"
  vector_name: "Surface_Wind_Stress"
  category: "Surface variables"
  scale_factor: -1
  add_offset: 0

closes #452
closes #425
closes #373

justin-richling and others added 30 commits November 3, 2022 08:25
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
@justin-richling justin-richling added enhancement New feature or request code clean-up Made code simpler and/or easier to read. plotting Related to plot generation config files Concerns the ADF config (YAML) files labels May 14, 2026
@justin-richling justin-richling marked this pull request as draft May 14, 2026 20:45
@justin-richling justin-richling changed the base branch from dev to main May 14, 2026 20:46
@justin-richling justin-richling marked this pull request as ready for review May 14, 2026 20:46

@nusbaume nusbaume left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Comment thread .gitignore
!.vscode/extensions.json
.history

#NCL RGB files

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
#NCL RGB files
# NCL RGB files

Comment on lines +214 to +215
vres["season"] = plot['season']
vres["hemi"] = plot['hemi']

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Suggested change
vres["season"] = plot['season']
vres["hemi"] = plot['hemi']
vres['season'] = plot['season']
vres['hemi'] = plot['hemi']

Comment on lines +430 to +437
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]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Comment on lines +470 to +475
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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.

Comment thread lib/plotting_functions.py

fig, ax = plt.subplots(nrows=3)
ax = [ax[0],ax[1],ax[2]]
fig, ax = plt.subplots(nrows=3)#figsize=(6,8),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove commented-out code?

Comment thread lib/plotting_functions.py
Comment on lines +991 to +995
plot_kwargs = kwargs.copy()
plot_kwargs["colormap_2d"] = use_cmap
diff_kwargs = plot_kwargs.copy()
diff_kwargs["type"] = "diff"
diff_kwargs.pop("norm", None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/plotting_functions.py
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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meridional plot?

Suggested change
# Generate zonal plot:
# Generate meridional plot:

Comment thread lib/plotting_functions.py
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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/plotting_functions.py
Comment on lines +1333 to +1338
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get away with only one copy of kwargs here as well?

Comment thread lib/plotting_functions.py
height="90%", # height : 90%
loc='lower left',
bbox_to_anchor=(1.05, 0.05, 1, 1),
bbox_transform=ax2.transAxes,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this ax2 needs to be pointed at an axs element

Comment thread lib/plotting_functions.py
height="90%", # height : 90%
loc='lower left',
bbox_to_anchor=(1.05, 0.05, 1, 1),
bbox_transform=ax3.transAxes,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ax3 should be in axs

Comment thread lib/plotting_functions.py
height="90%", # height : 90%
loc='lower left',
bbox_to_anchor=(1.05, 0.05, 1, 1),
bbox_transform=ax4.transAxes,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ax4 should be in axs

Comment thread lib/plotting_functions.py
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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is range(0,90,10)going to work for southern hemisphere?

Comment thread lib/plotting_functions.py
loc='lower left',
bbox_to_anchor=(1.05, 0, 1, 1),
bbox_to_anchor=(1.02, 0, 1, 1),
bbox_transform=ax2.transAxes,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should ax2 be axs[...]?

Comment thread lib/plotting_functions.py
height="100%",
loc='lower left',
bbox_to_anchor=(1.02, 0, 1, 1),
bbox_transform=ax3.transAxes,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ax3 maybe axs[...]

Comment thread lib/plotting_utils.py
msg += f" Trying if this an NCL color map"

url = guess_ncl_url(cmap_pctdiff)
locfil = "." / f"{cmap_pctdiff}.rgb"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you have to use some pathlib thing here, probably: locfil = Path.cwd() / f"{cmap_pctdiff}.rgb"

Comment thread lib/plotting_functions.py
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread lib/plotting_functions.py
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')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 put pct_diff_colormap inside that block
    which means that the previous behavior of top-level placement is silently ignored.
  • If plot_type is 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 in lib/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.

Comment thread lib/plotting_utils.py
vmax = kwargs.get("vmax", None)
if use_cmap:
if kwargs["type"] == "pctdiff":
cmap = kwargs.get("pct_diff_colormap", "PuOr")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hard-coded yaml default was PuOr_r right? Should that be used here?

@brianpm brianpm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code clean-up Made code simpler and/or easier to read. config files Concerns the ADF config (YAML) files enhancement New feature or request plotting Related to plot generation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants