From 452389e1240bcbe3cb71c6259c0a37dc38d8ace4 Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 19 Oct 2022 19:37:40 +0700 Subject: [PATCH] add fallback graphs for empty rows --- web/src/graph_current.py | 4 ++ web/src/graph_nightly.py | 97 ++++++++++++++++++++++++---- web/static/img/fallback.png | Bin 0 -> 40546 bytes web/static/year-table_fallback.json | 1 + 4 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 web/static/img/fallback.png create mode 100755 web/static/year-table_fallback.json diff --git a/web/src/graph_current.py b/web/src/graph_current.py index 20b0d03..fdd9432 100644 --- a/web/src/graph_current.py +++ b/web/src/graph_current.py @@ -1,5 +1,6 @@ """ handle current graph export """ +import shutil from datetime import datetime import numpy as np @@ -10,6 +11,8 @@ from matplotlib import pyplot as plt from src.db import DatabaseConnect from src.helper import get_config, plt_fill +FALLBACK_GRAPH = "static/img/fallback.png" + class CurrentPlot: """ recreate the last 3h plot """ @@ -112,3 +115,4 @@ def main(): current.write_plt() else: print('no rows found to export current graph') + shutil.copy(FALLBACK_GRAPH, current.FILENAME) diff --git a/web/src/graph_nightly.py b/web/src/graph_nightly.py index 6013b1b..ba57986 100644 --- a/web/src/graph_nightly.py +++ b/web/src/graph_nightly.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import json +import shutil import numpy as np import pandas as pd @@ -12,6 +13,8 @@ from matplotlib import pyplot as plt from src.db import DatabaseConnect from src.helper import get_config, plt_fill +FALLBACK_GRAPH = "static/img/fallback.png" + class NightlyPlots: """ get nightly data """ @@ -79,26 +82,46 @@ class NightlyPlots: date_from = datetime.fromtimestamp(day_from).strftime('%d %b') date_until = datetime.fromtimestamp(day_until).strftime('%d %b') plt_title = f'AQI values from: {date_from} until {date_until}' - _ = LastSevenDays(rows, plt_title) + handler = LastSevenDays(rows, plt_title) + if handler.rows: + handler.create() + else: + handler.fallback() def recreate_last_3(self): """ last three days """ - _ = LastThreeDays(self.rows, self.now) + handler = LastThreeDays(self.rows, self.now) + if handler.rows: + handler.create() + else: + handler.fallback() def recreate_pm_chart(self): """ recreating pm2.5 and pm10 charts """ - _ = PmGraphs(self.rows) + handler = PmGraphs(self.rows) + if handler.rows: + handler.create() + else: + handler.fallback() def recreate_hour_bar(self): """ recreate hourly average through day bar chart """ day_until = int(self.now.date().strftime('%s')) day_from = day_until - 3 * 24 * 60 * 60 rows = [i for i in self.rows if day_from < i[0] < day_until] - _ = HourBar(rows) + handler = HourBar(rows) + if handler.rows: + handler.create() + else: + handler.fallback() def recreate_year_comparison(self): """ recreate year comparison chart and table for json """ - _ = YearComparison(self.rows, self.y_rows) + handler = YearComparison(self.rows, self.y_rows) + if handler.rows: + handler.create() + else: + handler.fallback() class LastSevenDays: @@ -110,7 +133,15 @@ class LastSevenDays: print('recreating last seven days') self.plt_title = plt_title self.rows = rows - self.axis = self.build_axis() + + def fallback(self): + """fallback for no data""" + print("use fallback last seven days") + shutil.copy(FALLBACK_GRAPH, self.FILENAME) + + def create(self): + """create graphs""" + self.build_axis() self.write_plt() def build_axis(self): @@ -179,8 +210,18 @@ class LastThreeDays: self.y_max = None self.now = now self.rows = rows + + def create(self): + """create graphs""" self.rebuild_last_three() + def fallback(self): + """fallback for empty rows""" + print("use fallback for last three days") + for i in range(1, 4): + new_path = f"static/dyn/day-{i}.png" + shutil.copy(FALLBACK_GRAPH, new_path) + def rebuild_last_three(self): """ recreate all three graphs """ # get axis @@ -252,10 +293,20 @@ class PmGraphs: print('recreating pm bar charts') self.rows = rows self.y_max = None - self.axis = self.get_axis() + self.axis = False + + def create(self): + """create pm charts""" + self.get_axis() self.write_plt(thresh=25, title='2.5') self.write_plt(thresh=50, title='10') + def fallback(self): + """use fallback for empty rows""" + print("use fallback for pm charts") + shutil.copy(FALLBACK_GRAPH, "static/dyn/pm10.png") + shutil.copy(FALLBACK_GRAPH, "static/dyn/pm25.png") + def get_axis(self): """ get pm2.5 and pm20 axis """ x_timeline = [datetime.fromtimestamp(i[0]) for i in self.rows] @@ -316,12 +367,22 @@ class PmGraphs: class HourBar: """ recreate hour by our avg bar chart """ + FILENAME = "static/dyn/hours.png" + def __init__(self, rows): print('recreating hour avg bar chart') self.rows = rows - self.axis = self.get_axis() + self.axis = False + + def create(self): + """create hour bar chart""" + self.get_axis() self.write_plt() + def fallback(self): + """fallback for empty rows""" + shutil.copy(FALLBACK_GRAPH, self.FILENAME) + def get_axis(self): """ get hourly bar chart axis """ x_timeline = [datetime.fromtimestamp(i[0]) for i in self.rows] @@ -361,7 +422,7 @@ class HourBar: plt.xticks(ticks=x_range, labels=x_hours) plt.title(plt_title, fontsize=20) plt.tight_layout() - plt.savefig('static/dyn/hours.png', dpi=300) + plt.savefig(self.FILENAME, dpi=300) plt.close('all') plt.figure() @@ -369,14 +430,26 @@ class HourBar: class YearComparison: """ export year on year graph and table """ + PLT_FILENAME = "static/dyn/year-graph.png" + TABLE_FILENAME = "static/dyn/year-table.json" + def __init__(self, rows, y_rows): print('recreating year comparison') self.rows = rows self.y_rows = y_rows - self.axis = self.get_axis() + self.axis = False + + def create(self): + """create year comparison graphs""" + self.get_axis() self.write_table() self.write_plt() + def fallback(self): + """create fallback""" + shutil.copy(FALLBACK_GRAPH, self.PLT_FILENAME) + shutil.copy("static/year-table_fallback.json", self.TABLE_FILENAME) + def get_axis(self): """ build axis """ # first df with current data @@ -453,7 +526,7 @@ class YearComparison: data_rows.insert(0, avg_row) json_dict = json.dumps({"data": data_rows}) # write to file - with open('static/dyn/year-table.json', 'w') as f: + with open(self.TABLE_FILENAME, 'w') as f: f.write(json_dict) def write_plt(self): @@ -484,7 +557,7 @@ class YearComparison: plt.yticks(np.arange(0, y_max, step=50)) plt.xticks(ticks=x_indexes, labels=x) plt.tight_layout() - plt.savefig('static/dyn/year-graph.png', dpi=300) + plt.savefig(self.PLT_FILENAME, dpi=300) plt.figure() diff --git a/web/static/img/fallback.png b/web/static/img/fallback.png new file mode 100644 index 0000000000000000000000000000000000000000..bb28c86be745b8a3830cde99720fc26a9d760d85 GIT binary patch literal 40546 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 literal 0 HcmV?d00001 diff --git a/web/static/year-table_fallback.json b/web/static/year-table_fallback.json new file mode 100755 index 0000000..d6145a9 --- /dev/null +++ b/web/static/year-table_fallback.json @@ -0,0 +1 @@ +{"data": [["nan", "nan", "nan", "nan"]]}