views: Purchase graph

feature/cython
informatic 2017-05-05 22:19:57 +02:00
parent ca2d83429f
commit 4e8e085587
7 changed files with 511 additions and 1 deletions

67
bitvend/graphs.py Normal file
View File

@ -0,0 +1,67 @@
from datetime import date, datetime, timedelta
from sqlalchemy import func
import itertools
from bitvend.models import db, Transaction
def daterange(start_date, end_date):
for n in range(int((end_date - start_date).days)+1):
yield (start_date + timedelta(n)).date()
def gen_graph_dataset(resultset, date_from=None, date_to=None, default=0):
date_hash = {datetime.strptime(xdate, '%Y-%m-%d').date(): count for xdate, count in resultset}
if not date_from:
date_from = min(date_hash.keys())
if not date_to:
date_to = datetime.utcnow().date()
return [
type(default)(date_hash[d]) if d in date_hash else default
for d in daterange(date_from, date_to)
]
def gen_database_graph(title, query, date_from, date_to, default=0):
date_column = query.column_descriptions[0]['expr']
resultset = query \
.group_by(
date_column
).filter(
date_column >= date_from,
date_column <= date_to
).all()
return [title] + gen_graph_dataset(resultset, date_from, date_to, default)
def zip_graph(dataset):
keys = [n[0] for n in dataset]
i = zip(*dataset)
next(i)
return [dict(zip(keys, values), values=None) for values in i]
def gen_main_graph(date_from=None, date_to=None):
if date_from is None:
date_from = date.today() - timedelta(days=30)
if date_to is None:
date_to = date.today()
date_from = datetime.combine(date_from, datetime.min.time())
date_to = datetime.combine(date_to, datetime.max.time())
return zip_graph([
['date'] + [n.strftime('%Y-%m-%d')
for n in daterange(date_from, date_to)],
gen_database_graph('purchases', db.session.query(
#func.date_trunc('day', Transaction.created),
func.strftime('%Y-%m-%d', Transaction.created),
func.count(Transaction.created),
).filter(Transaction.type == 'purchase'), date_from, date_to),
])

View File

@ -0,0 +1,401 @@
.mg-active-datapoint {
fill: black;
font-size: 0.9rem;
font-weight: 400;
opacity: 0.8;
}
.mg-area1-color {
fill: #0000ff;
}
.mg-area2-color {
fill: #05b378;
}
.mg-area3-color {
fill: #db4437;
}
.mg-area4-color {
fill: #f8b128;
}
.mg-area5-color {
fill: #5c5c5c;
}
text.mg-barplot-group-label {
font-weight:900;
}
.mg-barplot rect.mg-bar {
shape-rendering: auto;
}
.mg-barplot rect.mg-bar.default-bar {
fill: #b6b6fc;
}
.mg-barplot rect.mg-bar.default-active {
fill: #9e9efc;
}
.mg-barplot .mg-bar-prediction {
fill: #5b5b5b;
}
.mg-barplot .mg-bar-baseline {
stroke: #5b5b5b;
stroke-width: 2;
}
.mg-bar-target-element {
font-size:11px;
padding-left:5px;
padding-right:5px;
font-weight:300;
}
.mg-baselines line {
opacity: 1;
shape-rendering: auto;
stroke: #b3b2b2;
stroke-width: 1px;
}
.mg-baselines text {
fill: black;
font-size: 0.9rem;
opacity: 0.6;
stroke: none;
}
.mg-baselines-small text {
font-size: 0.6rem;
}
.mg-category-guides line {
stroke: #b3b2b2;
}
.mg-header {
cursor: default;
font-size: 1.2rem;
}
.mg-header .mg-chart-description {
fill: #ccc;
font-family: FontAwesome;
font-size: 1.2rem;
}
.mg-header .mg-warning {
fill: #ccc;
font-family: FontAwesome;
font-size: 1.2rem;
}
.mg-points circle {
opacity: 0.65;
}
.mg-popover {
font-size: 0.95rem;
}
.mg-popover-content {
cursor: auto;
line-height: 17px;
}
.mg-data-table {
margin-top: 30px;
}
.mg-data-table thead tr th {
border-bottom: 1px solid darkgray;
cursor: default;
font-size: 1.1rem;
font-weight: normal;
padding: 5px 5px 8px 5px;
text-align: right;
}
.mg-data-table thead tr th .fa {
color: #ccc;
padding-left: 4px;
}
.mg-data-table thead tr th .popover {
font-size: 1rem;
font-weight: normal;
}
.mg-data-table .secondary-title {
color: darkgray;
}
.mg-data-table tbody tr td {
margin: 2px;
padding: 5px;
vertical-align: top;
}
.mg-data-table tbody tr td.table-text {
opacity: 0.8;
padding-left: 30px;
}
.mg-y-axis line.mg-extended-yax-ticks {
opacity: 0.4;
}
.mg-x-axis line.mg-extended-xax-ticks {
opacity: 0.4;
}
.mg-histogram .axis path,
.mg-histogram .axis line {
fill: none;
opacity: 0.7;
shape-rendering: auto;
stroke: #ccc;
}
tspan.hist-symbol {
fill: #9e9efc;
}
.mg-histogram .mg-bar rect {
fill: #b6b6fc;
shape-rendering: auto;
}
.mg-histogram .mg-bar rect.active {
fill: #9e9efc;
}
.mg-least-squares-line {
stroke: red;
stroke-width: 1px;
}
.mg-lowess-line {
fill: none;
stroke: red;
}
.mg-line1-color {
stroke: #4040e8;
}
.mg-hover-line1-color {
fill: #4040e8;
}
.mg-line2-color {
stroke: #05b378;
}
.mg-hover-line2-color {
fill: #05b378;
}
.mg-line3-color {
stroke: #db4437;
}
.mg-hover-line3-color {
fill: #db4437;
}
.mg-line4-color {
stroke: #f8b128;
}
.mg-hover-line4-color {
fill: #f8b128;
}
.mg-line5-color {
stroke: #5c5c5c;
}
.mg-hover-line5-color {
fill: #5c5c5c;
}
.mg-line-legend text {
font-size: 0.9rem;
font-weight: 300;
stroke: none;
}
.mg-line1-legend-color {
color: #4040e8;
fill: #4040e8;
}
.mg-line2-legend-color {
color: #05b378;
fill: #05b378;
}
.mg-line3-legend-color {
color: #db4437;
fill: #db4437;
}
.mg-line4-legend-color {
color: #f8b128;
fill: #f8b128;
}
.mg-line5-legend-color {
color: #5c5c5c;
fill: #5c5c5c;
}
.mg-main-area-solid svg .mg-main-area {
fill: #ccccff;
opacity: 1;
}
.mg-markers line {
opacity: 1;
shape-rendering: auto;
stroke: #b3b2b2;
stroke-width: 1px;
}
.mg-markers text {
fill: black;
font-size: 0.8rem;
opacity: 0.6;
}
.mg-missing-text {
opacity: 0.9;
}
.mg-missing-background {
stroke: blue;
fill: none;
stroke-dasharray: 10,5;
stroke-opacity: 0.05;
stroke-width: 2;
}
.mg-missing .mg-main-line {
opacity: 0.1;
}
.mg-missing .mg-main-area {
opacity: 0.03;
}
path.mg-main-area {
opacity: 0.2;
stroke: none;
}
path.mg-confidence-band {
fill: #ccc;
opacity: 0.4;
stroke: none;
}
path.mg-main-line {
fill: none;
opacity: 0.8;
stroke-width: 1.1px;
}
.mg-points circle {
fill-opacity: 0.4;
stroke-opacity: 1;
}
circle.mg-points-mono {
fill: #0000ff;
stroke: #0000ff;
}
tspan.mg-points-mono {
fill: #0000ff;
stroke: #0000ff;
}
/* a selected point in a scatterplot */
.mg-points circle.selected {
fill-opacity: 1;
stroke-opacity: 1;
}
.mg-voronoi path {
fill: none;
pointer-events: all;
stroke: none;
stroke-opacity: 0.1;
}
.mg-x-rug-mono,
.mg-y-rug-mono {
stroke: black;
}
.mg-x-axis line,
.mg-y-axis line {
opacity: 1;
shape-rendering: auto;
stroke: #b3b2b2;
stroke-width: 1px;
}
.mg-x-axis text,
.mg-y-axis text,
.mg-histogram .axis text {
fill: black;
font-size: 0.9rem;
opacity: 0.6;
}
.mg-x-axis .label,
.mg-y-axis .label,
.mg-axis .label {
font-size: 0.8rem;
text-transform: uppercase;
font-weight: 400;
}
.mg-x-axis-small text,
.mg-y-axis-small text,
.mg-active-datapoint-small {
font-size: 0.6rem;
}
.mg-x-axis-small .label,
.mg-y-axis-small .label {
font-size: 0.65rem;
}
.mg-european-hours {
}
.mg-year-marker text {
fill: black;
font-size: 0.7rem;
opacity: 0.6;
}
.mg-year-marker line {
opacity: 1;
shape-rendering: auto;
stroke: #b3b2b2;
stroke-width: 1px;
}
.mg-year-marker-small text {
font-size: 0.6rem;
}

