mirror of
https://github.com/LukeHagar/pypistats.dev.git
synced 2025-12-11 04:21:20 +00:00
query-based fill charts
This commit is contained in:
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user