Source code for ncvue.ncvcontour

#!/usr/bin/env python
"""
Contour panel of ncvue.

The panel allows plotting contour or mesh plots of 2D-variables.

This module was written by Matthias Cuntz while at Institut National de
Recherche pour l'Agriculture, l'Alimentation et l'Environnement (INRAE), Nancy,
France.

Copyright (c) 2020-2021 Matthias Cuntz - mc (at) macu (dot) de

Released under the MIT License; see LICENSE file for details.

History:

* Written Nov-Dec 2020 by Matthias Cuntz (mc (at) macu (dot) de)
* Open new netcdf file, communicate via top widget, Jan 2021, Matthias Cuntz

.. moduleauthor:: Matthias Cuntz

The following classes are provided:

.. autosummary::
   ncvContour
"""
from __future__ import absolute_import, division, print_function
import sys
import tkinter as tk
try:
    import tkinter.ttk as ttk
except Exception:
    print('Using the themed widget set introduced in Tk 8.5.')
    print('Try to use mcview.py, which uses wxpython instead.')
    sys.exit()
from tkinter import filedialog
import os
import numpy as np
import netCDF4 as nc
from .ncvutils   import clone_ncvmain, set_axis_label, vardim2var
from .ncvmethods import analyse_netcdf, get_slice_miss
from .ncvmethods import set_dim_x, set_dim_y, set_dim_z
from .ncvwidgets import add_checkbutton, add_combobox, add_entry, add_imagemenu
from .ncvwidgets import add_spinbox, add_tooltip
import matplotlib
matplotlib.use('TkAgg')
from matplotlib import pyplot as plt
plt.style.use('seaborn-darkgrid')


__all__ = ['ncvContour']


