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.')
    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()