query-based fill charts

This commit is contained in:
crflynn
2018-09-06 03:20:48 -04:00
parent 97a94a202c
commit 8826f42f5e
4 changed files with 279 additions and 112 deletions

View File

@@ -1,32 +1,66 @@
{ {
"data": [ "downloads":{
{ "data":[
"x": [ {
"2017-05-01", "x":[
"2017-05-02", "2017-05-01",
"2017-05-03" "2017-05-02",
], "2017-05-03"
"y": [ ],
"2", "y":[
"5", "2",
"4" "5",
], "4"
"name": "Retention", ],
"type": "scatter", "name":"Downloads",
"mode": "lines+markers", "type":"scatter",
"connectgaps": true, "mode":"lines+markers",
"marker": { "connectgaps":true,
"symbol": "circle", "marker":{
"line": { "symbol":"circle",
"color": "#444", "line":{
"width": 1 "color":"#444",
} "width":1
}, }
"line": { },
"shape": "linear", "line":{
"smoothing": 1, "shape":"linear",
"width": 2 "smoothing":1,
} "width":2
}
}
]
},
"percentages":{
"data":[
{
"x":[
"2017-05-01",
"2017-05-02",
"2017-05-03"
],
"y":[
"2",
"5",
"4"
],
"text":[
"2",
"5",
"4"
],
"hoverinfo": "x+text+name",
"name":"Downloads",
"type":"scatter",
"mode":"lines",
"connectgaps":false,
"line":{
"shape":"linear",
"smoothing":1,
"width":2
},
"fill":"tonexty"
}
]
} }
]
} }

View File

@@ -1,78 +1,156 @@
{ {
"layout": { "downloads":{
"autosize": true, "layout":{
"height": 400, "autosize":true,
"margin": { "height":400,
"r": 100, "margin":{
"t": 40, "r":100,
"autoexpand": true, "t":40,
"b": 80, "autoexpand":true,
"l": 100, "b":80,
"pad": 0 "l":100,
"pad":0
},
"paper_bgcolor":"#fff",
"plot_bgcolor":"rgba(175, 175, 175, 0.2)",
"showlegend":true,
"legend":{
"orientation":"v",
"bgcolor":"#e7e7e7",
"xanchor":"left",
"yanchor":"middle",
"x":0,
"y":0.5
},
"title":"Downloads",
"xaxis":{
"tickformat":"%m-%d",
"dtick":604800000,
"tick0":"2017-08-07",
"gridcolor":"#FFF",
"gridwidth":2,
"anchor":"y",
"domain":[
0,
1
],
"title":"Date",
"titlefont":{
"family":"'Geneva', Verdana, Geneva, sans-serif",
"size":16,
"color":"#7f7f7f"
},
"showline":true,
"linecolor":"rgba(148, 148, 148, 1)",
"linewidth":2,
"tickangle":-45
},
"yaxis":{
"hoverformat":",.0",
"tickformat":",.0",
"gridcolor":"#FFF",
"gridwidth":2,
"autotick":true,
"rangemode":"tozero",
"showline":true,
"title":"Downloads",
"ticksuffix":"",
"tickmode":"auto",
"linecolor":"rgba(148, 148, 148, 1)",
"linewidth":2
}
},
"config":{
"displaylogo":false,
"modeBarButtonsToRemove":[
"toImage",
"sendDataToCloud",
"zoom2d",
"pan2d",
"select2d",
"lasso2d",
"zoomIn2d",
"zoomOut2d",
"toggleSpikelines"
]
}
}, },
"paper_bgcolor": "#fff", "percentages":{
"plot_bgcolor": "rgba(175, 175, 175, 0.2)", "layout":{
"showlegend": true, "autosize":true,
"legend": { "height":400,
"orientation": "v", "margin":{
"bgcolor": "#e7e7e7", "r":100,
"xanchor": "left", "t":40,
"yanchor": "middle", "autoexpand":true,
"x": 0, "b":80,
"y": 0.5 "l":100,
}, "pad":0
"title": "Downloads", },
"yaxis": { "paper_bgcolor":"#fff",
}, "plot_bgcolor":"rgba(175, 175, 175, 0.2)",
"xaxis": { "showlegend":true,
"tickformat": "%m-%d", "legend":{
"dtick": 604800000, "orientation":"v",
"tick0": "2017-08-07", "bgcolor":"#e7e7e7",
"gridcolor": "#FFF", "xanchor":"left",
"gridwidth": 2, "yanchor":"middle",
"anchor": "y", "x":0,
"domain": [ "y":0.5
0, },
1 "title":"Proportional Downloads",
], "xaxis":{
"title": "Date", "tickformat":"%m-%d",
"titlefont": { "dtick":604800000,
"family": "'Geneva', Verdana, Geneva, sans-serif", "tick0":"2017-08-07",
"size": 16, "gridcolor":"#FFF",
"color": "#7f7f7f" "gridwidth":2,
}, "anchor":"y",
"showline": true, "domain":[
"linecolor": "rgba(148, 148, 148, 1)", 0,
"linewidth": 2, 1
"tickangle": -45 ],
}, "title":"Date",
"yaxis": { "titlefont":{
"hoverformat": ",.0", "family":"'Geneva', Verdana, Geneva, sans-serif",
"tickformat": ",.0", "size":16,
"gridcolor": "#FFF", "color":"#7f7f7f"
"gridwidth": 2, },
"autotick": true, "showline":true,
"rangemode": "tozero", "linecolor":"rgba(148, 148, 148, 1)",
"showline": true, "linewidth":2,
"title": "Downloads", "tickangle":-45
"ticksuffix": "", },
"tickmode": "auto", "yaxis":{
"linecolor": "rgba(148, 148, 148, 1)", "range":[
"linewidth": 2 0,
100
],
"dtick":20,
"gridcolor":"#FFF",
"gridwidth":2,
"autotick":false,
"showline":true,
"title":"Proportional Downloads",
"ticksuffix":"%",
"tickmode":"auto",
"linecolor":"rgba(148, 148, 148, 1)",
"linewidth":2
}
},
"config":{
"displaylogo":false,
"modeBarButtonsToRemove":[
"toImage",
"sendDataToCloud",
"zoom2d",
"pan2d",
"select2d",
"lasso2d",
"zoomIn2d",
"zoomOut2d",
"toggleSpikelines"
]
}
} }
},
"config": {
"displaylogo": false,
"modeBarButtonsToRemove": [
"toImage",
"sendDataToCloud",
"zoom2d",
"pan2d",
"select2d",
"lasso2d",
"zoomIn2d",
"zoomOut2d",
"toggleSpikelines"
]
}
} }

