Before tackling the parsing of the other style of data file (well more a file of daily notes), I thought I’d play around with Plotly. I figured I’d see what kinds of charts/indicators I might be able to generate for use on the dashboard. Will give me a bit of a break from parsing rainfall data and is likely something I’d need to do eventually—one way or another.

Prior to this project I pretty much just used matplotlib. But Plotly will possibly save a bit of the heavy lifting that matplotlib would require to get data displayed on the dashboard.

This will likely also mean some new methods for the weather database class. E.G. to get the current month-to-date rainfall or the historical average, minimum and maximum for the specific month. That might actually be just one method. Probably something similar for year-to-date.

New Module

As I will have a variety of graphic elements for the dashboard, I am going to create a new module. It will contain the various functions I will write to generate a specific chart or indicator. I will test my functions in this module. Which will likely require importing at least the database class module.

Here’s the initial setup.

# db_charts.py: module for dashboard chart/indicator functions
# ver: 0.1.0, 2025.08.02, rek, init version

import plotly.graph_objects as go
import pandas as pd

if __name__ == "__main__":
  from pathlib import Path
  from utils.weather_db import Weather_db

def rf_mon2dt(mon2dt, m_min, m_avg, m_max):
  pass

if __name__ == "__main__":
  # instantiate database class using empty database file
  cwd = Path(__file__).cwd()
  fl_pth = cwd/"data"
  fl_nm = "weather.db"
  db_pth = fl_pth/fl_nm
  rfall = Weather_db(db_pth)

  # let's generate a gauge for month to date rainfall
  # it will also show the min, avg and max for the specified month

For now I am going to execute a query, in the module test code, to get the data I need for the gauge. Well and a bit of hardcoded data for the current month-to-date rainfall. Will use data for June 2025. We haven’t yet had any rain in August and only two days of light rain in July (I started working on this draft August 2nd).

Once again I will use logical variables to control my test/playing around code. And, playing around there was. No new functions or methods just yet. But, figured I’d show you the dev code and the output. I put output in italics because Plotly doesn’t directly allow displaying images in a terminal window. The charts it generates are meant to be interactive, so they, in my case, need to be displayed in a browser window. For the post I used screen capture to get pngs I could use here.

Month-to-date Rain Gauge

I wanted to use a semi-circular gauge to display the current month-to-date rainfall. And, I want some way to indicate the average, minimum and maximum historical values for the month in question. Plotly really did make that fairly easy. Though it took me a while to sort out sizing plots and to select some colours I liked. There were a few other bugs I had to sort as well. The result was likely an hour or more of playing around (I wouldn’t call it effort).

On the gauge, the end of the light blue curve, before it turns into the darker blue, marks the historical minimum and the maximum for the darker blue area before it turns white. The historical average is marked by the red line. The larger, dark blue number is the current month-to-date rainfall. The smaller red number is where the month-to-date is in relation to the historical average. In this case, it is 13.4mm less.

if __name__ == "__main__":
  do_mh_gauge = True
