From 5d6cc5f1e3e235a486d77aa34ce5a13a4e19581a Mon Sep 17 00:00:00 2001 From: Josef Skladanka Date: May 17 2024 07:55:15 +0000 Subject: Add Velocity plot --- diff --git a/kanban/components/Filters.jinja b/kanban/components/Filters.jinja index 28123f9..e3f14bf 100644 --- a/kanban/components/Filters.jinja +++ b/kanban/components/Filters.jinja @@ -8,7 +8,8 @@ hx-trigger="input changed from:.filter-form-input delay:500ms" hx-select="#kanban-columns" hx-target="#kanban-columns" - hx-swap="outerHTML"> + hx-swap="outerHTML" + hx-replace-url="true">
diff --git a/kanban/components/Index.jinja b/kanban/components/Index.jinja index f0755a6..08d7940 100644 --- a/kanban/components/Index.jinja +++ b/kanban/components/Index.jinja @@ -1,19 +1,14 @@ -{#def backlog=[], assigned=[], finished=[], sums=(0, 0, 0), filtered=False#} +{#def backlog=[], assigned=[], finished=[], plot_data=[], sums=(0, 0, 0), filtered=False#} - +
+ +
+ + +
diff --git a/kanban/components/Layout.jinja b/kanban/components/Layout.jinja index 61884fe..c534ed7 100644 --- a/kanban/components/Layout.jinja +++ b/kanban/components/Layout.jinja @@ -27,6 +27,9 @@ class="button {% if request.args.get('repos') or request.args.get('assigned_to') %}is-warning{% else %}is-primary{% endif %} js-toggler" data-target="filtersMenu">Filters
+
- {{ content }}
diff --git a/kanban/components/Root.jinja b/kanban/components/Root.jinja index 78bdaa0..d26a5e0 100644 --- a/kanban/components/Root.jinja +++ b/kanban/components/Root.jinja @@ -15,6 +15,9 @@ integrity="sha384-rgjA7mptc2ETQqXoYC3/zJvkU7K/aP44Y+z7xQuJiVnB/422P/Ak+F/AqFR7E4Wr" crossorigin="anonymous"> + + + {{ catalog.render_assets() }} diff --git a/kanban/components/SpPlot.jinja b/kanban/components/SpPlot.jinja new file mode 100644 index 0000000..79ebe35 --- /dev/null +++ b/kanban/components/SpPlot.jinja @@ -0,0 +1,38 @@ +{# def data=[] #} +
+ + + + + + + + + + + {% for xlabel, ysize, ydata in data %} + + + + + {% endfor %} + +
Completed storypoints per month
MonthStory Points
{{ xlabel }} + {{ ydata }} +
+
+ diff --git a/kanban/controllers/main.py b/kanban/controllers/main.py index b50b6b2..eaffeac 100644 --- a/kanban/controllers/main.py +++ b/kanban/controllers/main.py @@ -1,10 +1,11 @@ import json +import math import urllib.parse from collections import defaultdict from datetime import datetime from functools import reduce -from flask import Blueprint, flash, redirect, request, url_for +from flask import Blueprint, flash, make_response, redirect, request, url_for from flask_login import current_user, login_required, login_user, logout_user from flask_wtf import FlaskForm from wtforms import BooleanField, PasswordField, StringField, SubmitField @@ -23,7 +24,7 @@ class LoginForm(FlaskForm): submit = SubmitField("Submit") -def filter_issues(filters): +def filter_issues(filters={}): backlog = {} assigned = {} finished = [] @@ -78,6 +79,83 @@ def filter_issues(filters): return backlog, assigned, finished +def chunks(lst, n): + ch_size = math.ceil(len(lst) / n) + return [lst[i : i + ch_size] for i in range(0, len(lst), ch_size)] + +def prepare_plotdata(finished): + # Prepares data for the "velocity" graph + # The goal here is "normalizing" the loosk of the graph so it + # looks more flat-ish even with some weird outliers. + # To do this, we split the actual story-points values (summed per month) + # into three groups (low/mid/hi) by value. + # Each value group than has an accompanying band on the graph, + # thus keeping the "this is a higher number than the other one" fact, + # but making the height-difference of the graphed bars non-linear. + # e.g. on this set: [1, 2, 5, 8, 30, 50, 200] + # the value groups would be [1, 2], [5, 8, 30], [50, 200] + # and the respective "heights" woud end up like this: + # [0.15, 0.3, 0.375, 0.414, 0.7, 0.78, 1.0] + # Visualy squashing the "outliers" closer to the middle ground + + sps_per_month = {} + for k, v in sorted(finished.items())[-12:]: + sps_per_month[k] = sum([i.story_points for i in v[0]]) + + values = sorted(sps_per_month.values()) + unique_values_cnt = len(set(values)) + low = [] + med = [] + hi = [] + if unique_values_cnt >= 6: + vln = len(values) + med = sorted(set(values[vln//6:vln-vln//6])) + low = sorted(set(values[0:vln//6]) - set([med[0], ])) + hi = sorted(set(values[vln-vln//6:]) - set([med[-1], ])) + + med_low = med[0] + while low and low[-1] == med_low: + low.pop() + + med_hi = med[-1] + while hi and hi[0] <= med_hi*1.2: + med.append(hi.pop(0)) + else: + med = sorted(set(values)) + if unique_values_cnt == 2: + hi = [med.pop()] + elif unique_values_cnt >= 3: + low = [med.pop(0)] + hi = [med.pop()] + + lsc = 0.3 + msf = 0.31 + msc = 0.7 + hsf = 0.71 + hsc = 1.0 + + if not low: + msf = 0.0 + if not hi: + msc = 1.0 + + graph_heights = {} + + for v in low: + graph_heights[v] = lsc * v/low[-1] + + med_bs = msc-msf + for v in med: + graph_heights[v] = msf + (med_bs * v/med[-1]) + + hi_bs = hsc-hsf + for v in hi: + graph_heights[v] = hsf + (hi_bs * v/hi[-1]) + + return [[k, graph_heights[v], v] for k, v in sps_per_month.items()] + + + @main.route("/") def index(): filters = {} @@ -108,6 +186,7 @@ def index(): assigned=assigned, finished=finished, sums=(blg_sum, ass_sum, fin_sum), + plot_data=prepare_plotdata(finished), filtered=( "repos" if filters.get("repos", "").strip() else None, "assigned_to" if filters.get("assigned_to", "").strip() else None,