OXYGENHUB
◆ main oxygenhub / templates / project_charts.html
5.1 KiB · HTML · last changed Board charts and forecasts from the board's history, and its timeline (closes #357, closes #361) 1 day ago source blame history raw
f2d2776b Andy McCall ↶ 1
{% extends "base.html" %}
2
{% block title %}Charts · {{ project.title }} · Projects · {{ project.owner }}{% endblock %}
3
{% block mainclass %} class="wide"{% endblock %}
4
{% block content %}
5
<div class="head"><h1>{{ project.title }} <small><a href="/projects/{{ project.owner }}">{{ project.owner }}'s projects</a> · #{{ project.number }}</small></h1></div>
6
<nav class="board-tabs" aria-label="Views of this board">
7
  <a class="board-tab" href="/projects/{{ project.owner }}/{{ project.number }}">Board</a>
8
  {% for v in views %}<a class="board-tab" href="/projects/{{ project.owner }}/{{ project.number }}?view={{ v.name|urlencode }}">{{ v.name }} <span class="small mute">{{ v.layout }}</span></a>{% endfor %}
9
  <a class="board-tab on" href="/projects/{{ project.owner }}/{{ project.number }}/charts" aria-current="page">Charts</a>
10
</nav>
11
<p class="small mute">From the board's own history: when each card joined each column. No estimates; the forecasts sample the weeks the board has had.</p>
12
13
<section aria-labelledby="fc-h">
14
<h2 id="fc-h">Forecasts</h2>
15
<form class="board-filter" method="get" action="/projects/{{ project.owner }}/{{ project.number }}/charts" role="search">
16
  <label class="visually-hidden" for="fc-q">Forecast the cards matching</label><input type="search" id="fc-q" name="q" value="{{ q }}" placeholder="forecast cards matching: label:bug, milestone:…, repo:…">
17
  <button type="submit" class="small">FORECAST</button>
18
</form>
19
<div class="table-wrap"><table class="board-table">
20
  <thead><tr><th scope="col">Cards</th><th scope="col">Left</th><th scope="col">50% done by</th><th scope="col">85% done by</th></tr></thead>
21
  <tbody>{% for f in c.forecasts %}<tr><td>{{ f.scope }}</td><td>{{ f.remaining }}</td>
22
    {% match f.p50 %}{% when Some(d) %}<td><strong>{{ d }}</strong></td>{% when None %}<td class="mute" colspan="2">no forecast: no card has reached Done in the weeks it draws from</td>{% endmatch %}
23
    {% if let Some(d) = f.p85 %}<td>{{ d }}</td>{% else if f.p50.is_some() %}<td class="mute">beyond ten years</td>{% endif %}</tr>{% endfor %}</tbody>
24
</table></div>
25
{% if let Some(f) = c.forecasts.first() %}<p class="small mute">From {{ f.weeks_of_history }} week{% if f.weeks_of_history != 1 %}s{% endif %} of throughput (cards reaching Done each week), sampled {{ crate::web::board_charts::TRIALS_SHOWN }} times; the same history gives the same dates all day.</p>{% endif %}
26
</section>
27
28
<div class="chart-grid">
29
<figure class="chart-card wide-card" id="timeline"><figcaption>Timeline</figcaption>
30
  {% if timeline_svg.is_empty() %}<p class="mute">No iterations yet, and no open card with a milestone. Start an iteration on the board, or give cards milestones, and they appear here.</p>
31
  {% else %}{{ timeline_svg|safe }}
32
  <div class="legend"><span class="key s-main">open iteration</span><span class="key s-mute">past iteration</span><span class="key s-warn">forecast: 50% to 85%</span><span class="key s-bad">milestone due</span></div>{% endif %}
33
</figure>
34
{% if let Some((name, _, days)) = c.burn %}
35
<figure class="chart-card"><figcaption>{{ name }}: burn-down</figcaption>{{ burn_svg|safe }}
36
  <div class="legend"><span class="key s-main">cards left</span><span class="key s-mute">scope</span></div>
37
  <p class="small mute">{% if let Some((l, s)) = days.last() %}{{ l }} left of {{ s }} after {{ days.len() }} day{% if days.len() != 1 %}s{% endif %}{% endif %}.</p>
38
</figure>
39
{% endif %}
40
<figure class="chart-card"><figcaption>Cycle time</figcaption>
41
  {% match c.cycle_median_days %}{% when Some(m) %}<p class="kpi"><span class="kpi-v">{{ m }} d</span> <span class="kpi-l">median</span> · <span class="kpi-v">{% if let Some(p) = c.cycle_p85_days %}{{ p }}{% endif %} d</span> <span class="kpi-l">85th percentile</span></p>
42
  <p class="small mute">From leaving the first column to reaching Done, over {{ c.cycle_finished }} finished card{% if c.cycle_finished != 1 %}s{% endif %}.</p>
43
  {% when None %}<p class="mute">No card has gone from started to Done yet.</p>{% endmatch %}
44
</figure>
45
<figure class="chart-card"><figcaption>Throughput: cards done each week</figcaption>{{ throughput_svg|safe }}
46
  <details><summary class="small">the numbers</summary><table class="board-table"><thead><tr><th scope="col">Week from</th><th scope="col">Done</th></tr></thead><tbody>{% for (w, n) in week_table %}<tr><td>{{ w }}</td><td>{{ n }}</td></tr>{% endfor %}</tbody></table></details>
47
</figure>
48
<figure class="chart-card wide-card"><figcaption>Cumulative flow</figcaption>{{ flow_svg|safe }}
49
  <div class="legend">{% for name in c.columns %}<span class="key cf-key cf-{% if loop.last %}5{% else %}{{ loop.index0 % 5 }}{% endif %}">{{ name }}</span>{% endfor %}</div>
50
  <p class="small mute">Cards in each column at the end of each day. A band that keeps widening is where work waits.</p>
51
  <details><summary class="small">the last fortnight's numbers</summary><div class="table-wrap"><table class="board-table"><thead><tr><th scope="col">Day</th>{% for name in c.columns %}<th scope="col">{{ name }}</th>{% endfor %}</tr></thead><tbody>{% for (d, row) in flow_table %}<tr><td>{{ d }}</td>{% for n in row %}<td>{{ n }}</td>{% endfor %}</tr>{% endfor %}</tbody></table></div></details>
52
</figure>
53
</div>
54
{% endblock %}