... ...
# june 2025 rainfall data
  rf_25_06 = [
    ("2025.06.20 08:00", 2.0, 2.0),
    ("2025.06.21 08:00", 32.0, 34.0),
    ("2025.06.22 08:00", 2.0, 36.0),
    ("2025.06.27 08:00", 2.5, 38.5),
  ]

  if do_mh_gauge:
    # let's generate a gauge for month to date rainfall
    # it will also show the min, avg and max for the specified month
    # For now I will use local queries or data to get the info needed for the gauge
    c_mon = "06"
    c_yr = "2025"

    q_hist = f"""SELECT month, ROUND(avg, 2), min, max FROM {rfall.tnms["mh_tnm"]}
      WHERE month='{c_mon}'"""
    rslt = rfall.qry_exec(q_hist)
    print(rslt)
    
    m_rec = 3

    g_val = rf_25_06[m_rec][2]
    g_avg, g_min, g_max = rslt[0]
    g_rng = int(((g_max * 1.2) // 10) * 10)

    fig = go.Figure(go.Indicator(
      domain = {'x': [0, 1], 'y': [0, 1]},
      value = g_val,
      mode = "gauge+number+delta",
      title = {'text': "June Rainfall Month-to-date", 'font': {'size': 36}},
      delta = {'reference': g_avg, 'increasing': {'color': "royalblue"}},
      number = {'font': {'size': 54}},
      gauge = {
        'axis': {'range': [None, g_rng], 'tickwidth': 1, 'tickcolor': "darkblue"},
        'bar': {'color': "darkblue"},
        'bgcolor': "white",
        'borderwidth': 2,
        'bordercolor': "gray",
        'steps' : [
            {'range': [0, g_min], 'color': "lightblue"},
            {'range': [g_min, g_max], 'color': "royalblue"}],
        'threshold' : {'line': {'color': "red", 'width': 4}, 'thickness': 0.75, 'value': g_avg}}))

    fig.update_layout(autosize=False, width=600, height=400,
        margin=dict(l=25, r=15, b=0, t=20, pad=0),
        paper_bgcolor = "#dddddd", font = {'color': "darkblue", 'family': "Arial"})

    fig.show(renderer="browser")

In the terminal I got the following; the historical values for June.

(dbd-3.13) PS R:\learn\dashboard\utils> python db_charts.py
[('06', 51.85, 5.5, 81.89999999999999)]

And, in a browser window/tab I got the following gauge/chart/image.

Month-to-Date Rainfall Gauge
Plotly generated month-to-date rainfall gauge

Year-to-date Rainfall Gauge

Well, thought that worked well enough that I’d play around doing the same for the year-to-date rainfall.

I manually put together the year-to-date rainfall data for 2025.

if __name__ == "__main__":
  do_mh_gauge = False
  do_yh_gauge = True
... ...
rf_2025 = [
    ("2025.01.31 24:00", 21.0, 81.5),
    ("2025.02.26 08:00", 16.5, 138.5),
    ("2025.03.31 08:00", 4.0, 243.5),
    ("2025.04.29 08:00", 10.0, 72.0),
    ("2025.05.30 08:00", 8.0, 62.5),
    ("2025.06.27 08:00", 2.5, 38.5),
    ("2025.07.10 08:00", 7.0, 18.5),
  ]
... ...
  if do_yh_gauge:
    q_yrly = f"""SELECT AVG(totYear) AS avg_yr, MIN(totYear) AS min_yr, MAX(totYear) AS max_yr
      FROM
      (SELECT datetime, substr(datetime, 1, 4) as year, SUM(daily) as totYear
      FROM  {rfall.tnms["rf_tnm"]}
      WHERE year > '2014'
      GROUP BY year);"""
    rslt = rfall.qry_exec(q_yrly)
    print(rslt)

    tot_25 = 0
    for _, _, m_tot in rf_2025:
      tot_25 += m_tot
    print(tot_25)

    g_val = tot_25
    g_avg, g_min, g_max = rslt[0]
    g_rng = int(((g_max * 1.2) // 10) * 10)

    fig = go.Figure(go.Indicator(
      domain = {'x': [0, 1], 'y': [0, 1]},
      value = g_val,
      mode = "gauge+number+delta",
      title = {'text': "2025 June Rainfall Year-to-date", 'font': {'size': 36}},
      delta = {'reference': g_avg, 'increasing': {'color': "royalblue"}},
      number = {'font': {'size': 54}},
      gauge = {
        'axis': {'range': [None, g_rng], 'tickwidth': 1, 'tickcolor': "darkblue"},
        'bar': {'color': "darkblue"},
        'bgcolor': "white",
        'borderwidth': 2,
        'bordercolor': "gray",
        'steps' : [
            {'range': [0, g_min], 'color': "lightblue"},
            {'range': [g_min, g_max], 'color': "royalblue"}],
        'threshold' : {'line': {'color': "red", 'width': 4}, 'thickness': 0.75, 'value': g_avg}}))

    fig.update_layout(autosize=False, width=600, height=400,
        margin=dict(l=25, r=15, b=0, t=20, pad=0),
        paper_bgcolor = "#dddddd", font = {'color': "darkblue", 'family': "Arial"})

    fig.show(renderer="browser")

In the terminal I get the following.

(dbd-3.13) PS R:\learn\dashboard\utils> python db_charts.py
[(1756.95, 1457.8, 1953.65)]
655.0

And in the browser I got the following gauge.

Year-to-Date Rainfall Gauge
Plotly generated year-to-date rainfall gauge

Refactor, Add Functions, etc.

Okay, time to eliminate all that duplication. And, perhaps to add methods to the Weather_db class to get the bits of data needed for the gauges.

rf_gauge()

Let’s start with a function to generate the gauge. Bit of info needed. Month or year for the title. Though, outside of development, that can likely be obtained using Python’s time functions. And, I will allow for that. I am going to stick with the colours and such. So only need to pass in the pertinent data for the particular month or year. And, I don’t want any queries to be used in the gauge function (“single responsibility principle”?).

I messed around with some obtuse code to get the full month name. Likely should just have used a look-up table, but…

And, for now, no new functions/methods to get the historical data from the database.

... ...
import time
... ...
def rf_gauge(r_prd, r_todt, r_avg, r_min, r_max, do_mon=True):
  """Generate and return figure for gauge showing rainfall to-date for some period,
     month or year.

    Params:
      r_prd: period to plot, year (e.g. '2025') or month (e.g. 'June'), string
      r_todt: amount of rainfall to-date for the specified period, float
      r_avg: historical average rainfall for period
      r_min: historical minimum rainfall for period
      r_max: historical maximum rainfall for period
      do_mon: if true period is month, else period is year
  """
  g_ttl = f"{r_prd} Rainfall {'Month' if do_mon else 'Year'}-to-date"
  g_rng = int(((r_max * 1.2) // 10) * 10)

  fig = go.Figure(go.Indicator(
    domain = {'x': [0, 1], 'y': [0, 1]},
    value = r_todt,
    mode = "gauge+number+delta",
    title = {'text': g_ttl, 'font': {'size': 36}},
    delta = {'reference': r_avg, 'increasing': {'color': "royalblue"}},
    number = {'font': {'size': 54}},
    gauge = {
      'axis': {'range': [None, g_rng], 'tickwidth': 1, 'tickcolor': "darkblue"},
      'bar': {'color': "darkblue"},
      'bgcolor': "white",
      'borderwidth': 2,
      'bordercolor': "gray",
      'steps' : [
          {'range': [0, r_min], 'color': "lightblue"},
          {'range': [r_min, r_max], 'color': "royalblue"}],
      'threshold' : {'line': {'color': "red", 'width': 4}, 'thickness': 0.75, 'value': r_avg}}))

  fig.update_layout(autosize=False, width=600, height=400,
      margin=dict(l=25, r=15, b=0, t=20, pad=0),
      paper_bgcolor = "#dddddd", font = {'color': "darkblue", 'family': "Arial"})

  return fig
... ...
if __name__ == "__main__":
  do_mh_gauge = False
  do_yh_gauge = False
  do_g_func = True
... ...
  if do_g_func:
    do_mn_2dt = False
    do_yr_2dt = False
    
    if do_mn_2dt:
      # let's generate a gauge for month to date rainfall using the new function
      # For now I will use local queries or data to get the info needed for the gauge
      c_mon = "06"
      c_yr = "2025"

      # refactored to limit all monthly values to 2 decimals
      s_mon = time.strftime("%B", time.strptime(f"{c_yr} {c_mon} 01", "%Y %m %d"))
      print(s_mon)
      q_hist = f"""SELECT month, ROUND(avg, 2), ROUND(min, 2), ROUND(max, 2) FROM {rfall.tnms["mh_tnm"]}
        WHERE month='{c_mon}'"""
      r_mo, r_av, r_mn, r_mx = rfall.qry_exec(q_hist)[0]
      print(f"rslt: {r_mo}, {r_av}, {r_mn}, {r_mx}")

      m_rec = 3
      g_val = rf_25_06[m_rec][2]

      fig = rf_gauge(s_mon, g_val, r_av, r_mn, r_mx)
      fig.show(renderer="browser")

    if do_yr_2dt:
      # let's generate a gauge for year to date rainfall using the new function
      # For now I will use local queries or data to get the info needed for the gauge
      b_yr = "2015"   # 2014 missing first two months
      c_yr = "2025"

      q_yrly = f"""SELECT AVG(totYear) AS avg_yr, MIN(totYear) AS min_yr, MAX(totYear) AS max_yr
        FROM
        (SELECT datetime, substr(datetime, 1, 4) as year, SUM(daily) as totYear
        FROM  {rfall.tnms["rf_tnm"]}
        WHERE year >= '{b_yr}' AND year < '{c_yr}'
        GROUP BY year);"""
      rslt = rfall.qry_exec(q_yrly)
      print(rslt)

      tot_25 = 0
      for _, _, m_tot in rf_2025:
        tot_25 += m_tot
      print(tot_25)

      g_val = tot_25
      r_av, r_mn, r_mx = rslt[0]

      fig = rf_gauge(c_yr, g_val, r_av, r_mn, r_mx, do_mon=False)
      fig.show(renderer="browser")

In the terminal, I got the following.

(dbd-3.13) PS R:\learn\dashboard\utils> python db_charts.py
June
rslt: 06, 51.85, 5.5, 81.9
[(1756.95, 1457.8, 1953.65)]
655.0

And, in two separate browser tabs I got the same images we obtained earlier in the post. But, you’ll just have to take my word for that.

Database Queries

I am not sure exactly what to do about the two queries I am using above. Add methods to database class? Add functions to graph module? Just have them in the code where needed? The latter may prove not to be DRY down the road. So, I think the last suggestion is out. It will also make for tidier code in the dashboard module.

And, they really are not directly related to what the graph module is meant to provide. So, in the database class it is.

I added the following to the initialization method.

    # incomplete year not to be used when generating historical data
    # on a yearly basis 
    self.x_yr = '2014'

And, then the new method. Might be doing a little more than necessary, but I don’t think it is too complicated.

  def get_history(self, s_prd, do_mon=True):
    """Get historical data for a given month or years.

      Params:
        s_prd: period label, string (e.g. 'June' if do_mon is True
          '2025' if do_mon is False). In the case of do_mon, this is
          the month for which we want the historical data. For yearly
          situation, this is the current year and should be excluded
          from the historical data being retrieved from the database.
        do_mon: getting historical data for month if True,
                for years otherwise
        
        returns: tuple of historical data for specified period, (avg, min, max)
    """
    if do_mon:
      q_hist = f"""SELECT ROUND(avg, 2), ROUND(min, 2), ROUND(max, 2) FROM {self.tnms["mh_tnm"]}
        WHERE month='{s_prd}';"""
    else:
      q_hist = f"""SELECT ROUND(AVG(totYear), 2) AS avg_yr, MIN(totYear) AS min_yr, MAX(totYear) AS max_yr
        FROM
        (SELECT substr(datetime, 1, 4) as year, ROUND(SUM(daily), 2) as totYear
        FROM  {self.tnms["rf_tnm"]}
        WHERE year > '{self.x_yr}' AND year < '{s_prd}'
        GROUP BY year);"""
    rslt = self.qry_exec(q_hist)
    return rslt[0]

Refactor Tests

Okay, let’s have a look at putting that method to work.

  if do_g_func:
    do_mn_2dt = False
    do_yr_2dt = False
    
    if do_mn_2dt:
      # let's generate a gauge for month to date rainfall using the new function
      # For now I will use local queries or data to get the info needed for the gauge
      c_mon = "06"
      c_yr = "2025"

      s_mon = time.strftime("%B", time.strptime(f"{c_yr} {c_mon} 01", "%Y %m %d"))
      print(s_mon)
      r_av, r_mn, r_mx = rfall.get_history(c_mon)
      print(f"rslt: {r_av}, {r_mn}, {r_mx}")

      m_rec = 3
      g_val = rf_25_06[m_rec][2]

      fig = rf_gauge(s_mon, g_val, r_av, r_mn, r_mx)
      fig.show(renderer="browser")

    if do_yr_2dt:
      # let's generate a gauge for year to date rainfall using the new function
      # For now I will use local queries or data to get the info needed for the gauge
      c_yr = "2025"

      r_av, r_mn, r_mx  = rfall.get_history(c_yr, do_mon=False)
      print(f"rslt: {r_av}, {r_mn}, {r_mx}")

      tot_25 = 0
      for _, _, m_tot in rf_2025:
        tot_25 += m_tot
      print(tot_25)

      fig = rf_gauge(c_yr, tot_25, r_av, r_mn, r_mx, do_mon=False)
      fig.show(renderer="browser")

And, I assure you that works exactly as the various versions of code above.

This One Done

I had thought I’d look at another plot or two. I was thinking I would, on the dashboard, also show a bar chart of the rainfall for each of the two situations. For the current month to-date display, a stacked barchart showing the daily rainfall for that month for the prior 5 to 10 years. And, for the year to-date scenario, a stacked bar chart showing the monthly rainfall for each of the past 5 to 10 years.

But, I am thinking this post has covered one thing reasonably well. And is of a decent length with a reasonable amount of meaningful content.

So until next time, do play with your code—always an opportunity to discover or learn something new.

Resources