From ac7261469699fbccd265f303fc9e1dcdc47514d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 9 Jan 2018 17:21:25 -0500 Subject: [PATCH 01/29] :recycle: Renamed WeatherAvgGraph to WeatherViewer --- gwhat/HydroPrint2.py | 4 ++-- gwhat/meteo/weather_viewer.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gwhat/HydroPrint2.py b/gwhat/HydroPrint2.py index b903a4d8e..5e218dfe6 100644 --- a/gwhat/HydroPrint2.py +++ b/gwhat/HydroPrint2.py @@ -30,7 +30,7 @@ import gwhat.hydrograph4 as hydrograph import gwhat.mplFigViewer3 as mplFigViewer -from gwhat.meteo.weather_viewer import WeatherAvgGraph +from gwhat.meteo.weather_viewer import WeatherViewer from gwhat.colors2 import ColorsReader, ColorsSetupWin from gwhat.common import QToolButtonNormal, QToolButtonSmall @@ -54,7 +54,7 @@ def __init__(self, datamanager, parent=None): self.dmngr.wldsetChanged.connect(self.wldset_changed) self.dmngr.wxdsetChanged.connect(self.wxdset_changed) - self.weather_avg_graph = WeatherAvgGraph(self) + self.weather_avg_graph = WeatherViewer(self) self.page_setup_win = PageSetupWin(self) self.page_setup_win.newPageSetupSent.connect(self.layout_changed) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 6fe2be421..85e9b52b7 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -79,13 +79,13 @@ def __init__(self, language): "JUL", u"AOÛ", "SEP", "OCT", "NOV", u"DÉC"] -class WeatherAvgGraph(DialogWindow): +class WeatherViewer(DialogWindow): """ GUI that allows to plot weather normals, save the graphs to file, see various stats about the dataset, etc... """ def __init__(self, parent=None): - super(WeatherAvgGraph, self).__init__(parent) + super(WeatherViewer, self).__init__(parent) self.wxdset = None From eed2bce9b2967566994a5d7c4520d29462dd1148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 9 Jan 2018 17:23:08 -0500 Subject: [PATCH 02/29] :art: Code style changes --- gwhat/meteo/weather_viewer.py | 15 ++++++--------- gwhat/widgets/buttons.py | 5 ++--- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 85e9b52b7..a8a83ace2 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -95,16 +95,14 @@ def __init__(self, parent=None): self.__initUI__() - # ========================================================================= - def __initUI__(self): self.setWindowTitle('Weather Averages') self.setWindowIcon(icons.get_icon('master')) - # ---------------------------------------------------- TOOLBAR ---- + # ---- TOOLBAR - # Widgets : + # Initialize the widgets : menu_save = QMenu() menu_save.addAction('Save normals graph as...', self.save_graph) @@ -154,16 +152,15 @@ def __initUI__(self): subgrid_toolbar.setContentsMargins(0, 0, 0, 0) toolbar_widget.setLayout(subgrid_toolbar) + # ---- MAIN GRID - # -------------------------------------------------- MAIN GRID ---- - - # ---- widgets ---- + # Initialize the widgets : self.fig_weather_normals = FigWeatherNormals() self.grid_weather_normals = GridWeatherNormals() self.grid_weather_normals.hide() - # ---- layout ---- + # Generate the layout : mainGrid = QGridLayout() @@ -970,7 +967,7 @@ def populate_table(self, NORMALS): "FARNHAM (7022320)_2005-2010.out") wxdset = WXDataFrame(fmeteo) - w = WeatherAvgGraph() + w = WeatherViewer() w.save_fig_dir = os.getcwd() w.set_lang('English') diff --git a/gwhat/widgets/buttons.py b/gwhat/widgets/buttons.py index 8924195c9..5598c715f 100644 --- a/gwhat/widgets/buttons.py +++ b/gwhat/widgets/buttons.py @@ -6,11 +6,11 @@ # This file is part of GWHAT (Ground-Water Hydrograph Analysis Toolbox). # Licensed under the terms of the GNU General Public License. -# ---- Imports: standard libraries +# ---- Imports: Standard Libraries import sys -# ---- Imports: third parties +# ---- Imports: Third Parties from PyQt5.QtCore import pyqtSignal as QSignal from PyQt5.QtCore import QSize, Qt @@ -102,7 +102,6 @@ def focusOutEvent(self, event): self.hide() -if __name__ == '__main__': # pragma: no cover import sys app = QApplication(sys.argv) From 578364fd5a9c19e8cb7f8beee8ce3b874dbf611e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 9 Jan 2018 17:23:48 -0500 Subject: [PATCH 03/29] Added a new RangeSpinBoxes class --- gwhat/widgets/buttons.py | 74 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/gwhat/widgets/buttons.py b/gwhat/widgets/buttons.py index 5598c715f..e4f77892f 100644 --- a/gwhat/widgets/buttons.py +++ b/gwhat/widgets/buttons.py @@ -13,8 +13,79 @@ # ---- Imports: Third Parties from PyQt5.QtCore import pyqtSignal as QSignal +from PyQt5.QtCore import pyqtSlot as QSlot from PyQt5.QtCore import QSize, Qt -from PyQt5.QtWidgets import QApplication, QToolButton, QListWidget, QStyle +from PyQt5.QtWidgets import (QApplication, QToolButton, QListWidget, QStyle, + QSpinBox, QWidget, QAbstractSpinBox) + + +class RangeSpinBoxes(QWidget): + """ + Consists of two QSpinBox that are linked togheter so that one represent a + lower boundary and the other one the upper boundary of a range. + """ + sig_range_changed = QSignal(tuple) + + def __init__(self, min_value=0, max_value=0, orientation=Qt.Horizontal, + parent=None): + super(RangeSpinBoxes, self).__init__(parent) + self.spb_lower = QSpinBox() + self.spb_lower.setButtonSymbols(QAbstractSpinBox.NoButtons) + self.spb_lower.setAlignment(Qt.AlignCenter) + self.spb_lower.setRange(0, 9999) + self.spb_lower.setKeyboardTracking(False) + + self.spb_upper = QSpinBox() + self.spb_upper.setButtonSymbols(QAbstractSpinBox.NoButtons) + self.spb_upper.setAlignment(Qt.AlignCenter) + self.spb_upper.setRange(0, 9999) + self.spb_upper.setKeyboardTracking(False) + + self.spb_lower.valueChanged.connect(self.constain_bounds_to_minmax) + self.spb_upper.valueChanged.connect(self.constain_bounds_to_minmax) + + self.set_range(1000, 9999) + + @property + def lower_bound(self): + return self.spb_lower.value() + + @property + def upper_bound(self): + return self.spb_upper.value() + + def set_range(self, min_value, max_value): + """Set the min max values of the range for both spin boxes.""" + self.min_value = min_value + self.max_value = max_value + self.spb_lower.setValue(min_value) + self.spb_upper.setValue(max_value) + + @QSlot(int) + def constain_bounds_to_minmax(self, new_value): + """ + Makes sure that the new value is within the min and max values that + were set for the range. Also makes sure that the + """ + is_range_changed = False + corr_value = min(max(new_value, self.min_value), self.max_value) + if corr_value != new_value: + is_range_changed = True + self.sender().blockSignals(True) + self.sender().setValue(corr_value) + self.sender().blockSignals(False) + if corr_value > self.spb_upper.value(): + is_range_changed = True + self.spb_upper.blockSignals(True) + self.spb_upper.setValue(corr_value) + self.spb_upper.blockSignals(False) + if corr_value < self.spb_lower.value(): + is_range_changed = True + self.spb_lower.blockSignals(True) + self.spb_lower.setValue(corr_value) + self.spb_lower.blockSignals(False) + if is_range_changed is True: + self.sig_range_changed.emit((self.lower_bound, self.upper_bound)) class DropDownButton(QToolButton): @@ -102,6 +173,7 @@ def focusOutEvent(self, event): self.hide() +if __name__ == '__main__': import sys app = QApplication(sys.argv) From 5ca22c9d2e2e5278f5009a800b7ba9026c54b985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Tue, 9 Jan 2018 17:24:42 -0500 Subject: [PATCH 04/29] Added a year range widget to the weather viewer --- gwhat/meteo/weather_viewer.py | 37 ++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index a8a83ace2..081374584 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -14,6 +14,7 @@ import os import csv from time import strftime +from datetime import datetime # ---- Third party imports @@ -25,14 +26,15 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import (QMenu, QToolButton, QGridLayout, QWidget, QFileDialog, QApplication, QTableWidget, - QTableWidgetItem) + QTableWidgetItem, QLabel, QHBoxLayout) # ---- Local imports from gwhat.colors2 import ColorsReader from gwhat.common import StyleDB, QToolButtonNormal from gwhat.common import icons -from gwhat.common.widgets import DialogWindow +from gwhat.common.widgets import DialogWindow, VSep +from gwhat.widgets.buttons import RangeSpinBoxes from gwhat import __namever__ mpl.rc('font', **{'family': 'sans-serif', 'sans-serif': ['Arial']}) @@ -133,25 +135,32 @@ def __initUI__(self): btn_showStats.setToolTip('Show monthly weather normals data table.') btn_showStats.clicked.connect(self.show_monthly_grid) - # Layout : + # Instantiate and define a layout for the year range widget : + + self.year_rng = RangeSpinBoxes() + self.year_rng.set_range(1800, datetime.now().year) + + qgrid = QHBoxLayout(self.year_rng) + qgrid.setContentsMargins(0, 0, 0, 0) + qgrid.addWidget(QLabel('Year Range :')) + qgrid.addWidget(self.year_rng.spb_lower) + qgrid.addWidget(QLabel('to')) + qgrid.addWidget(self.year_rng.spb_upper) + + # Generate the layout of the toolbar : - subgrid_toolbar = QGridLayout() toolbar_widget = QWidget() + subgrid_toolbar = QGridLayout(toolbar_widget) - col = 0 - row = 0 - subgrid_toolbar.addWidget(btn_save, row, col) - col += 1 - subgrid_toolbar.addWidget(self.btn_export, row, col) - col += 1 - subgrid_toolbar.addWidget(btn_showStats, row, col) - col += 1 - subgrid_toolbar.setColumnStretch(col, 4) + buttons = [btn_save, self.btn_export, btn_showStats, VSep(), + self.year_rng] + for col, btn in enumerate(buttons): + subgrid_toolbar.addWidget(btn, 0, col) + subgrid_toolbar.setColumnStretch(subgrid_toolbar.columnCount(), 4) subgrid_toolbar.setSpacing(5) subgrid_toolbar.setContentsMargins(0, 0, 0, 0) - toolbar_widget.setLayout(subgrid_toolbar) # ---- MAIN GRID # Initialize the widgets : From 89424e7f6ae20a10365d8ae3f7b4434fda1030dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Wed, 10 Jan 2018 10:42:01 -0500 Subject: [PATCH 05/29] :book: Added some docstrings to functions --- gwhat/meteo/weather_reader.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/gwhat/meteo/weather_reader.py b/gwhat/meteo/weather_reader.py index 5e4c615f0..62e3ef4d8 100644 --- a/gwhat/meteo/weather_reader.py +++ b/gwhat/meteo/weather_reader.py @@ -518,10 +518,18 @@ def fill_nan(time, data, name='data', fill_mode='zeros'): # ----- Base functions: monthly downscaling def calc_monthly_sum(yy_dly, mm_dly, x_dly): + """ + Calcul monthly cumulative values from daily values, where yy_dly are the + years, mm_dly are the months (1 to 12), and x_dly are the daily values. + """ return calc_monthly(yy_dly, mm_dly, x_dly, np.sum) def calc_monthly_mean(yy_dly, mm_dly, x_dly): + """ + Calcul monthly mean values from daily values, where yy_dly are the + years, mm_dly are the months (1 to 12), and x_dly are the daily values. + """ return calc_monthly(yy_dly, mm_dly, x_dly, np.mean) @@ -558,10 +566,18 @@ def calcul_monthly_normals(mm_mly, x_mly): # ----- Base functions: yearly downscaling def calc_yearly_sum(yy_dly, x_dly): + """ + Calcul yearly cumulative values from daily values, where yy_dly are the + years and x_dly are the daily values. + """ return calc_yearly(yy_dly, x_dly, np.sum) def calc_yearly_mean(yy_dly, x_dly): + """ + Calcul yearly mean values from daily values, where yy_dly are the years + and x_dly are the daily values. + """ return calc_yearly(yy_dly, x_dly, np.mean) From fd34b4fda6ff09930313b2212f5dd95b3bd00cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Wed, 10 Jan 2018 11:15:28 -0500 Subject: [PATCH 06/29] :art: Some code style changes --- gwhat/meteo/weather_reader.py | 16 ++++++++-------- gwhat/meteo/weather_viewer.py | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/gwhat/meteo/weather_reader.py b/gwhat/meteo/weather_reader.py index 62e3ef4d8..7e55842d2 100644 --- a/gwhat/meteo/weather_reader.py +++ b/gwhat/meteo/weather_reader.py @@ -87,13 +87,13 @@ def __init__(self, filename, *args, **kwargs): 'Snow': None, 'PET': None} - # -------------------------------------------- Import primary data ---- + # ---- Import primary data data = read_weather_datafile(filename) for key in data.keys(): self[key] = data[key] - # ------------------------------------------------- Import missing ---- + # ---- Import missing finfo = filename[:-3] + 'log' if os.path.exists(finfo): @@ -102,7 +102,7 @@ def __init__(self, filename, *args, **kwargs): self['Missing Tavg'] = load_weather_log(finfo, 'Mean Temp (deg C)') self['Missing Ptot'] = load_weather_log(finfo, 'Total Precip (mm)') - # ---------------------------------------------------- format data ---- + # ---- format data print('Make daily time series continuous.') @@ -125,7 +125,7 @@ def __init__(self, filename, *args, **kwargs): for vbr in ['Ptot', 'Rain', 'Snow']: self[vbr] = fill_nan(self['Time'], self[vbr], vbr, 'zeros') - # ---------------------------------------------- monthly & normals ---- + # ---- Monthly & Normals # Temperature based variables: @@ -151,9 +151,9 @@ def __init__(self, filename, *args, **kwargs): self['normals']['Ptot'] = calcul_monthly_normals(x[1], x[2]) - # ------------------------------------------------- secondary vrbs ---- + # ---- Secondary Variables - # ---- Rain ---- + # Rain if self['Rain'] is None: self['Rain'] = calcul_rain_from_ptot( @@ -168,7 +168,7 @@ def __init__(self, filename, *args, **kwargs): self['normals']['Rain'] = calcul_monthly_normals(x[1], x[2]) - # ---- Snow ---- + # Snow if self['Snow'] is None: self['Snow'] = self['Ptot'] - self['Rain'] @@ -182,7 +182,7 @@ def __init__(self, filename, *args, **kwargs): self['normals']['Snow'] = calcul_monthly_normals(x[1], x[2]) - # ---- Potential Evapotranspiration ---- + # Potential Evapotranspiration if self['PET'] is None: dates = [self['Year'], self['Month'], self['Day']] diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 081374584..309dabb14 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -204,12 +204,11 @@ def show_monthly_grid(self): # ========================================================================= def set_lang(self, lang): + """Sets the language of all the labels in the figure.""" self.language = lang self.fig_weather_normals.set_lang(lang) self.fig_weather_normals.draw() - # ========================================================================= - def generate_graph(self, wxdset): self.wxdset = wxdset self.fig_weather_normals.plot_monthly_normals(wxdset['normals']) @@ -633,6 +632,7 @@ def plot_legend(self): # ========================================================= Plot data ===== def plot_monthly_normals(self, normals): + """Plot the normals on the figure.""" self.normals = normals @@ -960,7 +960,7 @@ def populate_table(self, NORMALS): self.resizeColumnsToContents() -# ---- if __name__ == '__main__' +# %% if __name__ == '__main__' if __name__ == '__main__': from gwhat.meteo.weather_reader import WXDataFrame From fff15a165d73d8b4eeec4e3ef6a486a75e5ae651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Wed, 10 Jan 2018 11:16:42 -0500 Subject: [PATCH 07/29] :recycle: Added year range for normals calc Added the possibility to define year limits when computing the normals from monthly values. --- gwhat/meteo/weather_reader.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/gwhat/meteo/weather_reader.py b/gwhat/meteo/weather_reader.py index 7e55842d2..eeb312727 100644 --- a/gwhat/meteo/weather_reader.py +++ b/gwhat/meteo/weather_reader.py @@ -136,7 +136,7 @@ def __init__(self, filename, *args, **kwargs): x = calc_monthly_mean(self['Year'], self['Month'], self[vrb]) self['monthly'][vrb] = x[2] - self['normals'][vrb] = calcul_monthly_normals(x[1], x[2]) + self['normals'][vrb] = calcul_monthly_normals(x[0], x[1], x[2]) # Precipitation : @@ -149,7 +149,7 @@ def __init__(self, filename, *args, **kwargs): self['monthly']['Year'] = x[0] self['monthly']['Month'] = x[1] - self['normals']['Ptot'] = calcul_monthly_normals(x[1], x[2]) + self['normals']['Ptot'] = calcul_monthly_normals(x[0], x[1], x[2]) # ---- Secondary Variables @@ -166,7 +166,7 @@ def __init__(self, filename, *args, **kwargs): x = calc_monthly_sum(self['Year'], self['Month'], self['Rain']) self['monthly']['Rain'] = x[2] - self['normals']['Rain'] = calcul_monthly_normals(x[1], x[2]) + self['normals']['Rain'] = calcul_monthly_normals(x[0], x[1], x[2]) # Snow @@ -180,7 +180,7 @@ def __init__(self, filename, *args, **kwargs): x = calc_monthly_sum(self['Year'], self['Month'], self['Snow']) self['monthly']['Snow'] = x[2] - self['normals']['Snow'] = calcul_monthly_normals(x[1], x[2]) + self['normals']['Snow'] = calcul_monthly_normals(x[0], x[1], x[2]) # Potential Evapotranspiration @@ -198,7 +198,7 @@ def __init__(self, filename, *args, **kwargs): x = calc_monthly_sum(self['Year'], self['Month'], self['PET']) self['monthly']['PET'] = x[2] - self['normals']['PET'] = calcul_monthly_normals(x[1], x[2]) + self['normals']['PET'] = calcul_monthly_normals(x[0], x[1], x[2]) print('-'*78) @@ -349,7 +349,7 @@ def add_PET_to_weather_datafile(filename): Tavg = data[:, vrbs.index('Mean Temp (deg C)')] x = calc_monthly_mean(Year, Month, Tavg) - Ta = calcul_monthly_normals(x[1], x[2]) + Ta = calcul_monthly_normals(x[0], x[1], x[2]) PET = calcul_Thornthwaite(Dates, Tavg, lat, Ta) @@ -551,10 +551,26 @@ def calc_monthly(yy_dly, mm_dly, x_dly, func): return yy_mly, mm_mly, x_mly -def calcul_monthly_normals(mm_mly, x_mly): +def calcul_monthly_normals(years, months, x_mly, yearmin=None, yearmax=None): + """Calcul the monthly normals from monthly values.""" + if len(years) != len(months) != len(x_mly): + raise ValueError("The dimension of the years, months, and x_mly array" + " must match exactly.") + if np.min(months) < 1 or np.max(months) > 12: + raise ValueError("Months values must be between 1 and 12.") + + # Mark as nan monthly values that are outside the year range that is + # defined by yearmin and yearmax : + x_mly = np.copy(x_mly) + if yearmin is not None: + x_mly[years < yearmin] = np.nan + if yearmax is not None: + x_mly[years > yearmax] = np.nan + + # Calcul the monthly normals : x_norm = np.zeros(12) for i, mm in enumerate(range(1, 13)): - indx = np.where((mm_mly == mm) & (~np.isnan(x_mly)))[0] + indx = np.where((months == mm) & (~np.isnan(x_mly)))[0] if len(indx) > 0: x_norm[i] = np.mean(x_mly[indx]) else: From b7914bd715e9a03af262a1922750ddcb1ec9e87d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Wed, 10 Jan 2018 22:22:03 -0500 Subject: [PATCH 08/29] Removed annoying prints --- gwhat/colors2.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/gwhat/colors2.py b/gwhat/colors2.py index 0c3c217e4..ea22ab63b 100644 --- a/gwhat/colors2.py +++ b/gwhat/colors2.py @@ -64,13 +64,11 @@ def load_colors_db(self): self.save_colors_db() else: - print('Loading the color settings...', end=" ") with open(fname, 'r') as f: reader = list(csv.reader(f, delimiter=',')) for row in reader: self.RGB[row[0]] = [int(x) for x in row[1:]] - print('done') def save_colors_db(self): """Save the color settings to Colors.db.""" @@ -80,7 +78,6 @@ def save_colors_db(self): fcontent.append([key]) fcontent[-1].extend(self.RGB[key]) save_content_to_csv(fname, fcontent) - print('Color settings saved successfully.') class ColorsSetupWin(myqt.DialogWindow): From e891eb361d5ea95af91152e346272aaa9eb5171f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Wed, 10 Jan 2018 22:24:17 -0500 Subject: [PATCH 09/29] :art: Code style changes --- gwhat/meteo/weather_viewer.py | 76 ++++++++++------------------------- 1 file changed, 22 insertions(+), 54 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 309dabb14..bd0771632 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -8,7 +8,7 @@ from __future__ import division, unicode_literals -# ---- Standard library imports +# ---- Imports: Standard Libraries import sys import os @@ -16,7 +16,7 @@ from time import strftime from datetime import datetime -# ---- Third party imports +# ---- Imports: Third Parties import xlsxwriter import numpy as np @@ -28,7 +28,7 @@ QFileDialog, QApplication, QTableWidget, QTableWidgetItem, QLabel, QHBoxLayout) -# ---- Local imports +# ---- Imports: Local from gwhat.colors2 import ColorsReader from gwhat.common import StyleDB, QToolButtonNormal @@ -201,8 +201,6 @@ def show_monthly_grid(self): # self.setFixedWidth(self.size().width()-75) self.sender().setAutoRaise(True) - # ========================================================================= - def set_lang(self, lang): """Sets the language of all the labels in the figure.""" self.language = lang @@ -218,7 +216,6 @@ def generate_graph(self, wxdset): self.grid_weather_normals.populate_table(wxdset['normals']) - # --------------------------------------------------------------------- def save_graph(self): yrmin = np.min(self.wxdset['Year']) @@ -400,9 +397,6 @@ def export_series_tofile(self, filename, time_frame): QApplication.restoreOverrideCursor() -# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: - - class FigWeatherNormals(FigureCanvasQTAgg): """ This is the class that does all the plotting of the weather normals. @@ -636,7 +630,7 @@ def plot_monthly_normals(self, normals): self.normals = normals - # ------------------------------------------- assign local variables -- + # Assign local variables : Tmax_norm = normals['Tmax'] Tmin_norm = normals['Tmin'] @@ -645,18 +639,9 @@ def plot_monthly_normals(self, normals): Rain_norm = normals['Rain'] Snow_norm = Ptot_norm - Rain_norm - print('Tmax Yearly Avg. = %0.1f' % np.mean(Tmax_norm)) - print('Tmin Yearly Avg. = %0.1f' % np.mean(Tmin_norm)) - print('Tavg Yearly Avg. = %0.1f' % np.mean(Tavg_norm)) - print('Ptot Yearly Acg. = %0.1f' % np.sum(Ptot_norm)) - - # ------------------------------------------------ DEFINE AXIS RANGE -- - - if np.sum(Ptot_norm) < 500: - Yscale0 = 10 # Precipitation (mm) - else: - Yscale0 = 20 + # Define the range of the axis : + Yscale0 = 10 if np.sum(Ptot_norm) < 500 else 20 # Precipitation (mm) Yscale1 = 5 # Temperature (deg C) SCA0 = np.arange(0, 10000, Yscale0) @@ -701,7 +686,7 @@ def plot_monthly_normals(self, normals): Ymin1 = -20 # ---- - # ------------------------------------------------- YTICKS FORMATING -- + # Define the fomatting of the yticks : ax0 = self.figure.axes[1] ax1 = self.figure.axes[2] @@ -723,7 +708,7 @@ def plot_monthly_normals(self, normals): yticks1_minor = np.arange(yticks1[0], yticks1[-1], Yscale1/5.) ax1.set_yticks(yticks1_minor, minor=True) - # --------------------------------------------------- SET AXIS RANGE -- + # Set the range of the axis : ax0.set_ylim(Ymin0, Ymax0) ax1.set_ylim(Ymin1, Ymax1) @@ -768,48 +753,39 @@ def set_axes_labels(self): ax1.set_ylabel(labelDB.Tlabel, va='bottom', fontsize=16) ax1.yaxis.set_label_coords(-0.09, 0.5) - # ========================================================================= + # ---- Plot the Data def plot_precip(self, PNORM, SNORM): - # ---- define vertices manually ---- + # Define the vertices manually : Xmid = np.arange(0.5, 12.5, 1) n = 0.5 # Controls the width of the bins - f = 0.65 # Controls the spacing between the bins + f = 0.75 # Controls the spacing between the bins - Xpos = np.vstack((Xmid - n * f, - Xmid - n * f, - Xmid + n * f, - Xmid + n * f)).transpose().flatten() + Xpos = np.vstack((Xmid - n * f, Xmid - n * f, + Xmid + n * f, Xmid + n * f)).transpose().flatten() - Ptot = np.vstack((PNORM * 0, - PNORM, - PNORM, - PNORM * 0)).transpose().flatten() + Ptot = np.vstack((PNORM * 0, PNORM, + PNORM, PNORM * 0)).transpose().flatten() - Snow = np.vstack((SNORM * 0, - SNORM, - SNORM, - SNORM * 0)).transpose().flatten() + Snow = np.vstack((SNORM * 0, SNORM, + SNORM, SNORM * 0)).transpose().flatten() - # -- plot data -- + # Plot the data : ax = self.figure.axes[1] - for collection in reversed(ax.collections): collection.remove() colors = ColorsReader() colors.load_colors_db() - ax.fill_between(Xpos, 0., Ptot, edgecolor='none', + ax.fill_between(Xpos, 0, Ptot, edgecolor='none', color=colors.rgb['Rain']) - ax.fill_between(Xpos, 0., Snow, edgecolor='none', + ax.fill_between(Xpos, 0, Snow, edgecolor='none', color=colors.rgb['Snow']) - # --------------------------------------------------------------------- - def plot_air_temp(self, Tmax_norm, Tavg_norm, Tmin_norm): for i, Tnorm in enumerate([Tmax_norm, Tavg_norm, Tmin_norm]): T0 = (Tnorm[-1]+Tnorm[0])/2 @@ -817,33 +793,25 @@ def plot_air_temp(self, Tmax_norm, Tavg_norm, Tmin_norm): self.figure.axes[2].lines[i].set_ydata(T) self.figure.axes[2].lines[3].set_ydata(Tavg_norm) - # ========================================================================= - def update_yearly_avg(self): Tavg_norm = self.normals['Tavg'] Ptot_norm = self.normals['Ptot'] - ax = self.figure.axes[0] - # ---- update position ---- + # Update the position of the labels : bbox = ax.texts[0].get_window_extent(self.get_renderer()) bbox = bbox.transformed(ax.transAxes.inverted()) - ax.texts[1].set_position((0, bbox.y0)) - # ---- update labels ---- + # Update the text of the labels : labelDB = LabelDB(self.lang) - ax.texts[0].set_text(labelDB.Tyrly % np.mean(Tavg_norm)) ax.texts[1].set_text(labelDB.Pyrly % np.sum(Ptot_norm)) -# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: - - class GridWeatherNormals(QTableWidget): def __init__(self, parent=None): From 2b3b3df387ba8bfba4c30042bd69ed6ca6e6aafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Wed, 10 Jan 2018 22:26:13 -0500 Subject: [PATCH 10/29] Improved the RangeSpinBoxes implementation --- gwhat/widgets/buttons.py | 192 ++++++++++++++++++++++++++++++--------- 1 file changed, 149 insertions(+), 43 deletions(-) diff --git a/gwhat/widgets/buttons.py b/gwhat/widgets/buttons.py index e4f77892f..a70b55cdd 100644 --- a/gwhat/widgets/buttons.py +++ b/gwhat/widgets/buttons.py @@ -12,16 +12,139 @@ # ---- Imports: Third Parties +import numpy as np from PyQt5.QtCore import pyqtSignal as QSignal from PyQt5.QtCore import pyqtSlot as QSlot from PyQt5.QtCore import QSize, Qt from PyQt5.QtWidgets import (QApplication, QToolButton, QListWidget, QStyle, - QSpinBox, QWidget, QAbstractSpinBox) + QWidget, QDoubleSpinBox) + + +class SmartSpinBox(QDoubleSpinBox): + """ + A spinbox that can act as a QSpinBox or QDoubleSpinBox that stores its + value in an internal variable so that there is no loss in precision when + the value of the spinbox is set programatically. In addition, the + previous value of the spinbox is stored internally. + + The signal that is emitted when the value of the spinbox changes is also + smarter than the one implemented in the QDoubleSpinBox. The signal also + send the previous value in addition to the new value. + + Finally, it is allowed to enter values that are above or below the range + of the spinbox when editing the value in the line edit. The value will + be corrected to the maximum or minimum value once the editing is finished. + """ + sig_value_changed = QSignal(float, float) + + def __init__(self, val=0, dec=0, step=1, units=None, parent=None): + super(SmartSpinBox, self).__init__(parent) + self.setButtonSymbols(QDoubleSpinBox.NoButtons) + self.setAlignment(Qt.AlignCenter) + self.setKeyboardTracking(False) + self.setAccelerated(True) + + self.__current_value = val + self.__previous_value = None + self.setDecimals(dec) + self.setRange(0, 100) + self.setValue(val) + if step is not None: + self.setSingleStep(step) + else: + self.setSingleStep(10**-dec) + if units is not None: + self.setSuffix(units) + + self.editingFinished.connect(self.editValue) + + def keyPressEvent(self, event): + """ + Qt method overrides to block certain key events when we want the + spinbox to act as a QSpinBox instead of a QDoubleSpinBox. + """ + if (event.key() in [Qt.Key_Comma, Qt.Key_Period] and + self.decimals() == 0): + event.accept() + elif event.key() == Qt.Key_Minus and self.__min_value >= 0: + event.accept() + else: + super(SmartSpinBox, self).keyPressEvent(event) + + def editValue(self): + """ + Ensure that the value that was entered by editing the value of the + spin box is within the range of values of the spinbox. + """ + new_value = super(SmartSpinBox, self).value() + new_value = max(min(new_value, self.__max_value), self.__min_value) + self.setValue(new_value) + + def stepBy(self, n): + """ + Qt method overrides to ensure the value remains within the + range of values of the spinbox. + """ + new_value = self.value() + n*self.singleStep() + new_value = max(min(new_value, self.__max_value), self.__min_value) + self.setValue(new_value) + + def value(self): + """ + Qt method override that returns the value stocked in the internal + variable instead of the one displayed in the UI. + """ + return self.__current_value + + def previousValue(self): + """ + Returns the previous value of the spinbox. + """ + return self.__previous_value + + def setValue(self, new_value): + """Qt method override to save the value in an internal variable.""" + self.blockSignals(True) + super(SmartSpinBox, self).setValue(new_value) + self.blockSignals(False) + if new_value != self.__current_value: + self.__previous_value = self.__current_value + self.__current_value = new_value + self.sig_value_changed.emit( + self.__current_value, self.__previous_value) + + def setValueSilently(self, x): + """ + Sets the value of the spinbox silently. + """ + self.blockSignals(True) + self.setValue(x) + self.blockSignals(False) + + def setDecimal(self, x): + """Qt method override to force a reset of the displayed range.""" + super(SmartSpinBox, self).setDecimal(x) + self.setRange(self.__min_value, self.__max_value) + + def setRange(self, xmin, xmax): + """Qt method override to save the range in internal variables.""" + if xmin > xmax: + raise ValueError("xmin must be <= xmax") + self.__max_value = xmax + self.__min_value = xmin + + lenght_int = int(np.ceil(np.log10(max(abs(xmax), abs(xmin)))))+1 + max_edit = '9' * lenght_int + '.' + '9' * self.decimals() + max_edit = int(max_edit) if self.decimals() > 0 else float(max_edit) + super(SmartSpinBox, self).setRange(-max_edit, max_edit) + + new_value = max(min(self.value(), self.__max_value), self.__min_value) + self.setValue(new_value) class RangeSpinBoxes(QWidget): """ - Consists of two QSpinBox that are linked togheter so that one represent a + Consists of two spinboxes that are linked togheter so that one represent a lower boundary and the other one the upper boundary of a range. """ sig_range_changed = QSignal(tuple) @@ -29,22 +152,15 @@ class RangeSpinBoxes(QWidget): def __init__(self, min_value=0, max_value=0, orientation=Qt.Horizontal, parent=None): super(RangeSpinBoxes, self).__init__(parent) - self.spb_lower = QSpinBox() - self.spb_lower.setButtonSymbols(QAbstractSpinBox.NoButtons) - self.spb_lower.setAlignment(Qt.AlignCenter) - self.spb_lower.setRange(0, 9999) - self.spb_lower.setKeyboardTracking(False) - - self.spb_upper = QSpinBox() - self.spb_upper.setButtonSymbols(QAbstractSpinBox.NoButtons) - self.spb_upper.setAlignment(Qt.AlignCenter) - self.spb_upper.setRange(0, 9999) - self.spb_upper.setKeyboardTracking(False) + self.spb_lower = SmartSpinBox() + self.spb_upper = SmartSpinBox() - self.spb_lower.valueChanged.connect(self.constain_bounds_to_minmax) - self.spb_upper.valueChanged.connect(self.constain_bounds_to_minmax) + self.spb_lower.sig_value_changed.connect( + self.constain_bounds_to_minmax) + self.spb_upper.sig_value_changed.connect( + self.constain_bounds_to_minmax) - self.set_range(1000, 9999) + self.setRange(1000, 9999) @property def lower_bound(self): @@ -54,38 +170,26 @@ def lower_bound(self): def upper_bound(self): return self.spb_upper.value() - def set_range(self, min_value, max_value): + def setRange(self, min_value, max_value): """Set the min max values of the range for both spin boxes.""" - self.min_value = min_value - self.max_value = max_value - self.spb_lower.setValue(min_value) - self.spb_upper.setValue(max_value) - - @QSlot(int) - def constain_bounds_to_minmax(self, new_value): + if min_value > max_value: + raise ValueError("min_value > max_value") + self.spb_lower.setRange(min_value, max_value) + self.spb_upper.setRange(min_value, max_value) + self.spb_lower.setValueSilently(min_value) + self.spb_upper.setValueSilently(max_value) + + @QSlot(float, float) + def constain_bounds_to_minmax(self, new_value, old_value): """ Makes sure that the new value is within the min and max values that were set for the range. Also makes sure that the """ - is_range_changed = False - corr_value = min(max(new_value, self.min_value), self.max_value) - if corr_value != new_value: - is_range_changed = True - self.sender().blockSignals(True) - self.sender().setValue(corr_value) - self.sender().blockSignals(False) - if corr_value > self.spb_upper.value(): - is_range_changed = True - self.spb_upper.blockSignals(True) - self.spb_upper.setValue(corr_value) - self.spb_upper.blockSignals(False) - if corr_value < self.spb_lower.value(): - is_range_changed = True - self.spb_lower.blockSignals(True) - self.spb_lower.setValue(corr_value) - self.spb_lower.blockSignals(False) - if is_range_changed is True: - self.sig_range_changed.emit((self.lower_bound, self.upper_bound)) + if new_value > self.spb_upper.value(): + self.spb_upper.setValueSilently(new_value) + if new_value < self.spb_lower.value(): + self.spb_lower.setValueSilently(new_value) + self.sig_range_changed.emit((self.lower_bound, self.upper_bound)) class DropDownButton(QToolButton): @@ -173,6 +277,8 @@ def focusOutEvent(self, event): self.hide() +# %% if __name__ == '__main__' + if __name__ == '__main__': import sys From d7460cb2cd15b0f55c68f3b37c19a17dd6e316c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Wed, 10 Jan 2018 22:27:26 -0500 Subject: [PATCH 11/29] Update the graph and table when year range change. --- gwhat/HydroPrint2.py | 2 +- gwhat/meteo/weather_viewer.py | 54 +++++++++++++++++++++++++++++------ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/gwhat/HydroPrint2.py b/gwhat/HydroPrint2.py index 5e218dfe6..1875010fa 100644 --- a/gwhat/HydroPrint2.py +++ b/gwhat/HydroPrint2.py @@ -456,7 +456,7 @@ def show_weather_averages(self): return self.weather_avg_graph.save_fig_dir = self.workdir - self.weather_avg_graph.generate_graph(self.wxdset) + self.weather_avg_graph.set_weather_dataset(self.wxdset) self.weather_avg_graph.show() # ---- Datasets Handlers diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index bd0771632..6f7a32fd4 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -36,6 +36,7 @@ from gwhat.common.widgets import DialogWindow, VSep from gwhat.widgets.buttons import RangeSpinBoxes from gwhat import __namever__ +from gwhat.meteo.weather_reader import calcul_monthly_normals mpl.rc('font', **{'family': 'sans-serif', 'sans-serif': ['Arial']}) @@ -138,7 +139,8 @@ def __initUI__(self): # Instantiate and define a layout for the year range widget : self.year_rng = RangeSpinBoxes() - self.year_rng.set_range(1800, datetime.now().year) + self.year_rng.setRange(1800, datetime.now().year) + self.year_rng.sig_range_changed.connect(self.year_range_changed) qgrid = QHBoxLayout(self.year_rng) qgrid.setContentsMargins(0, 0, 0, 0) @@ -207,15 +209,51 @@ def set_lang(self, lang): self.fig_weather_normals.set_lang(lang) self.fig_weather_normals.draw() - def generate_graph(self, wxdset): + def set_weather_dataset(self, wxdset): + """ + Generates the graph, updates the table, and updates the GUI for + the new weather dataset. + """ self.wxdset = wxdset - self.fig_weather_normals.plot_monthly_normals(wxdset['normals']) - self.fig_weather_normals.draw() + # Update the GUI : self.setWindowTitle('Weather Averages for %s' % wxdset['Station Name']) - - self.grid_weather_normals.populate_table(wxdset['normals']) - + self.year_rng.setRange(np.min(wxdset['monthly']['Year']), + np.max(wxdset['monthly']['Year'])) + self.year_range_changed() + + def year_range_changed(self): + """ + Forces a replot of the normals and an update of the table with the + values calculated over the new range of years. + """ + normals = self.calcul_normals() + # Redraw the normals in the graph : + self.fig_weather_normals.plot_monthly_normals(normals) + self.fig_weather_normals.draw() + # Update the values in the table : + self.grid_weather_normals.populate_table(normals) + + def calcul_normals(self): + """ + Calcul the normal values of the weather dataset for the currently + defined period in the year range widget. + """ + keys = ['Tmax', 'Tmin', 'Tavg', 'Ptot', 'Rain', 'Snow', 'PET'] + monthly = self.wxdset['monthly'] + normals = {} + for key in keys: + if monthly[key] is None: + normals[key] = None + else: + normals[key] = calcul_monthly_normals( + monthly['Year'], monthly['Month'], monthly[key], + self.year_rng.lower_bound, self.year_rng.upper_bound) + + normals['Period'] = (self.year_rng.lower_bound, + self.year_rng.upper_bound) + + return normals def save_graph(self): yrmin = np.min(self.wxdset['Year']) @@ -948,7 +986,7 @@ def populate_table(self, NORMALS): w.save_fig_dir = os.getcwd() w.set_lang('English') - w.generate_graph(wxdset) + w.set_weather_dataset(wxdset) w.show() app.exec_() From dd9141fc8224c72810e7aa2ac34da03f0f875c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 11 Jan 2018 08:19:47 -0500 Subject: [PATCH 12/29] :art: More code style changes --- gwhat/meteo/weather_viewer.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 6f7a32fd4..2aae68fc1 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -78,8 +78,8 @@ def __init__(self, language): self.Tlabel = 'Températures moyennes mensuelles (°C)' self.Plabel = 'Précipitations totales mensuelles (mm)' - self.month_names = ["JAN", u"FÉV", "MAR", "AVR", "MAI", "JUN", - "JUL", u"AOÛ", "SEP", "OCT", "NOV", u"DÉC"] + self.month_names = ["JAN", "FÉV", "MAR", "AVR", "MAI", "JUN", + "JUL", "AOÛ", "SEP", "OCT", "NOV", "DÉC"] class WeatherViewer(DialogWindow): @@ -99,7 +99,6 @@ def __init__(self, parent=None): self.__initUI__() def __initUI__(self): - self.setWindowTitle('Weather Averages') self.setWindowIcon(icons.get_icon('master')) @@ -182,15 +181,13 @@ def __initUI__(self): row += 1 mainGrid.addWidget(self.grid_weather_normals, row, 0) - mainGrid.setContentsMargins(10, 10, 10, 10) # (L,T,R,B) + mainGrid.setContentsMargins(10, 10, 10, 10) # (L, T, R, B) mainGrid.setSpacing(10) mainGrid.setRowStretch(row, 500) mainGrid.setColumnStretch(0, 500) self.setLayout(mainGrid) - # ========================================================================= - def show_monthly_grid(self): if self.grid_weather_normals.isHidden(): self.grid_weather_normals.show() @@ -234,6 +231,8 @@ def year_range_changed(self): # Update the values in the table : self.grid_weather_normals.populate_table(normals) + # ---- Normals + def calcul_normals(self): """ Calcul the normal values of the weather dataset for the currently @@ -275,8 +274,6 @@ def save_graph(self): self.save_fig_dir = os.path.dirname(filename) self.fig_weather_normals.figure.savefig(filename) - # ========================================================================= - def save_normals(self): yrmin = np.min(self.wxdset['Year']) yrmax = np.max(self.wxdset['Year']) @@ -324,7 +321,7 @@ def save_normals(self): writer = csv.writer(f, delimiter=',', lineterminator='\n') writer.writerows(fcontent) - # ================================================= Export Time Series ==== + # ---- Export Time Series def select_export_file(self): if self.sender() == self.btn_export.menu().actions()[0]: @@ -348,8 +345,6 @@ def select_export_file(self): if filename: self.export_series_tofile(filename, time_frame) - # --------------------------------------------------------------------- - def export_series_tofile(self, filename, time_frame): if time_frame == 'daily': vrbs = ['Year', 'Month', 'Day'] From cd3cba6bb01f8c6213533df6d4a4d326ee74d226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 11 Jan 2018 13:55:17 -0500 Subject: [PATCH 13/29] Updated the save normals to file functionality --- gwhat/meteo/weather_viewer.py | 67 ++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 2aae68fc1..cf2bc2390 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -12,6 +12,7 @@ import sys import os +import os.path as osp import csv from time import strftime from datetime import datetime @@ -37,6 +38,7 @@ from gwhat.widgets.buttons import RangeSpinBoxes from gwhat import __namever__ from gwhat.meteo.weather_reader import calcul_monthly_normals +from gwhat.common.utils import save_content_to_excel, save_content_to_csv mpl.rc('font', **{'family': 'sans-serif', 'sans-serif': ['Arial']}) @@ -91,6 +93,7 @@ def __init__(self, parent=None): super(WeatherViewer, self).__init__(parent) self.wxdset = None + self.normals = None self.save_fig_dir = os.getcwd() self.meteo_dir = os.getcwd() @@ -139,7 +142,7 @@ def __initUI__(self): self.year_rng = RangeSpinBoxes() self.year_rng.setRange(1800, datetime.now().year) - self.year_rng.sig_range_changed.connect(self.year_range_changed) + self.year_rng.sig_range_changed.connect(self.update_normals) qgrid = QHBoxLayout(self.year_rng) qgrid.setContentsMargins(0, 0, 0, 0) @@ -217,19 +220,19 @@ def set_weather_dataset(self, wxdset): self.setWindowTitle('Weather Averages for %s' % wxdset['Station Name']) self.year_rng.setRange(np.min(wxdset['monthly']['Year']), np.max(wxdset['monthly']['Year'])) - self.year_range_changed() + self.update_normals() - def year_range_changed(self): + def update_normals(self): """ Forces a replot of the normals and an update of the table with the values calculated over the new range of years. """ - normals = self.calcul_normals() + self.normals = self.calcul_normals() # Redraw the normals in the graph : - self.fig_weather_normals.plot_monthly_normals(normals) + self.fig_weather_normals.plot_monthly_normals(self.normals) self.fig_weather_normals.draw() # Update the values in the table : - self.grid_weather_normals.populate_table(normals) + self.grid_weather_normals.populate_table(self.normals) # ---- Normals @@ -275,51 +278,49 @@ def save_graph(self): self.fig_weather_normals.figure.savefig(filename) def save_normals(self): - yrmin = np.min(self.wxdset['Year']) - yrmax = np.max(self.wxdset['Year']) + """ + Save the montly and yearly normals in a file. + """ + # Define a default name for the file : + yrmin = self.normals['Period'][0] + yrmax = self.normals['Period'][1] staname = self.wxdset['Station Name'] defaultname = 'WeatherNormals_%s (%d-%d)' % (staname, yrmin, yrmax) - ddir = os.path.join(self.save_fig_dir, defaultname) + ddir = osp.join(self.save_fig_dir, defaultname) + # Open a dialog to get a save file name : dialog = QFileDialog() filename, ftype = dialog.getSaveFileName( self, 'Save normals', ddir, '*.xlsx;;*.xls;;*.csv') - hheader = ['', 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', - 'AUG', 'SEP', 'OCT', 'NOV', 'DEC', 'YEAR'] + if filename: + self.save_fig_dir = osp.dirname(filename) - vrbs = ['Tmin', 'Tavg', 'Tmax', 'Rain', 'Snow', 'Ptot', 'PET'] + # Organise the content to save to file. + hheader = ['', 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', + 'AUG', 'SEP', 'OCT', 'NOV', 'DEC', 'YEAR'] - lbls = ['Daily Tmin (\u00B0C)', 'Daily Tavg (\u00B0C)', - 'Daily Tmax (\u00B0C)', 'Rain (mm)', 'Snow (mm)', - 'Total Precip. (mm)', 'ETP (mm)'] + vrbs = ['Tmin', 'Tavg', 'Tmax', 'Rain', 'Snow', 'Ptot', 'PET'] - if ftype in ['*.xlsx', '*.xls']: - wb = xlsxwriter.Workbook(filename) - ws = wb.add_worksheet() + lbls = ['Daily Tmin (\u00B0C)', 'Daily Tavg (\u00B0C)', + 'Daily Tmax (\u00B0C)', 'Rain (mm)', 'Snow (mm)', + 'Total Precip. (mm)', 'ETP (mm)'] - ws.write_row(0, 0, hheader) - for i, (vrb, lbl) in enumerate(zip(vrbs, lbls)): - ws.write(i+1, 0, lbl) - ws.write_row(i+1, 1, self.wxdset['normals'][vrb]) - if vrb in ['Tmin', 'Tavg', 'Tmax']: - ws.write(i+1, 13, np.mean(self.wxdset['normals'][vrb])) - else: - ws.write(i+1, 13, np.sum(self.wxdset['normals'][vrb])) - elif ftype == '*.csv': fcontent = [hheader] for i, (vrb, lbl) in enumerate(zip(vrbs, lbls)): fcontent.append([lbl]) - fcontent[-1].extend(self.wxdset['normals'][vrb].tolist()) + fcontent[-1].extend(self.normals[vrb].tolist()) if vrb in ['Tmin', 'Tavg', 'Tmax']: - fcontent[-1].append(np.mean(self.wxdset['normals'][vrb])) + fcontent[-1].append(np.mean(self.normals[vrb])) else: - fcontent[-1].append(np.sum(self.wxdset['normals'][vrb])) + fcontent[-1].append(np.sum(self.normals[vrb])) - with open(filename, 'w', encoding='utf8')as f: - writer = csv.writer(f, delimiter=',', lineterminator='\n') - writer.writerows(fcontent) + # Save the content to file : + if ftype in ['*.xlsx', '*.xls']: + save_content_to_excel(filename, fcontent) + else: + save_content_to_csv(filename, fcontent) # ---- Export Time Series From 3f4994c5eec8c681f6214837bcdc015e60ba0ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 11 Jan 2018 15:31:12 -0500 Subject: [PATCH 14/29] :wrench: Clean imports --- gwhat/meteo/weather_viewer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 4c2ea9656..405c51402 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -13,13 +13,11 @@ import sys import os import os.path as osp -import csv from time import strftime from datetime import datetime # ---- Imports: Third Parties -import xlsxwriter import numpy as np import matplotlib as mpl from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg From 7f65f8920902be5975cc4a693efd491c8633a2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 11 Jan 2018 17:07:37 -0500 Subject: [PATCH 15/29] :art: Some code style changes --- gwhat/meteo/weather_viewer.py | 70 +++++++++++++++++------------------ 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 405c51402..2dc5f49f5 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -424,7 +424,7 @@ def __init__(self, lang='English'): bottom_margin = 0.35/fh top_margin = 0.1/fh - # ------------------------------------------------ Yearly Avg Labels -- + # ---- Yearly Avg Labels # The yearly yearly averages for the mean air temperature and # the total precipitation are displayed in , which is placed on @@ -437,7 +437,7 @@ def __init__(self, lang='English'): right='off', labelbottom='off', labeltop='off', labelleft='off', labelright='off') - # ---- Mean Annual Air Temperature ---- + # Mean Annual Air Temperature : # Places first label at the top left corner of with a horizontal # padding of 5 points and downward padding of 3 points. @@ -449,7 +449,7 @@ def __init__(self, lang='English'): ax3.text(0., 1., 'Mean Annual Air Temperature', fontsize=13, va='top', transform=transform) - # ---- Mean Annual Precipitation ---- + # Mean Annual Precipitation : # Get the bounding box of the first label. @@ -466,7 +466,7 @@ def __init__(self, lang='English'): bbox = ax3.texts[1].get_window_extent(renderer) bbox = bbox.transformed(fig.transFigure.inverted()) - # ---- update geometry ---- + # Update geometry : # Updates the geometry and position of to accomodate the text. @@ -477,24 +477,24 @@ def __init__(self, lang='English'): ax3.set_position([x0, y0, axw, axh]) - # -------------------------------------------------------- Data Axes -- + # ---- Data Axes axh = y0 - bottom_margin y0 = y0 - axh - # ---- Precip ---- + # Precipitation : ax0 = fig.add_axes([x0, y0, axw, axh], zorder=1) ax0.patch.set_visible(False) ax0.spines['top'].set_visible(False) ax0.set_axisbelow(True) - # ---- Air Temp. ---- + # Air Temperature : ax1 = fig.add_axes(ax0.get_position(), frameon=False, zorder=5, sharex=ax0) - # ----------------------------------------------------- INIT ARTISTS -- + # ---- Initialize the Artists # This is only to initiates the artists and to set their parameters # in advance. The plotting of the data is actually done by calling @@ -506,23 +506,22 @@ def __init__(self, lang='English'): y = range(len(XPOS)) colors = ['#990000', '#FF0000', '#FF6666'] - # dashed lines for Tmax, Tavg, and Tmin : + # Dashed lines for Tmax, Tavg, and Tmin : for i in range(3): ax1.plot(XPOS, y, color=colors[i], ls='--', lw=1.5, zorder=100) - # markers for Tavg : + # Markers for Tavg : ax1.plot(XPOS[1:-1], y[1:-1], color=colors[1], marker='o', ls='none', ms=6, zorder=100, mec=colors[1], mfc='white', mew=1.5) - # ------------------------------------------------- XTICKS FORMATING -- + # ---- Xticks Formatting Xmin0 = 0 Xmax0 = 12.001 - # ---- major ---- - + # Major ticks ax0.xaxis.set_ticks_position('bottom') ax0.tick_params(axis='x', direction='out') ax0.xaxis.set_ticklabels([]) @@ -531,47 +530,43 @@ def __init__(self, lang='English'): ax1.tick_params(axis='x', which='both', bottom='off', top='off', labelbottom='off') - # ---- minor ---- - + # Minor ticks ax0.set_xticks(np.arange(Xmin0+0.5, Xmax0+0.49, 1), minor=True) ax0.tick_params(axis='x', which='minor', direction='out', length=0, labelsize=13) ax0.xaxis.set_ticklabels(month_names, minor=True) - # ------------------------------------------------- Yticks Formating -- - - # ---- Precipitation ---- + # ---- Y-ticks Formatting + # Precipitation ax0.yaxis.set_ticks_position('right') ax0.tick_params(axis='y', direction='out', labelsize=13) ax0.tick_params(axis='y', which='minor', direction='out') ax0.yaxis.set_ticklabels([], minor=True) - # ---- Air Temp. ---- - + # Air Temperature ax1.yaxis.set_ticks_position('left') ax1.tick_params(axis='y', direction='out', labelsize=13) ax1.tick_params(axis='y', which='minor', direction='out') ax1.yaxis.set_ticklabels([], minor=True) - # ------------------------------------------------------------- GRID -- + # ---- Grid Parameters # ax0.grid(axis='y', color=[0.5, 0.5, 0.5], linestyle=':', linewidth=1, # dashes=[1, 5]) # ax0.grid(axis='y', color=[0.75, 0.75, 0.75], linestyle='-', # linewidth=0.5) - # ------------------------------------------------------------ XLIMS -- + # ---- Limits of the Axes ax0.set_xlim(Xmin0, Xmax0) - # ------------------------------------------------------ Plot Legend -- + # ---- Legend self.plot_legend() - # =========================================================== Language ==== def set_lang(self, lang): self.lang = lang @@ -584,19 +579,23 @@ def set_lang(self, lang): month_names = LabelDB(self.lang).month_names self.figure.axes[1].xaxis.set_ticklabels(month_names, minor=True) - # ============================================================ Legend ===== + self.figure.axes[1].xaxis.set_ticklabels( + self.fig_labels.month_names, minor=True) + if self.normals is not None: + self.set_axes_labels() + self.update_yearly_avg() def plot_legend(self): + """Plot the legend of the figure.""" + ax = self.figure.axes[2] - ax = self.figure.axes[2] # Axe on which the legend is hosted - - # --- bbox transform --- # + # bbox transform : padding = mpl.transforms.ScaledTranslation(5/72, -5/72, self.figure.dpi_scale_trans) transform = ax.transAxes + padding - # --- proxy artists --- # + # Define proxy artists : colors = ColorsReader() colors.load_colors_db() @@ -606,22 +605,20 @@ def plot_legend(self): rec2 = mpl.patches.Rectangle((0, 0), 1, 1, fc=colors.rgb['Rain'], ec='none') - # --- legend entry --- # + # Define the legend labels and markers : lines = [ax.lines[0], ax.lines[1], ax.lines[2], rec2, rec1] labelDB = LabelDB(self.lang) labels = [labelDB.Tmax, labelDB.Tavg, labelDB.Tmin, labelDB.rain, labelDB.snow] - # --- plot legend --- # + # Plot the legend : leg = ax.legend(lines, labels, numpoints=1, fontsize=13, borderaxespad=0, loc='upper left', borderpad=0, bbox_to_anchor=(0, 1), bbox_transform=transform) leg.draw_frame(False) - # ========================================================= Plot data ===== - def plot_monthly_normals(self, normals): """Plot the normals on the figure.""" @@ -710,17 +707,18 @@ def plot_monthly_normals(self, normals): ax0.set_ylim(Ymin0, Ymax0) ax1.set_ylim(Ymin1, Ymax1) - # ----------------------------------------------------------- LABELS -- + # ---- LABELS self.set_axes_labels() + self.set_year_range() - # --------------------------------------------------------- PLOTTING -- + # ---- PLOTTING self.plot_precip(Ptot_norm, Snow_norm) self.plot_air_temp(Tmax_norm, Tavg_norm, Tmin_norm) self.update_yearly_avg() - # --------------------------------------------------------- Clipping -- + # ---- Clipping # There is currently a bug regarding this. So we need to do a # workaround From ad2fabc5906981514974072558df1dc3fd866a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 11 Jan 2018 17:09:43 -0500 Subject: [PATCH 16/29] :recycle: Refactoring of how language is handled. --- gwhat/meteo/weather_viewer.py | 139 +++++++++++++++++----------------- 1 file changed, 71 insertions(+), 68 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 2dc5f49f5..5e3c13a23 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -41,47 +41,6 @@ mpl.rc('font', **{'family': 'sans-serif', 'sans-serif': ['Arial']}) -class LabelDB(object): - - def __init__(self, language): - - # ---- Legend ---- - - self.Pyrly = 'Annual total precipitation = %0.0f mm' - self.Tyrly = 'Average annual air temperature = %0.1f °C' - self.rain = 'Rain' - self.snow = 'Snow' - self.Tmax = 'Temp. max.' - self.Tmin = 'Temp. min.' - self.Tavg = 'Temp. mean' - - # ---- Labels ---- - - self.Tlabel = 'Monthly Air Temperature (°C)' - self.Plabel = 'Monthly Total Precipitation (mm)' - self.month_names = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", - "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] - - if language == 'French': - - # ---- Legend ---- - - self.Pyrly = 'Précipitations totales annuelles = %0.0f mm' - self.Tyrly = 'Température moyenne annuelle = %0.1f °C' - self.rain = 'Pluie' - self.snow = 'Neige' - self.Tmax = 'Températures min.' - self.Tmin = 'Températures max.' - self.Tavg = 'Températures moy.' - - # ---- Labels ---- - - self.Tlabel = 'Températures moyennes mensuelles (°C)' - self.Plabel = 'Précipitations totales mensuelles (mm)' - self.month_names = ["JAN", "FÉV", "MAR", "AVR", "MAI", "JUN", - "JUL", "AOÛ", "SEP", "OCT", "NOV", "DÉC"] - - class WeatherViewer(DialogWindow): """ GUI that allows to plot weather normals, save the graphs to file, see @@ -394,6 +353,49 @@ def export_series_tofile(self, filename, time_frame): QApplication.restoreOverrideCursor() +class FigureLabels(object): + + LANGUAGES = ['english', 'french'] + + def __init__(self, language): + + # Legend : + + self.Pyrly = 'Annual total precipitation = %0.0f mm' + self.Tyrly = 'Average annual air temperature = %0.1f °C' + self.rain = 'Rain' + self.snow = 'Snow' + self.Tmax = 'Temp. max.' + self.Tmin = 'Temp. min.' + self.Tavg = 'Temp. mean' + + # Labels : + + self.Tlabel = 'Monthly Air Temperature (°C)' + self.Plabel = 'Monthly Total Precipitation (mm)' + self.month_names = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] + + if language.lower() == 'french': + + # Legend : + + self.Pyrly = 'Précipitations totales annuelles = %0.0f mm' + self.Tyrly = 'Température moyenne annuelle = %0.1f °C' + self.rain = 'Pluie' + self.snow = 'Neige' + self.Tmax = 'Températures min.' + self.Tmin = 'Températures max.' + self.Tavg = 'Températures moy.' + + # Labels : + + self.Tlabel = 'Températures moyennes mensuelles (°C)' + self.Plabel = 'Précipitations totales mensuelles (mm)' + self.month_names = ["JAN", "FÉV", "MAR", "AVR", "MAI", "JUN", + "JUL", "AOÛ", "SEP", "OCT", "NOV", "DÉC"] + + class FigWeatherNormals(FigureCanvasQTAgg): """ This is the class that does all the plotting of the weather normals. @@ -405,19 +407,15 @@ class FigWeatherNormals(FigureCanvasQTAgg): """ def __init__(self, lang='English'): + self.__figlang = lang if lang in FigureLabels.LANGUAGES else 'English' + self.__figlabels = FigureLabels(self.__figlang) + self.normals = None fw, fh = 8.5, 5. fig = mpl.figure.Figure(figsize=(fw, fh), facecolor='white') - super(FigWeatherNormals, self).__init__(fig) - self.lang = lang - self.normals = None - - labelDB = LabelDB(self.lang) - month_names = labelDB.month_names - - # --------------------------------------------------- Define Margins -- + # Define the Margins : left_margin = 1/fw right_margin = 1/fw @@ -534,7 +532,7 @@ def __init__(self, lang='English'): ax0.set_xticks(np.arange(Xmin0+0.5, Xmax0+0.49, 1), minor=True) ax0.tick_params(axis='x', which='minor', direction='out', length=0, labelsize=13) - ax0.xaxis.set_ticklabels(month_names, minor=True) + ax0.xaxis.set_ticklabels(self.fig_labels.month_names, minor=True) # ---- Y-ticks Formatting @@ -567,18 +565,22 @@ def __init__(self, lang='English'): self.plot_legend() + @property + def fig_labels(self): + return self.__figlabels + + @property + def fig_language(self): + return self.__figlang def set_lang(self, lang): - self.lang = lang - if self.normals is None: - return + """Sets the language of the figure labels.""" + lang = lang if lang in FigureLabels.LANGUAGES else 'English' + self.__figlabels = FigureLabels(lang) + self.__figlang = lang + # Update the labels in the plot : self.plot_legend() - self.set_axes_labels() - self.update_yearly_avg() - month_names = LabelDB(self.lang).month_names - self.figure.axes[1].xaxis.set_ticklabels(month_names, minor=True) - self.figure.axes[1].xaxis.set_ticklabels( self.fig_labels.month_names, minor=True) if self.normals is not None: @@ -608,9 +610,9 @@ def plot_legend(self): # Define the legend labels and markers : lines = [ax.lines[0], ax.lines[1], ax.lines[2], rec2, rec1] - labelDB = LabelDB(self.lang) - labels = [labelDB.Tmax, labelDB.Tavg, labelDB.Tmin, - labelDB.rain, labelDB.snow] + labels = [self.fig_labels.Tmax, self.fig_labels.Tavg, + self.fig_labels.Tmin, self.fig_labels.rain, + self.fig_labels.snow] # Plot the legend : @@ -738,14 +740,16 @@ def plot_monthly_normals(self, normals): line.set_clip_box(clip_bbox) def set_axes_labels(self): - labelDB = LabelDB(self.lang) - + """Sets the labels of the y axis.""" + # Set the label fo the precipitation : ax0 = self.figure.axes[1] - ax0.set_ylabel(labelDB.Plabel, va='bottom', fontsize=16, rotation=270) + ax0.set_ylabel(self.fig_labels.Plabel, va='bottom', + fontsize=16, rotation=270) ax0.yaxis.set_label_coords(1.09, 0.5) + # Set the label fo the air temperature : ax1 = self.figure.axes[2] - ax1.set_ylabel(labelDB.Tlabel, va='bottom', fontsize=16) + ax1.set_ylabel(self.fig_labels.Tlabel, va='bottom', fontsize=16) ax1.yaxis.set_label_coords(-0.09, 0.5) # ---- Plot the Data @@ -802,9 +806,8 @@ def update_yearly_avg(self): # Update the text of the labels : - labelDB = LabelDB(self.lang) - ax.texts[0].set_text(labelDB.Tyrly % np.mean(Tavg_norm)) - ax.texts[1].set_text(labelDB.Pyrly % np.sum(Ptot_norm)) + ax.texts[0].set_text(self.fig_labels.Tyrly % np.mean(Tavg_norm)) + ax.texts[1].set_text(self.fig_labels.Pyrly % np.sum(Ptot_norm)) class GridWeatherNormals(QTableWidget): From 96fb2c46b02238a1d20ede76f12d7464fb385742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 11 Jan 2018 17:10:07 -0500 Subject: [PATCH 17/29] Added the year range as a label of the x-axis --- gwhat/meteo/weather_viewer.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 5e3c13a23..fcfc1fa7a 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -419,7 +419,7 @@ def __init__(self, lang='English'): left_margin = 1/fw right_margin = 1/fw - bottom_margin = 0.35/fh + bottom_margin = 0.85/fh top_margin = 0.1/fh # ---- Yearly Avg Labels @@ -752,6 +752,17 @@ def set_axes_labels(self): ax1.set_ylabel(self.fig_labels.Tlabel, va='bottom', fontsize=16) ax1.yaxis.set_label_coords(-0.09, 0.5) + def set_year_range(self): + """Sets the year range label that is displayed below the x axis.""" + if self.normals is not None: + ax0 = self.figure.axes[1] + yearmin, yearmax = self.normals['Period'] + if yearmin == yearmax: + ax0.set_xlabel("%d" % yearmin, fontsize=16, labelpad=10) + else: + ax0.set_xlabel("%d - %d" % (yearmin, yearmax), fontsize=16, + labelpad=10) + # ---- Plot the Data def plot_precip(self, PNORM, SNORM): @@ -945,7 +956,7 @@ def populate_table(self, NORMALS): w = WeatherViewer() w.save_fig_dir = os.getcwd() - w.set_lang('English') + w.set_lang('French') w.set_weather_dataset(wxdset) w.show() From 1907c8a40c33f5d71748d05183bf252819f89794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 11 Jan 2018 17:12:54 -0500 Subject: [PATCH 18/29] Tweaked bottom margin and fixed set_lang --- gwhat/meteo/weather_viewer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index fcfc1fa7a..d8e2608f2 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -407,8 +407,9 @@ class FigWeatherNormals(FigureCanvasQTAgg): """ def __init__(self, lang='English'): - self.__figlang = lang if lang in FigureLabels.LANGUAGES else 'English' - self.__figlabels = FigureLabels(self.__figlang) + lang = lang if lang.lower() in FigureLabels.LANGUAGES else 'English' + self.__figlang = lang + self.__figlabels = FigureLabels(lang) self.normals = None fw, fh = 8.5, 5. @@ -419,7 +420,7 @@ def __init__(self, lang='English'): left_margin = 1/fw right_margin = 1/fw - bottom_margin = 0.85/fh + bottom_margin = 0.7/fh top_margin = 0.1/fh # ---- Yearly Avg Labels @@ -575,7 +576,7 @@ def fig_language(self): def set_lang(self, lang): """Sets the language of the figure labels.""" - lang = lang if lang in FigureLabels.LANGUAGES else 'English' + lang = lang if lang.lower() in FigureLabels.LANGUAGES else 'English' self.__figlabels = FigureLabels(lang) self.__figlang = lang From 2c0cfee34010d5ad0d4329d64906f9bb58fa5ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Thu, 11 Jan 2018 17:22:26 -0500 Subject: [PATCH 19/29] Show buttons by default in the SmartSpinBox --- gwhat/widgets/buttons.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gwhat/widgets/buttons.py b/gwhat/widgets/buttons.py index a70b55cdd..7a46f700a 100644 --- a/gwhat/widgets/buttons.py +++ b/gwhat/widgets/buttons.py @@ -37,9 +37,11 @@ class SmartSpinBox(QDoubleSpinBox): """ sig_value_changed = QSignal(float, float) - def __init__(self, val=0, dec=0, step=1, units=None, parent=None): + def __init__(self, val=0, dec=0, step=1, units=None, parent=None, + show_btns=True): super(SmartSpinBox, self).__init__(parent) - self.setButtonSymbols(QDoubleSpinBox.NoButtons) + if show_btns is False: + self.setButtonSymbols(QDoubleSpinBox.NoButtons) self.setAlignment(Qt.AlignCenter) self.setKeyboardTracking(False) self.setAccelerated(True) From 6d73c2a08b14bd1684f17a7d4c1c3071c26d2d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 12 Jan 2018 09:14:53 -0500 Subject: [PATCH 20/29] Added a button in toolbar to expand year range --- gwhat/common/icons.py | 7 + gwhat/common/widgets.py | 3 +- gwhat/meteo/weather_viewer.py | 31 +++- .../icons_png/expand_bottom_left.png | Bin 0 -> 1977 bytes .../icons_png/expand_range_vert.png | Bin 0 -> 13018 bytes .../icons_scalable/expand_bottom_left.svg | 125 ++++++++++++++++ .../icons_scalable/expand_range_vert.svg | 141 ++++++++++++++++++ 7 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 gwhat/ressources/icons_png/expand_bottom_left.png create mode 100644 gwhat/ressources/icons_png/expand_range_vert.png create mode 100644 gwhat/ressources/icons_scalable/expand_bottom_left.svg create mode 100644 gwhat/ressources/icons_scalable/expand_range_vert.svg diff --git a/gwhat/common/icons.py b/gwhat/common/icons.py index cdc97f5d4..fdd7b6dd5 100644 --- a/gwhat/common/icons.py +++ b/gwhat/common/icons.py @@ -22,6 +22,7 @@ dirname = os.path.join(__rootdir__, 'ressources', 'icons_png') ICON_NAMES = {'master': 'WHAT', + 'expand_range_vert': 'expand_range_vert', 'info': 'info', 'calc_brf': 'start', 'setup': 'page_setup', @@ -131,3 +132,9 @@ class QToolButtonSmall(QToolButtonBase): def __init__(self, Qicon, *args, **kargs): super(QToolButtonSmall, self).__init__(Qicon, *args, **kargs) self.setIconSize(QSize(20, 20)) + + +class QToolButtonVRectSmall(QToolButtonBase): + def __init__(self, Qicon, *args, **kargs): + super(QToolButtonVRectSmall, self).__init__(Qicon, *args, **kargs) + self.setIconSize(QSize(8, 20)) diff --git a/gwhat/common/widgets.py b/gwhat/common/widgets.py index edff287d6..d9ebdb1d9 100644 --- a/gwhat/common/widgets.py +++ b/gwhat/common/widgets.py @@ -282,7 +282,8 @@ def __init__(self, parent=None, resizable=False, maximize=True): else: self.__resizable = resizable self.setWindowFlags(Qt.Window | - Qt.WindowMinimizeButtonHint) + Qt.WindowMinimizeButtonHint | + Qt.WindowCloseButtonHint) self.setWindowIcon(icons.get_icon('master')) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index d8e2608f2..6ae41532b 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -30,8 +30,9 @@ # ---- Imports: Local from gwhat.colors2 import ColorsReader -from gwhat.common import StyleDB, QToolButtonNormal +from gwhat.common import StyleDB, QToolButtonNormal, QToolButtonSmall from gwhat.common import icons +from gwhat.common.icons import QToolButtonVRectSmall from gwhat.common.widgets import DialogWindow, VSep from gwhat.widgets.buttons import RangeSpinBoxes from gwhat import __namever__ @@ -62,7 +63,7 @@ def __initUI__(self): self.setWindowTitle('Weather Averages') self.setWindowIcon(icons.get_icon('master')) - # ---- TOOLBAR + # ---- Toolbar # Initialize the widgets : @@ -101,12 +102,22 @@ def __initUI__(self): self.year_rng.setRange(1800, datetime.now().year) self.year_rng.sig_range_changed.connect(self.update_normals) + btn_expand = QToolButtonVRectSmall(icons.get_icon('expand_range_vert')) + btn_expand.clicked.connect(self.expands_year_range) + btn_expand.setToolTip("Set the maximal possible year range.") + + lay_expand = QGridLayout() + lay_expand.addWidget(self.year_rng.spb_upper, 0, 0) + lay_expand.addWidget(btn_expand, 0, 1) + lay_expand.setContentsMargins(0, 0, 0, 0) + lay_expand.setSpacing(1) + qgrid = QHBoxLayout(self.year_rng) qgrid.setContentsMargins(0, 0, 0, 0) qgrid.addWidget(QLabel('Year Range :')) qgrid.addWidget(self.year_rng.spb_lower) qgrid.addWidget(QLabel('to')) - qgrid.addWidget(self.year_rng.spb_upper) + qgrid.addLayout(lay_expand) # Generate the layout of the toolbar : @@ -122,7 +133,7 @@ def __initUI__(self): subgrid_toolbar.setSpacing(5) subgrid_toolbar.setContentsMargins(0, 0, 0, 0) - # ---- MAIN GRID + # ---- Main Layout # Initialize the widgets : @@ -179,6 +190,16 @@ def set_weather_dataset(self, wxdset): np.max(wxdset['monthly']['Year'])) self.update_normals() + def expands_year_range(self): + """Sets the maximal possible year range.""" + self.year_rng.spb_upper.setValueSilently( + np.max(self.wxdset['monthly']['Year'])) + self.year_rng.spb_lower.setValueSilently( + np.min(self.wxdset['monthly']['Year'])) + self.update_normals() + + # ---- Normals + def update_normals(self): """ Forces a replot of the normals and an update of the table with the @@ -191,8 +212,6 @@ def update_normals(self): # Update the values in the table : self.grid_weather_normals.populate_table(self.normals) - # ---- Normals - def calcul_normals(self): """ Calcul the normal values of the weather dataset for the currently diff --git a/gwhat/ressources/icons_png/expand_bottom_left.png b/gwhat/ressources/icons_png/expand_bottom_left.png new file mode 100644 index 0000000000000000000000000000000000000000..a72d1f4ffe2d3012f975f86fe7af0338dc7301fa GIT binary patch literal 1977 zcmb`Ii&qnO7RT=lBbivl3_%bN3t@<)QDjp^m-=c969^!bqEJM%_yAJqYD5%@Xop2) z!rP)j5rI}wl=2WzID$6kXiI&dCf9RYupWnUr_x;_uzu&nt zqYV!7oi}&UTmYEor(!|?Fl1stTB0E`p)v*y#D0a}W)fYg;?s=K)w?PRMKDRJg=G_dHuTo z=h;V{7eAl-$ss%@RzHj5H3x0$F zQr3wm*1|ZG%z&xu?F$Y@b{suNVNV}@%yY%-(vRmU2}7RG8e588vv5LX*Xg;gZ%@;# z4c7dul2^}h^BLH)>W_UKw6+;DU2sjhl`mqhI}b5qW19#c*L{;I!Zm)sx(YC_qA9dX zdN0rngz(T-R1Y3!gLcdGmkfKt{cSugUv2k3(4LyUNEW0m{;r3nowVQ(K3=5-Y53Ui zGWtKhzmJ-nX?>-;sXc6B=nPBB!~yS*(E2*}$x#lx>+d&7aH}@DKbMr*2BaocanpvG zHv|1zxY3|**>W-j<-5_)+9JfR$L{;n6;Et`JoY(Z)Mc!*qRInwQyPXH3-7vRgI(Y0 zHy=$Ln{zgk?3i=!)3Q+Mt49Z0-iHzDlfRVoETPxiViD2O0SWGTPvL|#=MP=wsb11J zMU^R;OKYr?ExfLUGO5kmYk$t>E}3K^XhT|W7!qK`BADXf317aAmWSfOWn^$JG;Sa& z70~N~JD!C`CE>LMQ$JJ^H|J4NF?h#?Q084Yaq0{-#;*+Q^riJBMEQ=26BVRPf~iZ) zG@^sB$Baiw84ojZq(+T*I8cTHqTHL-|3JzwdMFLAUGNbVtUl8sB6m^TBx z>6(>TR9L5H_4b>oGn*)NA;?++sOe3kTapVwV>>`iM-sg>IOdX#zJOjlbv9@ck=$QO zcK}vyN2yr|E+jX*L6;l8fDg+AY5oKo_LIM~thQ(Pvu-D`5G!9?#U;DrZLZMhL*%JI zeVlaELdWG$X6=!2LmGJ>g`N$)e8~_oVrUF9b5c&k&@ibWVni^>5Y`*P*p3u0Ck6)x zo#W;&$FXwXs;`TWxZ!Vwkg^!(A=yU4gcb%7CK2SwiGyOuQ4j}_%@AWMSeQ%prGe~6 zbYeG6qG}=dTrIvZI6!MwU{SkKrkjGaVLqJOnx&s2SSy`GBKayy-$Jmq{?d=AB&J~|aCF0%KNw`XiW9Ym zveCqgM0@n)TSH5595Q~${?mj5%sVC10W{cTL1t6 literal 0 HcmV?d00001 diff --git a/gwhat/ressources/icons_png/expand_range_vert.png b/gwhat/ressources/icons_png/expand_range_vert.png new file mode 100644 index 0000000000000000000000000000000000000000..a5a9877f2f87138a1fde3f6bbfff00f07892b4b6 GIT binary patch literal 13018 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 + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/gwhat/ressources/icons_scalable/expand_range_vert.svg b/gwhat/ressources/icons_scalable/expand_range_vert.svg new file mode 100644 index 000000000..9ab9f8bd1 --- /dev/null +++ b/gwhat/ressources/icons_scalable/expand_range_vert.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + From a17733b5b2f9a2fd11effa8866280e2720203891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 12 Jan 2018 09:54:58 -0500 Subject: [PATCH 21/29] Make table more pretty --- gwhat/meteo/weather_viewer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 6ae41532b..b69206375 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -853,15 +853,16 @@ def initUI(self): self.setFrameStyle(StyleDB().frame) self.setShowGrid(False) self.setAlternatingRowColors(True) -# self.setMinimumWidth(650) - - # ------------------------------------------------------- Header -- HEADER = ('JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC', 'YEAR') self.setColumnCount(len(HEADER)) self.setHorizontalHeaderLabels(HEADER) + self.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed) + self.horizontalHeader().setHighlightSections(False) + self.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) + self.verticalHeader().setHighlightSections(False) self.setRowCount(7) self.setVerticalHeaderLabels(['Daily Tmax (°C)', 'Daily Tmin (°C)', From e53207c6b5d64551dc910f4b2ed41df48f2d4f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 12 Jan 2018 09:55:49 -0500 Subject: [PATCH 22/29] Updated tooltip and made window non resizable --- gwhat/meteo/weather_viewer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index b69206375..2ce0ab4a4 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -48,7 +48,7 @@ class WeatherViewer(DialogWindow): various stats about the dataset, etc... """ def __init__(self, parent=None): - super(WeatherViewer, self).__init__(parent) + super(WeatherViewer, self).__init__(parent, False, False) self.wxdset = None self.normals = None @@ -93,7 +93,8 @@ def __initUI__(self): "QToolButton::menu-indicator {image: none;}") btn_showStats = QToolButtonNormal(icons.get_icon('showGrid')) - btn_showStats.setToolTip('Show monthly weather normals data table.') + btn_showStats.setToolTip( + "Show the monthly weather normals data table.") btn_showStats.clicked.connect(self.show_monthly_grid) # Instantiate and define a layout for the year range widget : From 5fad844fb5fc39f00675c5a7c9cd06e7c36a5b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 12 Jan 2018 09:56:06 -0500 Subject: [PATCH 23/29] Imports --- gwhat/meteo/weather_viewer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 2ce0ab4a4..15ee4780b 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -25,7 +25,8 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import (QMenu, QToolButton, QGridLayout, QWidget, QFileDialog, QApplication, QTableWidget, - QTableWidgetItem, QLabel, QHBoxLayout) + QTableWidgetItem, QLabel, QHBoxLayout, + QHeaderView) # ---- Imports: Local From d78a1599fcf84b076475c0c16edde1346d699f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 12 Jan 2018 10:20:20 -0500 Subject: [PATCH 24/29] Stretch the columns and rows --- gwhat/meteo/weather_viewer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 15ee4780b..937cf1c5a 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -861,9 +861,9 @@ def initUI(self): self.setColumnCount(len(HEADER)) self.setHorizontalHeaderLabels(HEADER) - self.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed) + self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.horizontalHeader().setHighlightSections(False) - self.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) + self.verticalHeader().setSectionResizeMode(QHeaderView.Stretch) self.verticalHeader().setHighlightSections(False) self.setRowCount(7) From baf66c89e408d36e70199089bf2a83f8c629b721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 12 Jan 2018 10:23:36 -0500 Subject: [PATCH 25/29] Remove the call to resize to contents --- gwhat/meteo/weather_viewer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 937cf1c5a..cbcb942ce 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -957,8 +957,6 @@ def populate_table(self, NORMALS): item.setTextAlignment(Qt.AlignCenter) self.setItem(row, 12, item) - self.resizeColumnsToContents() - # %% if __name__ == '__main__' From f8cd8e1aed21290dae4a54459f9819f04d13c962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Fri, 12 Jan 2018 10:51:52 -0500 Subject: [PATCH 26/29] Make the height of the table not hard coded --- gwhat/meteo/weather_viewer.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index cbcb942ce..80c2a3835 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -164,13 +164,15 @@ def __initUI__(self): def show_monthly_grid(self): if self.grid_weather_normals.isHidden(): self.grid_weather_normals.show() - self.setFixedHeight(self.size().height()+250) -# self.setFixedWidth(self.size().width()+75) + self.setFixedHeight(self.size().height() + + self.layout().verticalSpacing() + + self.grid_weather_normals.calcul_height()) self.sender().setAutoRaise(False) else: self.grid_weather_normals.hide() - self.setFixedHeight(self.size().height()-250) -# self.setFixedWidth(self.size().width()-75) + self.setFixedHeight(self.size().height() - + self.layout().verticalSpacing() - + self.grid_weather_normals.calcul_height()) self.sender().setAutoRaise(True) def set_lang(self, lang): @@ -863,8 +865,6 @@ def initUI(self): self.setHorizontalHeaderLabels(HEADER) self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.horizontalHeader().setHighlightSections(False) - self.verticalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.verticalHeader().setHighlightSections(False) self.setRowCount(7) self.setVerticalHeaderLabels(['Daily Tmax (°C)', 'Daily Tmin (°C)', @@ -872,6 +872,10 @@ def initUI(self): 'Snow (mm)', 'Total Precip (mm)', 'ETP (mm)']) + self.resizeRowsToContents() + self.verticalHeader().setSectionResizeMode(QHeaderView.Fixed) + self.verticalHeader().setHighlightSections(False) + def populate_table(self, NORMALS): # ---- Air Temperature ---- @@ -957,6 +961,12 @@ def populate_table(self, NORMALS): item.setTextAlignment(Qt.AlignCenter) self.setItem(row, 12, item) + def calcul_height(self): + h = self.horizontalHeader().height() + 2*self.frameWidth() + for i in range(self.rowCount()): + h += self.rowHeight(i) + return h + # %% if __name__ == '__main__' From ec94d74477b19115a9a9c3bef7fbc7c30829f0e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Sat, 13 Jan 2018 14:08:44 -0500 Subject: [PATCH 27/29] :art: Some code style changes --- gwhat/meteo/weather_reader.py | 8 ++++---- gwhat/meteo/weather_viewer.py | 11 ++++++----- gwhat/projet/reader_projet.py | 26 +++++++++++++------------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/gwhat/meteo/weather_reader.py b/gwhat/meteo/weather_reader.py index eeb312727..ba10c0e87 100644 --- a/gwhat/meteo/weather_reader.py +++ b/gwhat/meteo/weather_reader.py @@ -102,22 +102,22 @@ def __init__(self, filename, *args, **kwargs): self['Missing Tavg'] = load_weather_log(finfo, 'Mean Temp (deg C)') self['Missing Ptot'] = load_weather_log(finfo, 'Total Precip (mm)') - # ---- format data - - print('Make daily time series continuous.') + # ---- Format Data time = copy(self['Time']) date = [copy(self['Year']), copy(self['Month']), copy(self['Day'])] vbrs = ['Tmax', 'Tavg', 'Tmin', 'Ptot', 'Rain', 'PET'] data = [self[x] for x in vbrs] + # Make daily time series continuous : + time, date, data = make_timeserie_continuous(self['Time'], date, data) self['Time'] = time self['Year'], self['Month'], self['Day'] = date[0], date[1], date[2] for i, vbr in enumerate(vbrs): self[vbr] = data[i] - print('Fill missing with estimated values.') + # Fill missing with estimated values : for vbr in ['Tmax', 'Tavg', 'Tmin', 'PET']: self[vbr] = fill_nan(self['Time'], self[vbr], vbr, 'interp') diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 80c2a3835..555d39ec6 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -878,7 +878,7 @@ def initUI(self): def populate_table(self, NORMALS): - # ---- Air Temperature ---- + # ---- Air Temperature for row, key in enumerate(['Tmax', 'Tmin', 'Tavg']): # Months @@ -895,7 +895,7 @@ def populate_table(self, NORMALS): item.setTextAlignment(Qt.AlignCenter) self.setItem(row, 12, item) - # ---- Rain ---- + # ---- Rain row = 3 # Months @@ -912,7 +912,7 @@ def populate_table(self, NORMALS): item.setTextAlignment(Qt.AlignCenter) self.setItem(row, 12, item) - # ---- Snow ---- + # ---- Snow row = 4 # Months @@ -930,7 +930,7 @@ def populate_table(self, NORMALS): item.setTextAlignment(Qt.AlignCenter) self.setItem(row, 12, item) - # ---- Total Precip ---- + # ---- Total Precip row = 5 # Months @@ -946,9 +946,10 @@ def populate_table(self, NORMALS): item.setTextAlignment(Qt.AlignCenter) self.setItem(row, 12, item) - # ---- ETP ---- + # ---- ETP row = 6 + # Months for col in range(12): item = QTableWidgetItem('%0.1f' % NORMALS['PET'][col]) item.setFlags(item.flags() & ~Qt.ItemIsEditable) diff --git a/gwhat/projet/reader_projet.py b/gwhat/projet/reader_projet.py index 89dba3105..22df3b1f2 100644 --- a/gwhat/projet/reader_projet.py +++ b/gwhat/projet/reader_projet.py @@ -106,10 +106,11 @@ def convert_projet_format(self, filename): def close_projet(self): try: self.db.close() - except: - pass # projet is None or already closed + except AttributeError: + # projet is None or already closed. + pass - # ========================================================================= + # ---- Project Properties @property def name(self): @@ -169,7 +170,7 @@ def lon(self): def lon(self, x): self.db.attrs['longitude'] = x - # ======================================================== water level ==== + # ---- Water Levels Dataset Handlers @property def wldsets(self): @@ -232,7 +233,7 @@ def add_wldset(self, name, df): self.db.flush() print('New dataset created sucessfully') - except: + except Exception: print('Unable to save dataset to project db') del self.db['wldsets'][name] @@ -241,7 +242,7 @@ def add_wldset(self, name, df): def del_wldset(self, name): del self.db['wldsets/%s' % name] - # =========================================================== weather ===== + # ---- Weather Dataset Handlers @property def wxdsets(self): @@ -474,12 +475,11 @@ def get_layout(self): return layout -# ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: - - class WXDataFrameHDF5(dict): - # This is a wrapper around the h5py group that is used to mimick the - # structure of WXDataFrame in meteo_utils. + """ + This is a wrapper around the h5py group that is used to mimick the + structure of WXDataFrame in gwhat.meteo.weather_reader. + """ def __init__(self, dset, *args, **kwargs): super(WXDataFrameHDF5, self).__init__(*args, **kwargs) self.dset = dset @@ -508,6 +508,6 @@ def name(self): if __name__ == '__main__': - f = 'C:/Users/jnsebgosselin/Desktop/testé/testé.what' - f = 'C:/Users/jsgosselin/OneDrive/WHAT/WHAT/tests/Example.what' + f = ("C:/Users/jsgosselin/GWHAT/" + "gwhat/tests/@ new-prô'jèt!/@ new-prô'jèt!.gwt") pr = ProjetReader(f) From 9e8054f8b17122c14f6463ab1af38be3ff56516a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Sat, 13 Jan 2018 14:14:34 -0500 Subject: [PATCH 28/29] Added 'Period' key to WXDataFrame --- gwhat/meteo/weather_reader.py | 6 +++++- gwhat/projet/reader_projet.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/gwhat/meteo/weather_reader.py b/gwhat/meteo/weather_reader.py index ba10c0e87..fbb28aed3 100644 --- a/gwhat/meteo/weather_reader.py +++ b/gwhat/meteo/weather_reader.py @@ -85,7 +85,8 @@ def __init__(self, filename, *args, **kwargs): 'Ptot': np.array([]), 'Rain': None, 'Snow': None, - 'PET': None} + 'PET': None, + 'Period': (None, None)} # ---- Import primary data @@ -117,6 +118,9 @@ def __init__(self, filename, *args, **kwargs): for i, vbr in enumerate(vbrs): self[vbr] = data[i] + self['normals']['Period'] = (np.min(self['Year']), + np.max(self['Year'])) + # Fill missing with estimated values : for vbr in ['Tmax', 'Tavg', 'Tmin', 'PET']: diff --git a/gwhat/projet/reader_projet.py b/gwhat/projet/reader_projet.py index 22df3b1f2..098f47d91 100644 --- a/gwhat/projet/reader_projet.py +++ b/gwhat/projet/reader_projet.py @@ -491,6 +491,11 @@ def __getitem__(self, key): x = {} for vrb in self.dset[key].keys(): x[vrb] = self.dset[key][vrb].value + if key == 'normals' and 'Period' not in x.keys(): + # This is needed for backward compatibility with + # gwhat < 0.2.3 (see PR#142). + x['Period'] = (np.min(self.dset['Year']), + np.max(self.dset['Year'])) return x elif key == 'daily': vrbs = ['Year', 'Month', 'Day', 'Tmin', 'Tavg', 'Tmax', From 08d3da9dc6954d38768d764ab9533235d9b34903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Gosselin?= Date: Sat, 13 Jan 2018 15:14:17 -0500 Subject: [PATCH 29/29] :wrench: clean imports --- gwhat/meteo/weather_viewer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gwhat/meteo/weather_viewer.py b/gwhat/meteo/weather_viewer.py index 555d39ec6..592f0696f 100644 --- a/gwhat/meteo/weather_viewer.py +++ b/gwhat/meteo/weather_viewer.py @@ -31,9 +31,9 @@ # ---- Imports: Local from gwhat.colors2 import ColorsReader -from gwhat.common import StyleDB, QToolButtonNormal, QToolButtonSmall +from gwhat.common import StyleDB from gwhat.common import icons -from gwhat.common.icons import QToolButtonVRectSmall +from gwhat.common.icons import QToolButtonVRectSmall, QToolButtonNormal from gwhat.common.widgets import DialogWindow, VSep from gwhat.widgets.buttons import RangeSpinBoxes from gwhat import __namever__