First commit.
This commit is contained in:
6
bin/coronaflask
Executable file
6
bin/coronaflask
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from coronaflask.app import app
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
||||||
1
coronaflask.wsgi
Normal file
1
coronaflask.wsgi
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from coronaflask.app import app as application
|
||||||
2
coronaflask/__init__.py
Normal file
2
coronaflask/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import app
|
||||||
|
from . import corona
|
||||||
30
coronaflask/app.py
Normal file
30
coronaflask/app.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from flask import Flask, render_template
|
||||||
|
from markupsafe import escape
|
||||||
|
|
||||||
|
if __package__ is None:
|
||||||
|
import corona
|
||||||
|
else:
|
||||||
|
from . import corona
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
Cor = corona.Corona()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def world():
|
||||||
|
return cor('World')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/<place>')
|
||||||
|
def cor(place):
|
||||||
|
return render_template('index.html', place=place, figure=Cor.plot_cases(escape(place)), regions=Cor.regions)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
181
coronaflask/corona.py
Normal file
181
coronaflask/corona.py
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import datetime
|
||||||
|
import pandas
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
import matplotlib.dates as mdates
|
||||||
|
import mpld3
|
||||||
|
import requests
|
||||||
|
import io
|
||||||
|
|
||||||
|
|
||||||
|
class Plots:
|
||||||
|
@staticmethod
|
||||||
|
def get_date(date_s):
|
||||||
|
date_d = {k: int(i) + (2000 if k == 'year' else 0) for i, k in zip(date_s.split('/'), ('month', 'day', 'year'))}
|
||||||
|
date_o = datetime.datetime(**date_d)
|
||||||
|
return int(mdates.date2num(date_o) + 0.5), date_s, date_o
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fmt_axis(ax):
|
||||||
|
now = int(mdates.date2num(datetime.datetime.now()) + 0.5)
|
||||||
|
ax.set_xlim(now - 100, now)
|
||||||
|
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
|
||||||
|
ax.xaxis.set_major_locator(mdates.DayLocator(interval=5))
|
||||||
|
xy = [a.get_xydata() for a in ax.get_children() if hasattr(a, 'get_xydata')]
|
||||||
|
if len(xy):
|
||||||
|
xy = np.vstack(xy)
|
||||||
|
ax.set_ylim(Plots.getylim(xy[:, 1], xy[:, 0], ax.get_xlim(), log=ax.get_xscale() == 'log'))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getylim(y, x=None, xlim=None, margin=0.05, log=False):
|
||||||
|
""" get limits for plots according to data
|
||||||
|
copied from matplotlib.axes._base.autoscale_view
|
||||||
|
|
||||||
|
y: the y data
|
||||||
|
optional, for when xlim is set manually on the plot
|
||||||
|
x: corresponding x data
|
||||||
|
xlim: limits on the x-axis in the plot, example: xlim=(0, 100)
|
||||||
|
margin: what fraction of white-space to have at all borders
|
||||||
|
y and x can be lists or tuples of different data in the same plot
|
||||||
|
|
||||||
|
wp@tl20191220
|
||||||
|
"""
|
||||||
|
y = np.array(y).flatten()
|
||||||
|
if log:
|
||||||
|
y = np.log(y)
|
||||||
|
if x is not None and xlim is not None:
|
||||||
|
x = np.array(x).flatten()
|
||||||
|
y = y[(np.nanmin(xlim) < x)*(x < np.nanmax(xlim))*(np.abs(x) > 0)]
|
||||||
|
if not np.any(np.isfinite(y)):
|
||||||
|
return 0, 1
|
||||||
|
if len(y) == 0:
|
||||||
|
return -margin, margin
|
||||||
|
y0t, y1t = np.nanmin(y), np.nanmax(y)
|
||||||
|
if np.isfinite(y1t) and np.isfinite(y0t):
|
||||||
|
delta = (y1t - y0t) * margin
|
||||||
|
if y0t == y1t:
|
||||||
|
delta = 0.5
|
||||||
|
else: # If at least one bound isn't finite, set margin to zero
|
||||||
|
delta = 0
|
||||||
|
if log:
|
||||||
|
return np.exp(y0t - delta), np.exp(y1t + delta)
|
||||||
|
else:
|
||||||
|
return y0t - delta, y1t + delta
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transform(a, b, x, xlim):
|
||||||
|
return np.polyval(np.polyfit(Plots.getylim(a, x, xlim), Plots.getylim(b, x, xlim), 1), a)
|
||||||
|
|
||||||
|
|
||||||
|
class Corona:
|
||||||
|
def __init__(self):
|
||||||
|
self.home = os.path.dirname(__file__)
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self.last_update = datetime.datetime.now()
|
||||||
|
self.confirmed_global = self.get_data(
|
||||||
|
'https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_global.csv')
|
||||||
|
self.deaths_global = self.get_data(
|
||||||
|
'https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_deaths_global.csv')
|
||||||
|
self.regions = sorted(self.confirmed_global['Province/State'].tolist())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_data(url):
|
||||||
|
with requests.get(url, allow_redirects=True) as r:
|
||||||
|
with io.BytesIO(r.content) as b:
|
||||||
|
p = pandas.read_csv(b)
|
||||||
|
|
||||||
|
null = p['Province/State'].isnull()
|
||||||
|
p.loc[null, 'Province/State'] = p.loc[null, 'Country/Region']
|
||||||
|
|
||||||
|
# make series searchable by region
|
||||||
|
countries = p['Country/Region'].unique()
|
||||||
|
states = p['Province/State'].unique()
|
||||||
|
for country in countries:
|
||||||
|
if country not in states:
|
||||||
|
ca = p.query('`Country/Region`=="{}"'.format(country))
|
||||||
|
cb = ca.sum(axis=0)
|
||||||
|
cb['Province/State'] = country
|
||||||
|
cb['Country/Region'] = country
|
||||||
|
cb['Lat'] /= len(ca)
|
||||||
|
cb['Long'] /= len(ca)
|
||||||
|
p.loc[p.index.max() + 1] = cb
|
||||||
|
|
||||||
|
# make World entry by summing
|
||||||
|
w = np.sum(p)
|
||||||
|
w.name = p.index.max() + 1
|
||||||
|
w['Province/State'] = 'World'
|
||||||
|
w['Country/Region'] = 'World'
|
||||||
|
w['Lat'] = 0
|
||||||
|
w['Long'] = 0
|
||||||
|
return p.append(w)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def plot_series(fig, ax, dates_number, dates_obj, cum, llabel=None, rlabel=None):
|
||||||
|
ax.plot(dates_obj, cum, 'ro-')
|
||||||
|
Plots.fmt_axis(ax)
|
||||||
|
ax.set_ylim(Plots.getylim(cum, dates_number, ax.get_xlim()))
|
||||||
|
|
||||||
|
dcum = cum - np.interp(dates_number, dates_number + 1, cum)
|
||||||
|
bx = ax.twinx()
|
||||||
|
p, = bx.plot(dates_obj, Plots.transform(cum, dcum, dates_number, ax.get_xlim()), 'o', color='none')
|
||||||
|
mpld3.plugins.connect(fig, mpld3.plugins.PointLabelTooltip(p, labels=[f'{n:.2g}' for n in cum]))
|
||||||
|
p, = bx.plot(dates_obj, dcum, 'ko-')
|
||||||
|
bx.patch.set_alpha(0.0)
|
||||||
|
mpld3.plugins.connect(fig, mpld3.plugins.PointLabelTooltip(p, labels=[f'{n:.2g}' for n in dcum]))
|
||||||
|
Plots.fmt_axis(bx)
|
||||||
|
if llabel is not None:
|
||||||
|
ax.set_ylabel(llabel, color='r')
|
||||||
|
if rlabel is not None:
|
||||||
|
bx.set_ylabel(rlabel, color='k')
|
||||||
|
|
||||||
|
def plot_cases(self, place, sicktime=14):
|
||||||
|
if datetime.datetime.now() - self.last_update > datetime.timedelta(hours=1):
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
n = self.confirmed_global.query(f'`Province/State`=="{place}"')
|
||||||
|
d = self.deaths_global.query(f'`Province/State`=="{place}"')
|
||||||
|
if n.empty:
|
||||||
|
n = self.confirmed_global.query(f'`Country/Region`=="{place}"')
|
||||||
|
d = self.deaths_global.query(f'`Country/Region`=="{place}"')
|
||||||
|
n = pandas.DataFrame(n.sum()).T
|
||||||
|
d = pandas.DataFrame(d.sum()).T
|
||||||
|
|
||||||
|
dates = [Plots.get_date(c) for c in n.columns if re.match('[\d/]+', c) is not None]
|
||||||
|
|
||||||
|
dates_number, dates_str, dates_obj = zip(*sorted(dates))
|
||||||
|
sick_cum = np.array([int(n[date]) for date in dates_str])
|
||||||
|
death_cum = np.array([int(d[date]) for date in dates_str])
|
||||||
|
dates_number = np.array(dates_number)
|
||||||
|
sick_now = sick_cum - np.interp(dates_number, dates_number + sicktime, sick_cum)
|
||||||
|
|
||||||
|
fig = Figure(figsize=(16, 8), dpi=100)
|
||||||
|
ax = fig.add_subplot(4, 1, 1)
|
||||||
|
self.plot_series(fig, ax, dates_number, dates_obj, sick_cum, 'sick cumulative', 'daily new cases')
|
||||||
|
ax.set_title('cumulative and new daily cases')
|
||||||
|
|
||||||
|
ax = fig.add_subplot(4, 1, 2)
|
||||||
|
self.plot_series(fig, ax, dates_number, dates_obj, sick_now, 'sick now', 'daily change')
|
||||||
|
ax.set_title('current sick and daily change')
|
||||||
|
|
||||||
|
ax = fig.add_subplot(4, 1, 3)
|
||||||
|
reproduction_rate = sick_now / np.clip(np.interp(dates_number, dates_number + 4, sick_now), 1e-15, np.inf)
|
||||||
|
p, = ax.plot(dates_obj, np.clip(reproduction_rate, 0, 3), 'ro-')
|
||||||
|
ax.plot((np.nanmin(dates_number), np.nanmax(dates_number)), (1, 1), '--k')
|
||||||
|
mpld3.plugins.connect(fig, mpld3.plugins.PointLabelTooltip(p, labels=[f'{n:.2f}' for n in reproduction_rate]))
|
||||||
|
Plots.fmt_axis(ax)
|
||||||
|
ax.set_ylim(0.5, 1.5)
|
||||||
|
ax.set_title('reproduction rate')
|
||||||
|
ax.set_ylabel('reproduction rate', color='r')
|
||||||
|
|
||||||
|
ax = fig.add_subplot(4, 1, 4)
|
||||||
|
self.plot_series(fig, ax, dates_number, dates_obj, death_cum, 'diseased cumulative', 'daily diseased')
|
||||||
|
ax.set_title('cumulative and new deaths')
|
||||||
|
|
||||||
|
fig.autofmt_xdate()
|
||||||
|
fig.tight_layout(h_pad=2)
|
||||||
|
|
||||||
|
return mpld3.fig_to_html(fig)
|
||||||
13
coronaflask/templates/index.html
Normal file
13
coronaflask/templates/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html >
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{{place}}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{figure|safe}}
|
||||||
|
<br />
|
||||||
|
{% for region in regions %}
|
||||||
|
<a href="{{region}}">{{region}}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
setup.py
Normal file
26
setup.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
import setuptools
|
||||||
|
|
||||||
|
with open('README.md', 'r') as fh:
|
||||||
|
long_description = fh.read()
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
name='coronaflask',
|
||||||
|
version='2022.1.0',
|
||||||
|
author='Wim Pomp',
|
||||||
|
author_email='wimpomp@gmail',
|
||||||
|
description='Flask for a corona graph plotting website.',
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type='text/markdown',
|
||||||
|
url='',
|
||||||
|
packages=setuptools.find_packages(),
|
||||||
|
classifiers=[
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
],
|
||||||
|
python_requires='>=3.8',
|
||||||
|
install_requires=['waitress', 'flask', 'markupsafe', 'pandas', 'numpy', 'matplotlib', 'requests', 'mpld3'],
|
||||||
|
scripts=[os.path.join('bin', script) for script in
|
||||||
|
os.listdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'bin'))],
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user