[docs]class ncvContour(ttk.Frame): """ Panel for contour plots. Sets up the layout with the figure canvas, variable selectors, dimension spinboxes, and options in __init__. Contains various commands that manage what will be drawn or redrawn if something is selected, changed, checked, etc. """ # # Panel setup # def __init__(self, master, **kwargs): from functools import partial from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk from matplotlib.figure import Figure super().__init__(master, **kwargs) self.name = 'Contour' self.master = master self.top = master.top # copy for ease of use self.fi = self.top.fi self.miss = self.top.miss self.dunlim = self.top.dunlim self.time = self.top.time self.tname = self.top.tname self.tvar = self.top.tvar self.dtime = self.top.dtime self.latvar = self.top.latvar self.lonvar = self.top.lonvar self.latdim = self.top.latdim self.londim = self.top.londim self.maxdim = self.top.maxdim self.cols = self.top.cols # new window self.rowwin = ttk.Frame(self) self.rowwin.pack(side=tk.TOP, fill=tk.X) self.newfile = ttk.Button(self.rowwin, text="Open File", command=self.newnetcdf) self.newfile.pack(side=tk.LEFT) self.newfiletip = add_tooltip(self.newfile, 'Open a new netcdf file') self.newwin = ttk.Button( self.rowwin, text="New Window", command=partial(clone_ncvmain, self.master)) self.newwin.pack(side=tk.RIGHT) self.newwintip = add_tooltip( self.newwin, 'Open secondary ncvue window') # plotting canvas self.figure = Figure(facecolor="white", figsize=(1, 1)) self.axes = self.figure.add_subplot(111) self.canvas = FigureCanvasTkAgg(self.figure, master=self) self.canvas.draw() # pack self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) # grid instead of pack - does not work # self.canvas.get_tk_widget().grid(column=0, row=0, # sticky=(tk.N, tk.S, tk.E, tk.W)) # self.canvas.get_tk_widget().columnconfigure(0, weight=1) # self.canvas.get_tk_widget().rowconfigure(0, weight=1) # matplotlib toolbar self.toolbar = NavigationToolbar2Tk(self.canvas, self) self.toolbar.update() self.toolbar.pack(side=tk.TOP, fill=tk.X) # selections and options columns = [''] + self.cols allcmaps = plt.colormaps() self.cmaps = [ i for i in allcmaps if not i.endswith('_r') ] self.cmaps.sort() # self.imaps = [ tk.PhotoImage(file=os.path.dirname(__file__) + # '/images/' + i + '.png') # for i in self.cmaps ] bundle_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__))) self.imaps = [ tk.PhotoImage(file=bundle_dir + '/images/' + i + '.png') for i in self.cmaps ] # 1. row # z-axis selection self.rowzz = ttk.Frame(self) self.rowzz.pack(side=tk.TOP, fill=tk.X) self.blockz = ttk.Frame(self.rowzz) self.blockz.pack(side=tk.LEFT) self.rowz = ttk.Frame(self.blockz) self.rowz.pack(side=tk.TOP, fill=tk.X) self.zlbl = tk.StringVar() self.zlbl.set("z") zlab = ttk.Label(self.rowz, textvariable=self.zlbl) zlab.pack(side=tk.LEFT) self.bprev_z = ttk.Button(self.rowz, text="<", width=1, command=self.prev_z) self.bprev_z.pack(side=tk.LEFT) self.bprev_ztip = add_tooltip(self.bprev_z, 'Previous variable') self.bnext_z = ttk.Button(self.rowz, text=">", width=1, command=self.next_z) self.bnext_z.pack(side=tk.LEFT) self.bnext_ztip = add_tooltip(self.bnext_z, 'Next variable') self.z = ttk.Combobox(self.rowz, values=columns, width=25) self.z.bind("<<ComboboxSelected>>", self.selected_z) self.z.pack(side=tk.LEFT) self.ztip = add_tooltip(self.z, 'Choose variable') self.trans_zlbl, self.trans_z, self.trans_ztip = add_checkbutton( self.rowz, label="transpose z", value=False, command=self.checked, tooltip="Transpose matrix") spacez = ttk.Label(self.rowz, text=" "*1) spacez.pack(side=tk.LEFT) self.zminlbl, self.zmin, self.zmintip = add_entry( self.rowz, label="zmin", text='None', width=7, command=self.entered_z, tooltip="Minimal display value. Free scaling if 'None'.") self.zmaxlbl, self.zmax, self.zmaxtip = add_entry( self.rowz, label="zmax", text='None', width=7, command=self.entered_z, tooltip="Maximal display value. Free scaling if 'None'.") # levels z self.rowzd = ttk.Frame(self.blockz) self.rowzd.pack(side=tk.TOP, fill=tk.X) self.zdlblval = [] self.zdlbl = [] self.zdval = [] self.zd = [] self.zdtip = [] for i in range(self.maxdim): zdlblval, zdlbl, zdval, zd, zdtip = add_spinbox( self.rowzd, label=str(i), values=(0,), wrap=True, command=self.spinned_z, state=tk.DISABLED, tooltip="None") self.zdlblval.append(zdlblval) self.zdlbl.append(zdlbl) self.zdval.append(zdval) self.zd.append(zd) self.zdtip.append(zdtip) # 2. row # x-axis selection self.rowxy = ttk.Frame(self) self.rowxy.pack(side=tk.TOP, fill=tk.X) self.blockx = ttk.Frame(self.rowxy) self.blockx.pack(side=tk.LEFT) self.rowx = ttk.Frame(self.blockx) self.rowx.pack(side=tk.TOP, fill=tk.X) self.xlbl, self.x, self.xtip = add_combobox( self.rowx, label="x", values=columns, command=self.selected_x, tooltip="Choose variable of x-axis.\nTake index if 'None' (fast).") self.inv_xlbl, self.inv_x, self.inv_xtip = add_checkbutton( self.rowx, label="invert x", value=False, command=self.checked, tooltip="Invert x-axis") self.rowxd = ttk.Frame(self.blockx) self.rowxd.pack(side=tk.TOP, fill=tk.X) self.xdlblval = [] self.xdlbl = [] self.xdval = [] self.xd = [] self.xdtip = [] for i in range(self.maxdim): xdlblval, xdlbl, xdval, xd, xdtip = add_spinbox( self.rowxd, label=str(i), values=(0,), wrap=True, command=self.spinned_x, state=tk.DISABLED, tooltip="None") self.xdlblval.append(xdlblval) self.xdlbl.append(xdlbl) self.xdval.append(xdval) self.xd.append(xd) self.xdtip.append(xdtip) # y-axis selection spacex = ttk.Label(self.rowxy, text=" "*3) spacex.pack(side=tk.LEFT) self.blocky = ttk.Frame(self.rowxy) self.blocky.pack(side=tk.LEFT) self.rowy = ttk.Frame(self.blocky) self.rowy.pack(side=tk.TOP, fill=tk.X) self.ylbl, self.y, self.ytip = add_combobox( self.rowy, label="y", values=columns, command=self.selected_y, tooltip="Choose variable of y-axis.\nTake index if 'None'.") self.inv_ylbl, self.inv_y, self.inv_ytip = add_checkbutton( self.rowy, label="invert y", value=False, command=self.checked, tooltip="Invert y-axis") self.rowyd = ttk.Frame(self.blocky) self.rowyd.pack(side=tk.TOP, fill=tk.X) self.ydlblval = [] self.ydlbl = [] self.ydval = [] self.yd = [] self.ydtip = [] for i in range(self.maxdim): ydlblval, ydlbl, ydval, yd, ydtip = add_spinbox( self.rowyd, label=str(i), values=(0,), wrap=True, command=self.spinned_y, state=tk.DISABLED, tooltip="None") self.ydlblval.append(ydlblval) self.ydlbl.append(ydlbl) self.ydval.append(ydval) self.yd.append(yd) self.ydtip.append(ydtip) # 3. row # options self.rowcmap = ttk.Frame(self) self.rowcmap.pack(side=tk.TOP, fill=tk.X) self.cmaplbl, self.cmap, self.cmaptip = add_imagemenu( self.rowcmap, label="cmap", values=self.cmaps, images=self.imaps, command=self.selected_cmap, tooltip="Choose colormap") self.cmap['text'] = 'RdYlBu' self.cmap['image'] = self.imaps[self.cmaps.index('RdYlBu')] self.rev_cmaplbl, self.rev_cmap, self.rev_cmaptip = add_checkbutton( self.rowcmap, label="reverse cmap", value=False, command=self.checked, tooltip="Reverse colormap") self.meshlbl, self.mesh, self.meshtip = add_checkbutton( self.rowcmap, label="mesh", value=True, command=self.checked, tooltip="Pseudocolor plot if checked, plot contours if unchecked") self.gridlbl, self.grid, self.gridtip = add_checkbutton( self.rowcmap, label="grid", value=False, command=self.checked, tooltip="Draw major grid lines") # # Bindings #
[docs] def checked(self): """ Command called if any checkbutton was checked or unchecked. Redraws plot. """ self.redraw()
[docs] def entered_z(self, event): """ Command called if values for `zmin`/`zmax` were entered. Triggering `event` was bound to entry. Redraws plot. """ self.redraw()
[docs] def next_z(self): """ Command called if next button for the plotting variable was pressed. Resets `zmin`/`zmax` and z-dimensions, resets `x` and `y` variables as well as their options and dimensions. Redraws plot. """ z = self.z.get() cols = self.z["values"] idx = cols.index(z) idx += 1 if idx < len(cols): self.z.set(cols[idx]) self.zmin.set('None') self.zmax.set('None') set_dim_z(self) self.x.set('') self.y.set('') self.inv_x.set(0) self.inv_y.set(0) set_dim_x(self) set_dim_y(self) self.redraw()
[docs] def prev_z(self): """ Command called if previous button for the plotting variable was pressed. Resets `zmin`/`zmax` and z-dimensions, resets `x` and `y` variables as well as their options and dimensions. Redraws plot. """ z = self.z.get() cols = self.z["values"] idx = cols.index(z) idx -= 1 if idx > 0: self.z.set(cols[idx]) self.zmin.set('None') self.zmax.set('None') set_dim_z(self) self.x.set('') self.y.set('') self.inv_x.set(0) self.inv_y.set(0) set_dim_x(self) set_dim_y(self) self.redraw()
[docs] def newnetcdf(self): """ Open a new netcdf file and connect it to top. """ # get new netcdf file name ncfile = filedialog.askopenfilename( parent=self, title='Choose netcdf file', multiple=False) if ncfile: # close old netcdf file if self.top.fi: self.top.fi.close() # reset empty defaults of top self.top.dunlim = '' # name of unlimited dimension self.top.time = None # datetime variable self.top.tname = '' # datetime variable name self.top.tvar = '' # datetime variable name in netcdf self.top.dtime = None # decimal year self.top.latvar = '' # name of latitude variable self.top.lonvar = '' # name of longitude variable self.top.latdim = '' # name of latitude dimension self.top.londim = '' # name of longitude dimension self.top.maxdim = 0 # maximum num of dims of all variables self.top.cols = [] # variable list # open new netcdf file self.top.fi = nc.Dataset(ncfile, 'r') analyse_netcdf(self.top) # reset panel self.reinit() self.redraw()
[docs] def selected_cmap(self, value): """ Command called if cmap was chosen from menu. `value` is the chosen colormap. Sets text and image on the menubutton. """ self.cmap['text'] = value self.cmap['image'] = self.imaps[self.cmaps.index(value)] self.redraw()
[docs] def selected_x(self, event): """ Command called if x-variable was selected with combobox. Triggering `event` was bound to the combobox. Resets `x` options and dimensions. Redraws plot. """ self.inv_x.set(0) set_dim_x(self) self.redraw()
[docs] def selected_y(self, event): """ Command called if y-variable was selected with combobox. Triggering `event` was bound to the combobox. Resets `y` options and dimensions. Redraws plot. """ self.inv_y.set(0) set_dim_y(self) self.redraw()
[docs] def selected_z(self, event): """ Command called if plotting variable was selected with combobox. Triggering `event` was bound to the combobox. Resets `zmin`/`zmax` and z-dimensions, resets `x` and `y` variables as well as their options and dimensions. Redraws plot. """ self.x.set('') self.y.set('') self.inv_x.set(0) self.inv_y.set(0) self.zmin.set('None') self.zmax.set('None') set_dim_x(self) set_dim_y(self) set_dim_z(self) self.redraw()
[docs] def spinned_x(self, event=None): """ Command called if spinbox of x-dimensions was changed. Triggering `event` was bound to the spinbox. Redraws plot. """ self.redraw()
[docs] def spinned_y(self, event=None): """ Command called if spinbox of y-dimensions was changed. Triggering `event` was bound to the spinbox. Redraws plot. """ self.redraw()
[docs] def spinned_z(self, event=None): """ Command called if spinbox of z-dimensions was changed. Triggering `event` was bound to the spinbox. Redraws plot. """ self.redraw()
# # Methods #
[docs] def reinit(self): """ Reinitialise the panel from top. """ # reinit from top self.fi = self.top.fi self.miss = self.top.miss self.dunlim = self.top.dunlim self.time = self.top.time self.tname = self.top.tname self.tvar = self.top.tvar self.dtime = self.top.dtime self.latvar = self.top.latvar self.lonvar = self.top.lonvar self.latdim = self.top.latdim self.londim = self.top.londim self.maxdim = self.top.maxdim self.cols = self.top.cols # reset dimensions for ll in self.zdlbl: ll.destroy() for ll in self.zd: ll.destroy() self.zdlblval = [] self.zdlbl = [] self.zdval = [] self.zd = [] self.zdtip = [] for i in range(self.maxdim): zdlblval, zdlbl, zdval, zd, zdtip = add_spinbox( self.rowzd, label=str(i), values=(0,), wrap=True, command=self.spinned_z, state=tk.DISABLED, tooltip="None") self.zdlblval.append(zdlblval) self.zdlbl.append(zdlbl) self.zdval.append(zdval) self.zd.append(zd) self.zdtip.append(zdtip) for ll in self.xdlbl: ll.destroy() for ll in self.xd: ll.destroy() self.xdlblval = [] self.xdlbl = [] self.xdval = [] self.xd = [] self.xdtip = [] for i in range(self.maxdim): xdlblval, xdlbl, xdval, xd, xdtip = add_spinbox( self.rowxd, label=str(i), values=(0,), wrap=True, command=self.spinned_x, state=tk.DISABLED, tooltip="None") self.xdlblval.append(xdlblval) self.xdlbl.append(xdlbl) self.xdval.append(xdval) self.xd.append(xd) self.xdtip.append(xdtip) for ll in self.ydlbl: ll.destroy() for ll in self.yd: ll.destroy() self.ydlblval = [] self.ydlbl = [] self.ydval = [] self.yd = [] self.ydtip = [] for i in range(self.maxdim): ydlblval, ydlbl, ydval, yd, ydtip = add_spinbox( self.rowyd, label=str(i), values=(0,), wrap=True, command=self.spinned_y, state=tk.DISABLED, tooltip="None") self.ydlblval.append(ydlblval) self.ydlbl.append(ydlbl) self.ydval.append(ydval) self.yd.append(yd) self.ydtip.append(ydtip) # set variables columns = [''] + self.cols self.z['values'] = columns self.z.set(columns[0]) self.zmin.set('None') self.zmax.set('None') self.x['values'] = columns self.x.set(columns[0]) self.y['values'] = columns self.y.set(columns[0])
# # Plotting #
[docs] def redraw(self): """ Redraws the plot. Reads `x`, `y`, `z` variable names, the current settings of their dimension spinboxes, as well as all other plotting options. Then redraws the plot. """ # get all states # rowz z = self.z.get() trans_z = self.trans_z.get() zmin = self.zmin.get() if zmin == 'None': zmin = None else: zmin = float(zmin) zmax = self.zmax.get() if zmax == 'None': zmax = None else: zmax = float(zmax) # rowxy x = self.x.get() y = self.y.get() inv_x = self.inv_x.get() inv_y = self.inv_y.get() # rowcmap cmap = self.cmap['text'] rev_cmap = self.rev_cmap.get() mesh = self.mesh.get() grid = self.grid.get() # Clear figure instead of axes because colorbar is on figure # Have to add axes again. self.figure.clear() self.axes = self.figure.add_subplot(111) xlim = [None, None] ylim = [None, None] # set x, y, axes labels vx = 'None' vy = 'None' vz = 'None' if (z != ''): # z axis vz = vardim2var(z) if vz == self.tname: # should throw an error later if mesh: zz = self.dtime zlab = 'Year' else: zz = self.time zlab = 'Date' else: zz = self.fi.variables[vz] zlab = set_axis_label(zz) zz = get_slice_miss(self, self.zd, zz) # both contourf and pcolormesh assume (row,col), # so transpose by default if not trans_z: zz = zz.T if (y != ''): # y axis vy = vardim2var(y) if vy == self.tname: if mesh: yy = self.dtime ylab = 'Year' else: yy = self.time ylab = 'Date' else: yy = self.fi.variables[vy] ylab = set_axis_label(yy) yy = get_slice_miss(self, self.yd, yy) if (x != ''): # x axis vx = vardim2var(x) if vx == self.tname: if mesh: xx = self.dtime xlab = 'Year' else: xx = self.time xlab = 'Date' else: xx = self.fi.variables[vx] xlab = set_axis_label(xx) xx = get_slice_miss(self, self.xd, xx) # set z to nan if not selected if (z == ''): if (x != ''): nx = xx.shape[0] else: nx = 1 if (y != ''): ny = yy.shape[0] else: ny = 1 zz = np.ones((ny, nx)) * np.nan zlab = '' if zz.ndim < 2: estr = 'Contour: z (' + vz + ') is not 2-dimensional:' print(estr, zz.shape) return # set x and y to index if not selected if (x == ''): nx = zz.shape[1] xx = np.arange(nx) xlab = '' if (y == ''): ny = zz.shape[0] yy = np.arange(ny) ylab = '' # plot options if rev_cmap: cmap = cmap + '_r' # plot # cc = self.axes.imshow(zz[:, ::-1], aspect='auto', cmap=cmap, # interpolation='none') # cc = self.axes.matshow(zz[:, ::-1], aspect='auto', cmap=cmap, # interpolation='none') extend = 'neither' if zmin is not None: zz = np.maximum(zz, zmin) if zmax is None: extend = 'min' else: extend = 'both' if zmax is not None: zz = np.minimum(zz, zmax) if zmin is None: extend = 'max' else: extend = 'both' if mesh: try: # zz is matrix notation: (row, col) cc = self.axes.pcolormesh(xx, yy, zz, vmin=zmin, vmax=zmax, cmap=cmap, shading='nearest') cb = self.figure.colorbar(cc, fraction=0.05, shrink=0.75, extend=extend) except Exception: estr = 'Contour: x (' + vx + '), y (' + vy + '),' estr += ' z (' + vz + ') shapes do not match for' estr += ' pcolormesh:' print(estr, xx.shape, yy.shape, zz.shape) return else: try: # if 1-D then len(x)==m (columns) and len(y)==n (rows): z(n,m) cc = self.axes.contourf(xx, yy, zz, vmin=zmin, vmax=zmax, cmap=cmap, extend=extend) cb = self.figure.colorbar(cc, fraction=0.05, shrink=0.75) except Exception: estr = 'Contour: x (' + vx + '), y (' + vy + '),' estr += ' z (' + vz + ') shapes do not match for' estr += ' contourf:' print(estr, xx.shape, yy.shape, zz.shape) return # help(self.figure) cb.set_label(zlab) self.axes.xaxis.set_label_text(xlab) self.axes.yaxis.set_label_text(ylab) # # Does not work # # might do it by hand, i.e. get ticks and use axhline and axvline # self.axes.grid(True, lw=5, color='k', zorder=100) # self.axes.set_zorder(100) # self.axes.xaxis.grid(True, zorder=999) # self.axes.yaxis.grid(True, zorder=999) xlim = self.axes.get_xlim() ylim = self.axes.get_ylim() # invert axes if inv_x: if (xlim[0] is not None): xlim = xlim[::-1] self.axes.set_xlim(xlim) if inv_y: if (ylim[0] is not None): ylim = ylim[::-1] self.axes.set_ylim(ylim) # draw grid lines xticks = np.array(self.axes.get_xticks()) yticks = np.array(self.axes.get_yticks()) if grid: ii = np.where((xticks > min(xlim)) & (xticks < max(xlim)))[0] if ii.size > 0: ggx = self.axes.vlines(xticks[ii], ylim[0], ylim[1], colors='w', linestyles='solid', linewidth=0.5) ii = np.where((yticks > min(ylim)) & (yticks < max(ylim)))[0] if ii.size > 0: ggy = self.axes.hlines(yticks[ii], xlim[0], xlim[1], colors='w', linestyles='solid', linewidth=0.5) # redraw self.canvas.draw() self.toolbar.update()