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)