From 98061e95c3a82f0f1985dc1802c4d7a800fc1ed3 Mon Sep 17 00:00:00 2001 From: 0xee Date: Sat, 4 Apr 2020 11:35:36 +0200 Subject: [PATCH] Initial POC --- default.nix | 10 ++++ setup.py | 11 +++++ shell.nix | 2 + tsplot | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 default.nix create mode 100644 setup.py create mode 100644 shell.nix create mode 100755 tsplot diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..4690983 --- /dev/null +++ b/default.nix @@ -0,0 +1,10 @@ +{ python3 }: +with python3.pkgs; +buildPythonPackage rec { + name = "tsplot"; + src = ./.; + propagatedBuildInputs = [ pyyaml pandas dash ]; + buildInputs = []; + doCheck = false; + shellHook = ""; +} diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5cff26c --- /dev/null +++ b/setup.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +from distutils.core import setup + +setup(name='tsplot', + version='0.1', + description='Simple time series plotting', + author='0xee', + author_email='tsplot@0xee.eu', + packages=[], + scripts=['tsplot']) diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..d0cf41d --- /dev/null +++ b/shell.nix @@ -0,0 +1,2 @@ +{ pkgs ? import {} }: +pkgs.callPackage ./. {} diff --git a/tsplot b/tsplot new file mode 100755 index 0000000..70392e7 --- /dev/null +++ b/tsplot @@ -0,0 +1,137 @@ +#!/usr/bin/env python + +from dash.dependencies import Input, Output +import dash +import dash_core_components as dcc +import dash_html_components as html + +import pandas as pd +from functools import lru_cache + +import argparse +import sqlite3 +import time +import sys +import datetime + +external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] + +app = dash.Dash(__name__, external_stylesheets=external_stylesheets) + + +def get_topics(db): + cursor = db.cursor() + query = 'SELECT name FROM sqlite_master WHERE type="table"' + q = cursor.execute(query) + topics = [t[0] for t in q.fetchall()] + return topics + + +update_interval_s = 4 + +# Since we're adding callbacks to elements that don't exist in the app.layout, +# Dash will raise an exception to warn us that we might be +# doing something wrong. +# In this case, we're adding the elements through a callback, so we can ignore +# the exception. +app.config.suppress_callback_exceptions = True + +app.layout = html.Div([ + dcc.Location(id='url', refresh=False), + html.Div(id='page-content') +]) + + +@lru_cache(maxsize=10) +def get_data(topic, timerange, ts): + conn = sqlite3.connect(db) + table = topic + + earliest = datetime.datetime.now() - timerange + query = f"SELECT * FROM {table} WHERE timestamp > '{earliest}'" + + df = pd.read_sql(query, conn, parse_dates=[ + 'timestamp'], index_col='timestamp') + return df + + +plot_page = html.Div([ + dcc.Interval( + id='interval-component', + interval=4*1000, # in milliseconds + n_intervals=0), + dcc.Dropdown( + id='timerange', + options=[ + {'label': 'Last hour', 'value': 'h'}, + {'label': 'Last day', 'value': 'd'}, + {'label': 'Last week', 'value': 'w'}, + {'label': 'All', 'value': 'all'} + ], + value='d' + ), + dcc.Graph(id='graph', animate=False), + dcc.Link('Back', href='/') +]) + +def get_timedelta(range_str): + range_to_delta = { + 'h': datetime.timedelta(hours=1), + 'd': datetime.timedelta(days=1), + 'w': datetime.timedelta(weeks=1), + 'all': datetime.timedelta(weeks=10000), + } + return range_to_delta[range_str] + + +@app.callback(Output('graph', 'figure'), + [Input('interval-component', 'n_intervals'), + Input('timerange', 'value'), + Input('url', 'pathname')]) +def update_graph(interval, timerange, pathname): + topics = pathname.strip('/').split('+') + if len(topics): + delta = get_timedelta(timerange) + now = int(time.time()) // update_interval_s + def make_data(df, name): + return { + 'x': df.index, + 'y': df['value'], + 'type': 'scatter', + 'mode': 'lines', + 'name': name + } + return { + 'data': [make_data(get_data(topic, delta, now), topic) for topic in topics]} + else: + return {} + +# Update the index +@app.callback(dash.dependencies.Output('page-content', 'children'), + [dash.dependencies.Input('url', 'pathname')]) +def display_page(pathname): + if pathname == '/': + return index_page + else: + return plot_page + # You could also return a 404 "URL not found" page here + + +if __name__ == '__main__': + + options = argparse.ArgumentParser() + options.add_argument('--debug', '-d', action='store_true') + options.add_argument('db') + + args = options.parse_args() + + db = args.db + + conn = sqlite3.connect(db) + topics = get_topics(conn) + + index_page = html.Div(sum([[ + dcc.Link(f'Go to {page}', href=f'/{page}'), + html.Br()] for page in topics], [])) + + app.run_server(debug=args.debug, host='0.0.0.0')