View File

@@ -7,6 +7,7 @@ from flask_sslify import SSLify
from pypistats.application import create_app from pypistats.application import create_app
from pypistats.application import create_celery from pypistats.application import create_celery
from pypistats.extensions import db
from pypistats.models.user import User from pypistats.models.user import User
from pypistats.settings import configs from pypistats.settings import configs
@@ -15,7 +16,7 @@ from pypistats.settings import configs
env = os.environ.get("ENV", "dev") env = os.environ.get("ENV", "dev")
app = create_app(configs[env]) app = create_app(configs[env])
sslify = SSLify(app) # sslify = SSLify(app)
celery = create_celery(app) celery = create_celery(app)
app.logger.info(f"Environment: {env}") app.logger.info(f"Environment: {env}")
@@ -27,3 +28,5 @@ def before_request():
g.user = None g.user = None
if "user_id" in session: if "user_id" in session:
g.user = User.query.get(session["user_id"]) g.user = User.query.get(session["user_id"])
if "db" not in g:
g.db = db

View File

@@ -1,4 +1,5 @@
"""General pages.""" """General pages."""
from collections import defaultdict
from copy import deepcopy from copy import deepcopy
import os import os
import re import re
@@ -12,6 +13,9 @@ from flask import redirect
from flask import render_template from flask import render_template
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
import requests import requests
from sqlalchemy import and_
from sqlalchemy import func
from sqlalchemy.sql.expression import label
from wtforms import StringField from wtforms import StringField
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
@@ -117,27 +121,35 @@ def package(package):
# Get data from db # Get data from db
model_data = [] model_data = []
for model in MODELS: for model in MODELS:
model_data.append({ if model == OverallDownloadCount:
"name": model.__tablename__, metrics = ["downloads"]
"data": get_download_data(package, model), else:
}) metrics = ["downloads", "percentages"]
for metric in metrics:
model_data.append({
"metric": metric,
"name": model.__tablename__,
"data": data_function[metric](package, model),
})
# Build the plots # Build the plots
plots = [] plots = []
for model in model_data: for model in model_data:
plot = deepcopy(current_app.config["PLOT_BASE"]) plot = deepcopy(current_app.config["PLOT_BASE"])[model["metric"]]
data = [] data = []
for category, values in model["data"].items(): for category, values in model["data"].items():
base = deepcopy(current_app.config["DATA_BASE"]["data"][0]) base = deepcopy(current_app.config["DATA_BASE"][model["metric"]]["data"][0])
base["x"] = values["x"] base["x"] = values["x"]
base["y"] = values["y"] base["y"] = values["y"]
if model["metric"] == "percentages":
base["text"] = values["text"]
base["name"] = category.title() base["name"] = category.title()
data.append(base) data.append(base)
plot["data"] = data plot["data"] = data
plot["layout"]["title"] = \ plot["layout"]["title"] = \
f"Downloads of {package} package - {model['name'].title().replace('_', ' ')}" # noqa f"Downloads of {package} package - {model['name'].title().replace('_', ' ')}" # noqa
plots.append(plot) plots.append(plot)
return render_template( return render_template(
"package.html", "package.html",
package=package, package=package,
@@ -147,7 +159,6 @@ def package(package):
user=g.user user=g.user
) )
def get_download_data(package, model): def get_download_data(package, model):
"""Get the download data for a package - model.""" """Get the download data for a package - model."""
records = model.query.filter_by(package=package).\ records = model.query.filter_by(package=package).\
@@ -162,6 +173,47 @@ def get_download_data(package, model):
data[category]["y"].append(record.downloads) data[category]["y"].append(record.downloads)
return data return data
def get_proportion_data(package, model):
totals = g.db.session.query(
model.date,
model.package,
func.sum(model.downloads).label("totals")
).filter_by(package=package).group_by(model.date, model.package).subquery()
records = g.db.session.query(
model.date,
model.package,
model.category,
model.downloads,
label("percentages", 100.0 * model.downloads / totals.c.totals)
).join(
totals,
and_(
model.date == totals.c.date,
model.package == totals.c.package
)
).order_by(model.category, model.date).all()
data = {}
cumsum = defaultdict(float)
for record in records:
date = str(record.date)
category = record.category
if category not in data:
data[category] = {"x": [], "y": [], "text": []}
data[category]["x"].append(date)
value = getattr(record, "percentages") or 0
cumsum[date] += value
data[category]["y"].append(cumsum[date])
data[category]["text"].append("{0:.2f}%".format(value) + " = {:,}".format(record.downloads))
return data
data_function = {
"downloads": get_download_data,
"percentages": get_proportion_data,
}
@blueprint.route("/top") @blueprint.route("/top")
def top(): def top():