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
|
||||
app.register_blueprint(stats.blueprint)
|
||||
|
||||
from . import graph
|
||||
app.register_blueprint(graph.blueprint)
|
||||
|
||||
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_sqlalchemy import SQLAlchemy
|
||||
from flask_login import UserMixin
|
||||
|
|
@ -63,6 +64,11 @@ class Accomplishment(db.Model):
|
|||
return Accomplishment.query.filter(
|
||||
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
|
||||
def get_day(user_id, day):
|
||||
# TODO: allow setting custom "start of day" hour
|
||||
|
|
@ -70,7 +76,17 @@ class Accomplishment(db.Model):
|
|||
end = timeutils.day_after(day)
|
||||
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
|
||||
def get_today(user_id):
|
||||
today = datetime.now()
|
||||
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>
|
||||
</form>
|
||||
</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="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue