add graph of total exp per day for last 7 days
This commit is contained in:
parent
64037e5a7a
commit
ad2f8d9093
|
|
@ -28,4 +28,7 @@ def create_app():
|
||||||
from . import stats
|
from . import stats
|
||||||
app.register_blueprint(stats.blueprint)
|
app.register_blueprint(stats.blueprint)
|
||||||
|
|
||||||
|
from . import graph
|
||||||
|
app.register_blueprint(graph.blueprint)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
|
||||||
16
app/db.py
16
app/db.py
|
|
@ -1,3 +1,4 @@
|
||||||
|
from sqlalchemy.sql import func
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
|
|
@ -63,6 +64,11 @@ class Accomplishment(db.Model):
|
||||||
return Accomplishment.query.filter(
|
return Accomplishment.query.filter(
|
||||||
Accomplishment.time >= start, Accomplishment.time < end, Accomplishment.user_id == user_id).all()
|
Accomplishment.time >= start, Accomplishment.time < end, Accomplishment.user_id == user_id).all()
|
||||||
|
|
||||||
|
def get_time_range_total(user_id, start, end):
|
||||||
|
result = db.session.query(func.sum(Accomplishment.difficulty).label('total')).filter(
|
||||||
|
Accomplishment.time >= start, Accomplishment.time < end, Accomplishment.user_id == user_id)[0][0]
|
||||||
|
return result if result is not None else 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_day(user_id, day):
|
def get_day(user_id, day):
|
||||||
# TODO: allow setting custom "start of day" hour
|
# TODO: allow setting custom "start of day" hour
|
||||||
|
|
@ -70,7 +76,17 @@ class Accomplishment(db.Model):
|
||||||
end = timeutils.day_after(day)
|
end = timeutils.day_after(day)
|
||||||
return Accomplishment.get_time_range(user_id, start, end)
|
return Accomplishment.get_time_range(user_id, start, end)
|
||||||
|
|
||||||
|
def get_day_total(user_id, day):
|
||||||
|
start = timeutils.day(day)
|
||||||
|
end = timeutils.day_after(day)
|
||||||
|
return Accomplishment.get_time_range_total(user_id, start, end)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_today(user_id):
|
def get_today(user_id):
|
||||||
today = datetime.now()
|
today = datetime.now()
|
||||||
return Accomplishment.get_day(user_id, today)
|
return Accomplishment.get_day(user_id, today)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_today_total(user_id):
|
||||||
|
today = datetime.now()
|
||||||
|
return Accomplishment.get_day_total(user_id, today)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
from flask import Blueprint, render_template
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from .db import db, Accomplishment
|
||||||
|
from . import timeutils
|
||||||
|
|
||||||
|
|
||||||
|
blueprint = Blueprint('graph', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.route('/graph.svg')
|
||||||
|
@login_required
|
||||||
|
def graph_svg():
|
||||||
|
count = 7
|
||||||
|
accomplishments = [0]*count
|
||||||
|
days = [""]*count
|
||||||
|
day = timeutils.today()
|
||||||
|
|
||||||
|
for i in range(1, count+1):
|
||||||
|
total_xp = Accomplishment.get_day_total(current_user.id, day)
|
||||||
|
accomplishments[-i] = total_xp
|
||||||
|
days[-i] = day.strftime('%a')[:2]
|
||||||
|
day = timeutils.day_before(day)
|
||||||
|
|
||||||
|
print(accomplishments)
|
||||||
|
|
||||||
|
return render_template('graph.svg', days=days, **gen_graph_data(accomplishments)), 200, {'Content-Type': 'image/svg+xml'}
|
||||||
|
|
||||||
|
|
||||||
|
def gen_scale(base=10):
|
||||||
|
return [base*i for i in range(0, 5)]
|
||||||
|
|
||||||
|
|
||||||
|
def find_scale_base(max_n):
|
||||||
|
if max_n < 20:
|
||||||
|
return 10
|
||||||
|
|
||||||
|
return (max_n - (max_n - 1) % 20 + 20) // 4
|
||||||
|
n = max_n % 20
|
||||||
|
while n % 20 != 0:
|
||||||
|
n += 1
|
||||||
|
return n//4
|
||||||
|
|
||||||
|
|
||||||
|
GRAPH_TOP_LINE = 16.6
|
||||||
|
GRAPH_BOTTOM_LINE = 83
|
||||||
|
|
||||||
|
GRAPH_RANGE = GRAPH_BOTTOM_LINE - GRAPH_TOP_LINE
|
||||||
|
|
||||||
|
|
||||||
|
def absolute_to_percentage_position(n, scale_base):
|
||||||
|
scale_top = scale_base * 4
|
||||||
|
return round(GRAPH_BOTTOM_LINE - n/scale_top * GRAPH_RANGE, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_graph_data(numbers):
|
||||||
|
assert len(numbers) > 1
|
||||||
|
max_n = max(numbers)
|
||||||
|
scale_base = find_scale_base(max_n)
|
||||||
|
scale = gen_scale(scale_base)
|
||||||
|
|
||||||
|
dots = [absolute_to_percentage_position(
|
||||||
|
n, scale_base) for n in numbers]
|
||||||
|
lines = list(zip(dots, dots[1:]))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"dots": dots,
|
||||||
|
"lines": lines,
|
||||||
|
"scale": scale,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{% macro x(n) -%}
|
||||||
|
{{ 15+((85-15)/(lines|length))*(n-1) }}%
|
||||||
|
{%- endmacro %}
|
||||||
|
<svg version="1.1"
|
||||||
|
baseProfile="full"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
svg {
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 1 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<g class="bg">
|
||||||
|
{% for n in scale %}
|
||||||
|
<line x1="10%" y1="{{ 16.6 * loop.index }}%" x2="90%" y2="{{ 16.6 * loop.index }}%" stroke="#ddd" stroke-width="2px" />
|
||||||
|
{% endfor %}
|
||||||
|
</g>
|
||||||
|
<g class="legend" style="font-size: 0.9em; text-align:right;">
|
||||||
|
{% for n in scale | reverse %}
|
||||||
|
<text x="10%" y="{{ 16.6 * (loop.index) }}%" style="text-anchor: end; font-weight: semibold" dx="-.6em" dy=".3em" fill="#aaa">{{ n }}</text>
|
||||||
|
{% endfor %}
|
||||||
|
</g>
|
||||||
|
<g class="lines">
|
||||||
|
{% for y1, y2 in lines %}
|
||||||
|
<line x1="{{ x(loop.index) }}" y1="{{ y1 }}%" x2="{{ x(loop.index + 1) }}" y2="{{ y2 }}%" stroke="#48bb78" stroke-width="3" />
|
||||||
|
{% endfor %}
|
||||||
|
</g>
|
||||||
|
<g class="dots">
|
||||||
|
{% for cy in dots %}
|
||||||
|
<circle cx="{{ x(loop.index) }}" cy="{{ cy }}%" r="4" stroke-width="0" fill="#2f855a" />
|
||||||
|
{% endfor %}
|
||||||
|
</g>
|
||||||
|
<g class="legend" style="font-size: 0.9em; text-anchor: middle">
|
||||||
|
{% for day in days %}
|
||||||
|
<text x="{{ x(loop.index) }}" y="95%" fill="#888">{{ day }}</text>
|
||||||
|
{% endfor %}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
@ -12,6 +12,9 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="max-w-lg mx-auto card">
|
||||||
|
<img class="w-full h-48" src="{{ url_for('graph.graph_svg') }}">
|
||||||
|
</div>
|
||||||
<div class="max-w-lg mx-auto card">
|
<div class="max-w-lg mx-auto card">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue