#!/usr/bin/env python3 # Copyright (C) 2017 Christian Thomas Jacobs. # This file is part of PyQSO. # PyQSO is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # PyQSO is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyQSO. If not, see . from gi.repository import Gtk import logging from os.path import basename, getmtime, expanduser, dirname, join, realpath from datetime import datetime, date try: import configparser except ImportError: import ConfigParser as configparser try: import matplotlib matplotlib.use('Agg') matplotlib.rcParams['font.size'] = 10.0 from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas from matplotlib.figure import Figure from matplotlib.dates import DateFormatter, MonthLocator have_matplotlib = True except ImportError as e: logging.warning(e) logging.warning("Could not import matplotlib, so you will not be able to plot annual logbook statistics. Check that all the PyQSO dependencies are satisfied.") have_matplotlib = False class Summary(object): def __init__(self, application): """ Create a summary page containing various statistics such as the number of logs in the logbook, the logbook's modification date, etc. :arg application: The PyQSO application containing the main Gtk window, etc. """ self.application = application self.logbook = self.application.logbook self.builder = self.application.builder glade_file_path = join(realpath(dirname(__file__)), "res", "pyqso.glade") self.builder.add_objects_from_file(glade_file_path, ("summary_page",)) self.summary_page = self.builder.get_object("summary_page") self.items = {} # Database name in large font at the top of the summary page self.builder.get_object("database_name").set_markup("%s" % basename(self.logbook.path)) self.items["LOG_COUNT"] = self.builder.get_object("log_count") self.items["QSO_COUNT"] = self.builder.get_object("qso_count") self.items["DATE_MODIFIED"] = self.builder.get_object("date_modified") # Yearly statistics config = configparser.ConfigParser() have_config = (config.read(expanduser('~/.config/pyqso/preferences.ini')) != []) (section, option) = ("general", "show_yearly_statistics") if(have_config and config.has_option(section, option)): if(config.getboolean("general", "show_yearly_statistics") and have_matplotlib): hbox = Gtk.HBox() label = Gtk.Label(label="Display statistics for year: ", halign=Gtk.Align.START) hbox.pack_start(label, False, False, 6) year_select = Gtk.ComboBoxText() min_year, max_year = self.get_year_bounds() if min_year and max_year: for year in range(max_year, min_year-1, -1): year_select.append_text(str(year)) year_select.append_text("") year_select.connect("changed", self.on_year_changed) hbox.pack_start(year_select, False, False, 6) self.summary_page.pack_start(hbox, False, False, 4) self.items["YEARLY_STATISTICS"] = Figure() canvas = FigureCanvas(self.items["YEARLY_STATISTICS"]) canvas.set_size_request(800, 175) canvas.show() self.summary_page.pack_start(canvas, True, True, 0) # Summary tab label and icon. tab = Gtk.HBox(homogeneous=False, spacing=0) label = Gtk.Label(label="Summary ") icon = Gtk.Image.new_from_icon_name(Gtk.STOCK_INDEX, Gtk.IconSize.MENU) tab.pack_start(label, False, False, 0) tab.pack_start(icon, False, False, 0) tab.show_all() self.logbook.notebook.insert_page(self.summary_page, tab, 0) # Append as a new tab self.logbook.notebook.show_all() return def on_year_changed(self, combo): """ Re-plot the statistics for the year selected by the user. """ # Clear figure self.items["YEARLY_STATISTICS"].clf() self.items["YEARLY_STATISTICS"].canvas.draw() # Get year to show statistics for. year = combo.get_active_text() try: year = int(year) except ValueError: # Empty year string. return # Number of contacts made each month contact_count_plot = self.items["YEARLY_STATISTICS"].add_subplot(121) contact_count = self.get_annual_contact_count(year) # x-axis formatting based on the date contact_count_plot.bar(list(contact_count.keys()), list(contact_count.values()), color="k", width=15, align="center") formatter = DateFormatter("%b") contact_count_plot.xaxis.set_major_formatter(formatter) month_locator = MonthLocator() contact_count_plot.xaxis.set_major_locator(month_locator) contact_count_plot.set_ylabel("Number of QSOs") # Set x-axis upper limit based on the current month. contact_count_plot.xaxis_date() contact_count_plot.set_xlim([date(year-1, 12, 16), date(year, 12, 15)]) # Make a bit of space either side of January and December of the selected year. # Pie chart of all the modes used. mode_count_plot = self.items["YEARLY_STATISTICS"].add_subplot(122) mode_count = self.get_annual_mode_count(year) (patches, texts, autotexts) = mode_count_plot.pie(list(mode_count.values()), labels=mode_count.keys(), autopct='%1.1f%%', shadow=False) for p in patches: # Make the patches partially transparent. p.set_alpha(0.75) mode_count_plot.set_title("Modes used") self.items["YEARLY_STATISTICS"].canvas.draw() return def get_year_bounds(self): """ Find the years of the oldest and newest QSOs across all logs in the logbook. :returns: The years of the oldest and newest QSOs. The tuple (None, None) is returned if no QSOs have been made or no QSO dates have been specified. :rtype: tuple """ c = self.logbook.connection.cursor() max_years = [] min_years = [] for log in self.logbook.logs: query = "SELECT min(QSO_DATE), max(QSO_DATE) FROM %s" % (log.name) c.execute(query) years = c.fetchone() if years[0] and years[1]: min_years.append(int(years[0][:4])) max_years.append(int(years[1][:4])) if len(min_years) == 0 or len(max_years) == 0: return None, None else: # Return the min and max across all logs. return min(min_years), max(max_years) def get_annual_contact_count(self, year): """ Find the total number of contacts made in each month in the specified year. :arg int year: The year of interest. :returns: The total number of contacts made in each month of a given year. :rtype: dict """ contact_count = {} c = self.logbook.connection.cursor() for log in self.logbook.logs: query = "SELECT QSO_DATE, count(QSO_DATE) FROM %s WHERE QSO_DATE >= %d0101 AND QSO_DATE < %d0101 GROUP by QSO_DATE" % (log.name, year, year+1) c.execute(query) xy = c.fetchall() for i in range(len(xy)): date_str = xy[i][0] y = int(date_str[0:4]) m = int(date_str[4:6]) date = datetime(y, m, 1) # Collect all contacts together by month. if date in contact_count.keys(): contact_count[date] += xy[i][1] else: contact_count[date] = xy[i][1] return contact_count def get_annual_mode_count(self, year): """ Find the total number of contacts made with each mode in a specified year. :arg int year: The year of interest. :returns: The total number of contacts made with each mode in a given year. :rtype: dict """ mode_count = {} for log in self.logbook.logs: query = "SELECT MODE, count(MODE) FROM %s WHERE QSO_DATE >= %d0101 AND QSO_DATE < %d0101 GROUP by MODE" % (log.name, year, year+1) c = self.logbook.connection.cursor() c.execute(query) xy = c.fetchall() for i in range(len(xy)): mode = xy[i][0] if mode == "": mode = "Unspecified" # Add to running total if mode in mode_count.keys(): mode_count[mode] += xy[i][1] else: mode_count[mode] = xy[i][1] return mode_count def update(self): """ Update the information presented on the summary page. """ self.items["LOG_COUNT"].set_label(str(self.logbook.log_count)) self.items["QSO_COUNT"].set_label(str(self.logbook.record_count)) try: t = datetime.fromtimestamp(getmtime(self.logbook.path)).strftime("%d %B %Y @ %H:%M") self.items["DATE_MODIFIED"].set_label(str(t)) except (IOError, OSError) as e: logging.exception(e) return