#!/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()