Files
AvianVisitors/scripts/daily_plot.py
T
cdkl 0448eb8134 Remote utc to localtime conversion from daily_plot.py
Introduction of yesterday/today handling added a bug that expresses itself on systems whose timezones are behind UTC. Specifically, the 'localtime' modifier to DATE() attempts to interpret the date to the left as UTC, and convert to localtime. The result is that in timezones behind UTC, the date selected is always 1 earlier than intended.

No timezone conversion should be done. The db data is in local time already.

Here's a demonstration of calling DATE() in various ways in North America EST:

sqlite> select DATE('now');
2024-02-26
sqlite> select DATETIME('now');
2024-02-26 17:20:46
sqlite> select DATE('2024-02-26');
2024-02-26
sqlite> select DATETIME('2024-02-26');
2024-02-26 00:00:00
sqlite> select DATE('now', 'localtime');
2024-02-26
sqlite> select DATE('2024-02-26', 'localtime');
2024-02-25   <--- this is the bug
2024-02-26 12:25:10 -05:00

157 lines
5.4 KiB
Python
Executable File

import argparse
import os
import sqlite3
import textwrap
from datetime import datetime
from time import sleep
import matplotlib.font_manager as font_manager
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from matplotlib import rcParams
from matplotlib.colors import LogNorm
from utils.helpers import DB_PATH
def get_data(now=None):
conn = sqlite3.connect(DB_PATH)
if now is None:
now = datetime.now()
df = pd.read_sql_query(f"SELECT * from detections WHERE Date = DATE('{now.strftime('%Y-%m-%d')}')",
conn)
# Convert Date and Time Fields to Panda's format
df['Date'] = pd.to_datetime(df['Date'])
df['Time'] = pd.to_datetime(df['Time'], unit='ns')
# Add round hours to dataframe
df['Hour of Day'] = [r.hour for r in df.Time]
return df, now
def create_plot(df_plt_today, now, is_top):
readings = 10
if is_top:
plt_selection_today = (df_plt_today['Com_Name'].value_counts()[:readings])
else:
plt_selection_today = (df_plt_today['Com_Name'].value_counts()[-readings:])
df_plt_selection_today = df_plt_today[df_plt_today.Com_Name.isin(plt_selection_today.index)]
# Set up plot axes and titles
f, axs = plt.subplots(1, 2, figsize=(10, 4), gridspec_kw=dict(width_ratios=[3, 6]), facecolor='#77C487')
plt.subplots_adjust(left=None, bottom=None, right=None, top=None, wspace=0, hspace=0)
# generate y-axis order for all figures based on frequency
freq_order = pd.value_counts(df_plt_selection_today['Com_Name']).index
# make color for max confidence --> this groups by name and calculates max conf
confmax = df_plt_selection_today.groupby('Com_Name')['Confidence'].max()
# reorder confmax to detection frequency order
confmax = confmax.reindex(freq_order)
# norm values for color palette
norm = plt.Normalize(confmax.values.min(), confmax.values.max())
if is_top:
# Set Palette for graphics
pal = "Greens"
colors = plt.cm.Greens(norm(confmax))
plot_type = "Top"
name = "Combo"
else:
# Set Palette for graphics
pal = "Reds"
colors = plt.cm.Reds(norm(confmax))
plot_type = "Bottom"
name = "Combo2"
# Generate frequency plot
plot = sns.countplot(y='Com_Name', data=df_plt_selection_today, palette=colors, order=freq_order, ax=axs[0])
# Try plot grid lines between bars - problem at the moment plots grid lines on bars - want between bars
yticklabels = ['\n'.join(textwrap.wrap(ticklabel.get_text(), 15)) for ticklabel in plot.get_yticklabels()]
plot.set_yticklabels(yticklabels, fontsize=10)
plot.set(ylabel=None)
plot.set(xlabel="Detections")
# Generate crosstab matrix for heatmap plot
heat = pd.crosstab(df_plt_selection_today['Com_Name'], df_plt_selection_today['Hour of Day'])
# Order heatmap Birds by frequency of occurrance
heat.index = pd.CategoricalIndex(heat.index, categories=freq_order)
heat.sort_index(level=0, inplace=True)
hours_in_day = pd.Series(data=range(0, 24))
heat_frame = pd.DataFrame(data=0, index=heat.index, columns=hours_in_day)
heat = (heat+heat_frame).fillna(0)
# Generatie heatmap plot
plot = sns.heatmap(heat, norm=LogNorm(), annot=True, annot_kws={"fontsize": 7}, fmt="g", cmap=pal, square=False,
cbar=False, linewidths=0.5, linecolor="Grey", ax=axs[1], yticklabels=False)
# Set color and weight of tick label for current hour
for label in plot.get_xticklabels():
if int(label.get_text()) == now.hour:
label.set_color('yellow')
plot.set_xticklabels(plot.get_xticklabels(), rotation=0, size=7)
# Set heatmap border
for _, spine in plot.spines.items():
spine.set_visible(True)
plot.set(ylabel=None)
plot.set(xlabel="Hour of Day")
# Set combined plot layout and titles
f.subplots_adjust(top=0.9)
plt.suptitle(f"{plot_type} {readings} Last Updated: {now.strftime('%Y-%m-%d %H:%M')}")
# Save combined plot
save_name = os.path.expanduser(f"~/BirdSongs/Extracted/Charts/{name}-{now.strftime('%Y-%m-%d')}.png")
plt.savefig(save_name)
plt.show()
plt.close()
def load_fonts():
# Add every font at the specified location
font_dir = [os.path.expanduser('~/BirdNET-Pi/homepage/static')]
for font in font_manager.findSystemFonts(font_dir, fontext='ttf'):
font_manager.fontManager.addfont(font)
# Set font family globally
rcParams['font.family'] = 'Roboto Flex'
def main(daemon, sleep_m):
load_fonts()
last_run = None
while True:
now = datetime.now()
if last_run and now.day != last_run.day:
print("getting yesterday's dataset")
yesterday = last_run.replace(hour=23, minute=59)
data, time = get_data(yesterday)
else:
data, time = get_data(now)
if not data.empty:
create_plot(data, time, is_top=True)
create_plot(data, time, is_top=False)
else:
print('empty dataset')
if daemon:
last_run = now
sleep(60 * sleep_m)
else:
break
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--daemon', action='store_true')
parser.add_argument('--sleep', default=2, type=int, help='Time between runs (minutes)')
args = parser.parse_args()
main(args.daemon, args.sleep)