From 7faacc6e86c06c299abe3a3bd1edb0db2d36d3cf Mon Sep 17 00:00:00 2001 From: Daniel Langbein Date: Sat, 5 Dec 2020 13:56:12 +0100 Subject: [PATCH] rotate date-labels of plot; show ticks for days and 6-hour intervals --- src/langfingaz/plotMeetings.py | 48 +++++++++++++++++++++++++++------- src/langfingaz/util/util.py | 44 +++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/src/langfingaz/plotMeetings.py b/src/langfingaz/plotMeetings.py index 6aec3ea..7ff42b4 100644 --- a/src/langfingaz/plotMeetings.py +++ b/src/langfingaz/plotMeetings.py @@ -1,11 +1,15 @@ from pathlib import Path from typing import List -import matplotlib.pyplot as pyplot +from datetime import date + +import matplotlib.pyplot as plt +import matplotlib.dates as mdates from langfingaz import loadData from langfingaz import parseMeetings from langfingaz.parseMeetings import BbbStatus, Meeting from langfingaz.util import fileUtil +from langfingaz.util import util def getDefaultPlotFolder() -> Path: @@ -40,25 +44,49 @@ def doPlotMeetings(bbbStati: List[BbbStatus]) -> Path: videos.append(bbbStatus.videoCount) voices.append(bbbStatus.voiceParticipantCount) - # Note that even in the OO-style, we use `.pyplot.figure` to create the figure. - _a, _b = pyplot.subplots() # Create a figure and an axes. - fig: pyplot.Figure = _a + _a, _b = plt.subplots() # Create a figure and an axes. + fig: plt.Figure = _a ax = _b # of type plt.axes.Axes ?? - if not (issubclass(type(fig), pyplot.Figure)): + if not (issubclass(type(fig), plt.Figure)): raise ValueError("expected Figure") - ax.plot(time, participants, label='participants') # Plot some data on the axes. - ax.plot(time, videos, label='video') # Plot more data on the axes... - ax.plot(time, voices, label='voice') # ... and some more. + ax.plot(time, participants, label='participants') + ax.plot(time, videos, label='video') + ax.plot(time, voices, label='voice') + + # format the ticks (on x-axis) + days = mdates.DayLocator() + hours = mdates.HourLocator(interval=6) # every 6 hours one minor tick + dateFmt = mdates.DateFormatter('%Y-%m-%d') + + ax.xaxis.set_major_locator(days) + ax.xaxis.set_major_formatter(dateFmt) + ax.xaxis.set_minor_locator(hours) + + # round to nearest day + dateMin = time[0].date() # round floor + dateMax = util.roundCeilingByDay(time[-1]) # round ceiling + ax.set_xlim(dateMin, dateMax) + ax.set_xlabel('time') # Add an x-label to the axes. ax.set_ylabel('numbers') # Add a y-label to the axes. ax.set_title("BigBlueButton Statistics") # Add a title to the axes. ax.legend() # Add a legend. + # rotates and right aligns the x labels (dates), and moves the bottom of the + # axes up to make room for them + # https://matplotlib.org/api/_as_gen/matplotlib.figure.Figure.html#matplotlib.figure.Figure.autofmt_xdate + fig.autofmt_xdate() + + imgFormat = 'png' image: Path = getDefaultPlotFolder().joinpath( - "{}_until_{}".format(fileUtil.asString(time[0]), fileUtil.asString(time[-1])) + "{}_until_{}.{}".format( + fileUtil.asString(time[0]), + fileUtil.asString(time[-1]), + imgFormat + ) ) - fig.savefig(image) + fig.savefig(image, format=imgFormat) # plt.show() return image diff --git a/src/langfingaz/util/util.py b/src/langfingaz/util/util.py index b7c9b6c..65321c4 100644 --- a/src/langfingaz/util/util.py +++ b/src/langfingaz/util/util.py @@ -1,4 +1,5 @@ import time +import datetime def sleep(seconds: float): @@ -13,6 +14,49 @@ def indentMultilineStr(s: str, indentWith='\t'): return indented +# def roundFloorByDay(t) -> datetime.date: +# """ +# rounds the (date)time up to the closest full day before or equal this point of time +# (round floor) +# +# :param t: datetime.date or datetime.datetime +# :return: the closest full day before or equal to (date)time t +# """ +# +# # note: order of datetime and date type check matters +# # as datetime is a SUBCLASS of date +# if issubclass(type(t), datetime.datetime): +# return t.date() +# elif issubclass(type(t), datetime.date): +# return t +# else: +# raise ValueError("illegal argument") + + +def roundCeilingByDay(t) -> datetime.date: + """ + rounds the (date)time up to the closest full day after or equal to this point of time + (round ceiling) + + :param t: datetime.date or datetime.datetime + :return: the closest full day after or equal to (date)time t + """ + + # note: order of datetime and date type check matters + # as datetime is a SUBCLASS of date + if issubclass(type(t), datetime.datetime): + t: datetime.datetime # "cast" to datetime.datetime for IDE linting + + if t.minute == 0 and t.second == 0 and t.microsecond == 0: + return t.date() + else: + return datetime.date(year=t.year, month=t.month, day=t.day + 1) + elif issubclass(type(t), datetime.date): + return t + else: + raise ValueError("illegal argument") + + def asString(o: object): attrs = vars(o) # attributes and their values return '\n'.join("%s: %s" % item for item in attrs.items())