8
bitvend/static/js/d3.v4.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="stylesheet" href="{{ static('css/bootstrap.css') }}" media="screen">
<link rel="stylesheet" href="{{ static('css/metricsgraphics.css') }}" media="screen">
<style>
body {
margin-top: 80px;
@ -93,5 +94,9 @@
<script src="{{ static('js/jquery-1.10.2.min.js') }}"></script>
<script src="{{ static('js/bootstrap.min.js') }}"></script>
<script src="{{ static('js/d3.v4.min.js') }}"></script>
<script src="{{ static('js/metricsgraphics.min.js') }}"></script>
{% block tail_js %}
{% endblock %}
</body>
</html>

View File

@ -152,8 +152,27 @@
</div>
{% endfor %}
</div>
<div id="purchases"></div>
<blockquote class="blockquote-reverse">
<p>Kowalski czuje zapach pieniędzy.</p>
<small>Someone famous</small>
</blockquote>
{% endblock %}
{% block tail_js %}
<script>
d3.json('/api/1/history.json', function(data) {
data = MG.convert.date(data, 'date');
MG.data_graphic({
title: "Purchases",
data: data,
height: 200,
width: 600,
target: document.getElementById('purchases'),
x_accessor: 'date',
y_accessor: 'purchases'
});
});
</script>
{% endblock %}

View File

@ -1,4 +1,5 @@
from flask import Blueprint, render_template, redirect, request, flash, url_for
from flask import Blueprint, render_template, redirect, request, flash, \
url_for, jsonify
from flask import current_app as app
from flask_login import login_required, current_user, logout_user
import six
@ -10,6 +11,7 @@ from bitvend import dev, proc
from bitvend.models import db, User, Transaction, NoFunds
from bitvend.auth import try_login, cap_required
from bitvend.forms import TransferForm
from bitvend.graphs import gen_main_graph
bp = Blueprint('bitvend', __name__, template_folder='templates')
@ -132,3 +134,7 @@ def qrcode_gen(data):
'Content-Type': 'image/svg+xml',
'Cache-Control': 'public,max-age=3600',
}
@bp.route('/api/1/history.json')
def history():
return jsonify(gen_main_graph())