mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
Burndown Chart
- Completed chart rendering for all three variations. - Updated assorted documentation. - Removed most of the obsolete code. - Not completed: - find rate calculation/algorithm - fix rate calculation/algorithm - completion date algorithm
This commit is contained in:
parent
3a4fff1683
commit
ead7cfe2b8
4 changed files with 211 additions and 519 deletions
|
@ -2,7 +2,9 @@
|
||||||
------ current release ---------------------------
|
------ current release ---------------------------
|
||||||
|
|
||||||
1.9.4 ()
|
1.9.4 ()
|
||||||
+ Added burndown charts - burndown.daily, burndown.weekly, burndown.monthly.
|
+ Added burndown charts - burndown.daily, burndown.weekly, burndown.monthly,
|
||||||
|
that use color.burndown.pending, color.burndown.started and
|
||||||
|
color.burndown.done colors.
|
||||||
+ Fixed bug #529, where the 'depends' attribute was not mentioned in the
|
+ Fixed bug #529, where the 'depends' attribute was not mentioned in the
|
||||||
task man page (thanks to Dirk Deimeke).
|
task man page (thanks to Dirk Deimeke).
|
||||||
+ Fixed bug #535 which omitted the holidays-NO.rc file from the packages
|
+ Fixed bug #535 which omitted the holidays-NO.rc file from the packages
|
||||||
|
|
3
NEWS
3
NEWS
|
@ -13,7 +13,8 @@ New commands in taskwarrior 1.9.4
|
||||||
|
|
||||||
New configuration options in taskwarrior 1.9.4
|
New configuration options in taskwarrior 1.9.4
|
||||||
|
|
||||||
-
|
- color.burndown.pending, color.burndown.started and color.burndown.done
|
||||||
|
control the color of the burndown charts.
|
||||||
|
|
||||||
Newly deprecated features in taskwarrior 1.9.4
|
Newly deprecated features in taskwarrior 1.9.4
|
||||||
|
|
||||||
|
|
|
@ -701,6 +701,19 @@ Colors the bars on the ghistory report graphs. Defaults to red, green and
|
||||||
yellow bars.
|
yellow bars.
|
||||||
.RE
|
.RE
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B color.burndown.pending=on red
|
||||||
|
.RE
|
||||||
|
.br
|
||||||
|
.B color.burndown.started=on yellow
|
||||||
|
.RE
|
||||||
|
.br
|
||||||
|
.B color.burndown.done=on green
|
||||||
|
.RS
|
||||||
|
Colors the bars on the burndown reports graphs. Defaults to red, green and
|
||||||
|
yellow bars.
|
||||||
|
.RE
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B color.undo.before=red
|
.B color.undo.before=red
|
||||||
.RE
|
.RE
|
||||||
|
|
710
src/burndown.cpp
710
src/burndown.cpp
|
@ -104,6 +104,36 @@ Bar::~Bar ()
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Data gathering algorithm:
|
||||||
|
//
|
||||||
|
// e = entry
|
||||||
|
// s = start
|
||||||
|
// C = end/Completed
|
||||||
|
// D = end/Deleted
|
||||||
|
// > = Pending/Waiting
|
||||||
|
//
|
||||||
|
// ID 30 31 01 02 03 04 05 06 07 08 09 10
|
||||||
|
// -- ------------------------------------
|
||||||
|
// 1 e-----s--C
|
||||||
|
// 2 e--s-----D
|
||||||
|
// 3 e-----s-------------->
|
||||||
|
// 4 e----------------->
|
||||||
|
// 5 e----->
|
||||||
|
// -- ------------------------------------
|
||||||
|
// pp 1 2 3 3 2 2 2 3 3 3
|
||||||
|
// ss 2 1 1 1 1 1 1 1
|
||||||
|
// dd 1 1 1 1 1 1 1
|
||||||
|
// -- ------------------------------------
|
||||||
|
//
|
||||||
|
// 5 | ss dd dd dd dd
|
||||||
|
// 4 | ss ss dd dd dd ss ss ss
|
||||||
|
// 3 | pp pp ss ss ss pp pp pp
|
||||||
|
// 2 | pp pp pp pp pp pp pp pp pp
|
||||||
|
// 1 | pp pp pp pp pp pp pp pp pp pp
|
||||||
|
// 0 +-------------------------------------
|
||||||
|
// 30 31 01 02 03 04 05 06 07 08 09 10
|
||||||
|
// Oct Nov
|
||||||
|
//
|
||||||
class Chart
|
class Chart
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -123,6 +153,7 @@ private:
|
||||||
Date increment (const Date&);
|
Date increment (const Date&);
|
||||||
Date decrement (const Date&);
|
Date decrement (const Date&);
|
||||||
void maxima ();
|
void maxima ();
|
||||||
|
void yLabels (std::vector <int>&);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
int width;
|
int width;
|
||||||
|
@ -136,6 +167,7 @@ public:
|
||||||
int actual_bars;
|
int actual_bars;
|
||||||
std::map <time_t, Bar> bars;
|
std::map <time_t, Bar> bars;
|
||||||
Date earliest;
|
Date earliest;
|
||||||
|
int carryover_done;
|
||||||
char period; // D, W, M.
|
char period; // D, W, M.
|
||||||
std::string grid;
|
std::string grid;
|
||||||
|
|
||||||
|
@ -175,6 +207,9 @@ Chart::Chart (char type)
|
||||||
period = type;
|
period = type;
|
||||||
std::cout << "# period " << period << "\n";
|
std::cout << "# period " << period << "\n";
|
||||||
|
|
||||||
|
carryover_done = 0;
|
||||||
|
std::cout << "# carryover_done " << carryover_done << "\n";
|
||||||
|
|
||||||
// Rates are calculated last.
|
// Rates are calculated last.
|
||||||
find_rate = 0.0;
|
find_rate = 0.0;
|
||||||
fix_rate = 0.0;
|
fix_rate = 0.0;
|
||||||
|
@ -195,13 +230,14 @@ Chart& Chart::operator= (const Chart& other)
|
||||||
height = other.height;
|
height = other.height;
|
||||||
graph_width = other.graph_width;
|
graph_width = other.graph_width;
|
||||||
graph_height = other.graph_height;
|
graph_height = other.graph_height;
|
||||||
max_value = other. max_value;
|
max_value = other.max_value;
|
||||||
max_label = other.max_label;
|
max_label = other.max_label;
|
||||||
labels = other.labels;
|
labels = other.labels;
|
||||||
estimated_bars = other.estimated_bars;
|
estimated_bars = other.estimated_bars;
|
||||||
actual_bars = other.actual_bars;
|
actual_bars = other.actual_bars;
|
||||||
bars = other.bars;
|
bars = other.bars;
|
||||||
earliest = other.earliest;
|
earliest = other.earliest;
|
||||||
|
carryover_done = other.carryover_done;
|
||||||
period = other.period;
|
period = other.period;
|
||||||
grid = other.grid;
|
grid = other.grid;
|
||||||
find_rate = other.find_rate;
|
find_rate = other.find_rate;
|
||||||
|
@ -222,13 +258,12 @@ void Chart::scan (std::vector <Task>& tasks)
|
||||||
{
|
{
|
||||||
generateBars ();
|
generateBars ();
|
||||||
|
|
||||||
std::cout << "# loaded " << tasks.size () << " tasks\n";
|
|
||||||
|
|
||||||
// Not quantized, so that "while (xxx < now)" is inclusive.
|
// Not quantized, so that "while (xxx < now)" is inclusive.
|
||||||
Date now;
|
Date now;
|
||||||
|
|
||||||
time_t epoch;
|
time_t epoch;
|
||||||
foreach (task, tasks)
|
std::vector <Task>::iterator task;
|
||||||
|
for (task = tasks.begin (); task != tasks.end (); ++task)
|
||||||
{
|
{
|
||||||
// The entry date is when the counting starts.
|
// The entry date is when the counting starts.
|
||||||
Date from = quantize (Date (task->get ("entry")));
|
Date from = quantize (Date (task->get ("entry")));
|
||||||
|
@ -282,11 +317,11 @@ void Chart::scan (std::vector <Task>& tasks)
|
||||||
if (bars.find (epoch) != bars.end ())
|
if (bars.find (epoch) != bars.end ())
|
||||||
++bars[epoch].removed;
|
++bars[epoch].removed;
|
||||||
|
|
||||||
|
// Maintain a running total of 'done' tasks that are off the left of the
|
||||||
|
// chart.
|
||||||
if (end < earliest)
|
if (end < earliest)
|
||||||
{
|
{
|
||||||
epoch = earliest.toEpoch ();
|
++carryover_done;
|
||||||
if (bars.find (epoch) != bars.end ())
|
|
||||||
++bars[epoch].done;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,21 +457,108 @@ std::string Chart::render ()
|
||||||
for (int i = 0; i < graph_height; ++i)
|
for (int i = 0; i < graph_height; ++i)
|
||||||
grid.replace (LOC (i + 1, max_label + 1), 1, "|");
|
grid.replace (LOC (i + 1, max_label + 1), 1, "|");
|
||||||
|
|
||||||
// TODO Draw y-axis labels.
|
// Determine y-axis labelling.
|
||||||
// char label [12];
|
std::vector <int> labels;
|
||||||
// sprintf (label, "%*d", max_label, labels[2]);
|
yLabels (labels);
|
||||||
// grid.replace (LOC (1, max_label - strlen (label)), strlen (label), label);
|
|
||||||
// sprintf (label, "%*d", max_label, labels[1]);
|
// Draw y-axis labels.
|
||||||
// grid.replace (LOC (1 + (graph_height / 2), max_label - strlen (label)), strlen (label), label);
|
char label [12];
|
||||||
|
sprintf (label, "%*d", max_label, labels[2]);
|
||||||
|
grid.replace (LOC (1, max_label - strlen (label)), strlen (label), label);
|
||||||
|
sprintf (label, "%*d", max_label, labels[1]);
|
||||||
|
grid.replace (LOC (1 + (graph_height / 2), max_label - strlen (label)), strlen (label), label);
|
||||||
grid.replace (LOC (graph_height + 1, max_label - 1), 1, "0");
|
grid.replace (LOC (graph_height + 1, max_label - 1), 1, "0");
|
||||||
|
|
||||||
// Draw x-axis.
|
// Draw x-axis.
|
||||||
grid.replace (LOC (height - 6, max_label + 1), 1, "+");
|
grid.replace (LOC (height - 6, max_label + 1), 1, "+");
|
||||||
grid.replace (LOC (height - 6, max_label + 2), graph_width, std::string (graph_width, '-'));
|
grid.replace (LOC (height - 6, max_label + 2), graph_width, std::string (graph_width, '-'));
|
||||||
|
|
||||||
// TODO Draw x-axis labels.
|
// Draw x-axis labels.
|
||||||
|
std::vector <time_t> bars_in_sequence;
|
||||||
|
std::map <time_t, Bar>::iterator it;
|
||||||
|
for (it = bars.begin (); it != bars.end (); ++it)
|
||||||
|
bars_in_sequence.push_back (it->first);
|
||||||
|
|
||||||
|
std::sort (bars_in_sequence.begin (), bars_in_sequence.end ());
|
||||||
|
std::vector <time_t>::iterator seq;
|
||||||
|
std::string major;
|
||||||
|
for (seq = bars_in_sequence.begin (); seq != bars_in_sequence.end (); ++seq)
|
||||||
|
{
|
||||||
|
Bar bar = bars[*seq];
|
||||||
|
|
||||||
|
// If it fits within the allowed space.
|
||||||
|
if (bar.offset < actual_bars)
|
||||||
|
{
|
||||||
|
grid.replace (LOC (height - 5, max_label + 3 + ((actual_bars - bar.offset - 1) * 3)), bar.minor.length (), bar.minor);
|
||||||
|
|
||||||
|
if (major != bar.major)
|
||||||
|
grid.replace (LOC (height - 4, max_label + 3 + ((actual_bars - bar.offset - 1) * 3)), bar.major.length (), bar.major);
|
||||||
|
|
||||||
|
major = bar.major;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw bars.
|
||||||
|
for (seq = bars_in_sequence.begin (); seq != bars_in_sequence.end (); ++seq)
|
||||||
|
{
|
||||||
|
Bar bar = bars[*seq];
|
||||||
|
|
||||||
|
// If it fits within the allowed space.
|
||||||
|
if (bar.offset < actual_bars)
|
||||||
|
{
|
||||||
|
int pending = (bar.pending * graph_height) / labels[2];
|
||||||
|
int started = (bar.started * graph_height) / labels[2];
|
||||||
|
int done = ((bar.done + carryover_done) * graph_height) / labels[2];
|
||||||
|
|
||||||
|
for (int b = 0; b < pending; ++b)
|
||||||
|
grid.replace (LOC (graph_height - b, max_label + 3 + ((actual_bars - bar.offset - 1) * 3)), 2, "pp");
|
||||||
|
|
||||||
|
for (int b = 0; b < started; ++b)
|
||||||
|
grid.replace (LOC (graph_height - b - pending, max_label + 3 + ((actual_bars - bar.offset - 1) * 3)), 2, "ss");
|
||||||
|
|
||||||
|
for (int b = 0; b < done; ++b)
|
||||||
|
grid.replace (LOC (graph_height - b - pending - started, max_label + 3 + ((actual_bars - bar.offset - 1) * 3)), 2, "dd");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw rates.
|
// Draw rates.
|
||||||
|
/*
|
||||||
|
// Calculate and render the rates.
|
||||||
|
// Calculate 30-day average.
|
||||||
|
int totalAdded30 = 0;
|
||||||
|
int totalRemoved30 = 0;
|
||||||
|
d = (Date () - 30 * 86400).startOfDay ();
|
||||||
|
for (unsigned int i = 0; i < 30; i++)
|
||||||
|
{
|
||||||
|
epoch = d.toEpoch ();
|
||||||
|
|
||||||
|
totalAdded30 += addGroup[epoch];
|
||||||
|
totalRemoved30 += removeGroup[epoch];
|
||||||
|
|
||||||
|
d++;
|
||||||
|
}
|
||||||
|
|
||||||
|
float find_rate30 = 1.0 * totalAdded30 / x_axis.size ();
|
||||||
|
float fix_rate30 = 1.0 * totalRemoved30 / x_axis.size ();
|
||||||
|
|
||||||
|
// Calculate 7-day average.
|
||||||
|
int totalAdded7 = 0;
|
||||||
|
int totalRemoved7 = 0;
|
||||||
|
d = (Date () - 7 * 86400).startOfDay ();
|
||||||
|
for (unsigned int i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
epoch = d.toEpoch ();
|
||||||
|
|
||||||
|
totalAdded7 += addGroup[epoch];
|
||||||
|
totalRemoved7 += removeGroup[epoch];
|
||||||
|
|
||||||
|
d++;
|
||||||
|
}
|
||||||
|
|
||||||
|
float find_rate7 = 1.0 * totalAdded7 / x_axis.size ();
|
||||||
|
float fix_rate7 = 1.0 * totalRemoved7 / x_axis.size ();
|
||||||
|
|
||||||
|
*/
|
||||||
char rate[12];
|
char rate[12];
|
||||||
sprintf (rate, "%.1f", find_rate);
|
sprintf (rate, "%.1f", find_rate);
|
||||||
grid.replace (LOC (height - 2, max_label + 3), 13 + strlen (rate), std::string ("Find rate: ") + rate + "/d");
|
grid.replace (LOC (height - 2, max_label + 3), 13 + strlen (rate), std::string ("Find rate: ") + rate + "/d");
|
||||||
|
@ -445,6 +567,25 @@ std::string Chart::render ()
|
||||||
grid.replace (LOC (height - 1, max_label + 3), 13 + strlen (rate), std::string ("Fix rate: ") + rate + "/d");
|
grid.replace (LOC (height - 1, max_label + 3), 13 + strlen (rate), std::string ("Fix rate: ") + rate + "/d");
|
||||||
|
|
||||||
// Draw completion date.
|
// Draw completion date.
|
||||||
|
/*
|
||||||
|
if (last_pending == 0)
|
||||||
|
{
|
||||||
|
; // Do not render an estimated completion date.
|
||||||
|
}
|
||||||
|
else if (find_rate7 < fix_rate7)
|
||||||
|
{
|
||||||
|
int current_pending = pendingGroup[Date ().startOfDay ().toEpoch ()];
|
||||||
|
float days = 2.0 * current_pending / (fix_rate30 + fix_rate7);
|
||||||
|
Date end;
|
||||||
|
end += (int) (days * 86400);
|
||||||
|
std::string formatted = end.toString (context.config.get ("dateformat"));
|
||||||
|
grid.replace (LOC (height - 2, max_label + 27), 22 + formatted.length (), "Estimated completion: " + formatted);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
grid.replace (LOC (height - 2, max_label + 27), 36, "Estimated completion: No convergence");
|
||||||
|
}
|
||||||
|
*/
|
||||||
if (completion.length ())
|
if (completion.length ())
|
||||||
grid.replace (LOC (height - 2, max_label + 27), 22 + completion.length (), "Estimated completion: " + completion);
|
grid.replace (LOC (height - 2, max_label + 27), 22 + completion.length (), "Estimated completion: " + completion);
|
||||||
|
|
||||||
|
@ -480,7 +621,6 @@ void Chart::optimizeGrid ()
|
||||||
while (grid[non_ws] == ' ')
|
while (grid[non_ws] == ' ')
|
||||||
--non_ws;
|
--non_ws;
|
||||||
|
|
||||||
// std::cout << "# WS at EOL " << non_ws + 1 << "-" << ws << "\n";
|
|
||||||
grid.replace (non_ws + 1, ws - non_ws + 1, "\n");
|
grid.replace (non_ws + 1, ws - non_ws + 1, "\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -673,7 +813,8 @@ void Chart::maxima ()
|
||||||
// Determine max_label.
|
// Determine max_label.
|
||||||
int total = it->second.pending +
|
int total = it->second.pending +
|
||||||
it->second.started +
|
it->second.started +
|
||||||
it->second.done;
|
it->second.done +
|
||||||
|
carryover_done;
|
||||||
|
|
||||||
// Determine max_value.
|
// Determine max_value.
|
||||||
if (total > max_value)
|
if (total > max_value)
|
||||||
|
@ -695,10 +836,9 @@ void Chart::maxima ()
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Given the vertical chart area size (height), the largest value (value),
|
// Given the vertical chart area size (graph_height), the largest value
|
||||||
// populate a vector of labels for the y axis.
|
// (max_value), populate a vector of labels for the y axis.
|
||||||
// TODO Make this a member of Chart.
|
void Chart::yLabels (std::vector <int>& labels)
|
||||||
void calculateYAxis (std::vector <int>& labels, int height, int value)
|
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
double logarithm = log10 ((double) value);
|
double logarithm = log10 ((double) value);
|
||||||
|
@ -736,8 +876,8 @@ void calculateYAxis (std::vector <int>& labels, int height, int value)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// For now, simply select 0, n/2 and n, where n is value rounded up to the
|
// For now, simply select 0, n/2 and n, where n is value rounded up to the
|
||||||
// nearest 10.
|
// nearest 10. This is a poor solution.
|
||||||
int high = value;
|
int high = max_value;
|
||||||
int mod = high % 10;
|
int mod = high % 10;
|
||||||
if (mod)
|
if (mod)
|
||||||
high += 10 - mod;
|
high += 10 - mod;
|
||||||
|
@ -750,67 +890,12 @@ void calculateYAxis (std::vector <int>& labels, int height, int value)
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Graph should render like this:
|
|
||||||
// +---------------------------------------------------------------------+
|
|
||||||
// | |
|
|
||||||
// | 20 | |
|
|
||||||
// | | dd dd dd dd dd dd dd dd |
|
|
||||||
// | | dd dd dd dd dd dd dd dd dd dd dd dd dd dd |
|
|
||||||
// | | pp pp ss ss ss ss ss ss ss ss ss dd dd dd dd dd dd dd Done |
|
|
||||||
// | 10 | pp pp pp pp pp pp ss ss ss ss ss ss dd dd dd dd dd ss Started|
|
|
||||||
// | | pp pp pp pp pp pp pp pp pp pp pp ss ss ss ss dd dd pp Pending|
|
|
||||||
// | | pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp ss dd |
|
|
||||||
// | | pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp |
|
|
||||||
// | 0 +---------------------------------------------------- |
|
|
||||||
// | 21 22 23 24 25 26 27 28 29 30 31 01 02 03 04 05 06 |
|
|
||||||
// | July August |
|
|
||||||
// | |
|
|
||||||
// | Find rate 1.7/d Estimated completion 8/12/2010 |
|
|
||||||
// | Fix rate 1.3/d |
|
|
||||||
// +---------------------------------------------------------------------+
|
|
||||||
//
|
|
||||||
// e = entry
|
|
||||||
// s = start
|
|
||||||
// C = end/Completed
|
|
||||||
// D = end/Deleted
|
|
||||||
// > = Pending/Waiting
|
|
||||||
//
|
|
||||||
// ID 30 31 01 02 03 04 05 06 07 08 09 10
|
|
||||||
// -- ------------------------------------
|
|
||||||
// 1 e-----s--C
|
|
||||||
// 2 e--s-----D
|
|
||||||
// 3 e-----s-------------->
|
|
||||||
// 4 e----------------->
|
|
||||||
// 5 e----->
|
|
||||||
// -- ------------------------------------
|
|
||||||
// pp 1 2 3 3 2 2 2 3 3 3
|
|
||||||
// ss 2 1 1 1 1 1 1 1
|
|
||||||
// dd 1 1 1 1 1 1 1
|
|
||||||
// -- ------------------------------------
|
|
||||||
//
|
|
||||||
// 5 | ss dd dd dd dd
|
|
||||||
// 4 | ss ss dd dd dd ss ss ss
|
|
||||||
// 3 | pp pp ss ss ss pp pp pp
|
|
||||||
// 2 | pp pp pp pp pp pp pp pp pp
|
|
||||||
// 1 | pp pp pp pp pp pp pp pp pp pp
|
|
||||||
// 0 +-------------------------------------
|
|
||||||
// 30 31 01 02 03 04 05 06 07 08 09 10
|
|
||||||
// Oct Nov
|
|
||||||
//
|
|
||||||
int handleReportBurndownDaily (std::string& outs)
|
int handleReportBurndownDaily (std::string& outs)
|
||||||
{
|
{
|
||||||
int rc = 0;
|
int rc = 0;
|
||||||
|
|
||||||
if (context.hooks.trigger ("pre-burndown-command"))
|
if (context.hooks.trigger ("pre-burndown-command"))
|
||||||
{
|
{
|
||||||
std::map <time_t, int> groups;
|
|
||||||
std::map <time_t, int> pendingGroup;
|
|
||||||
std::map <time_t, int> startedGroup;
|
|
||||||
std::map <time_t, int> doneGroup;
|
|
||||||
|
|
||||||
std::map <time_t, int> addGroup;
|
|
||||||
std::map <time_t, int> removeGroup;
|
|
||||||
|
|
||||||
// Scan the pending tasks, applying any filter.
|
// Scan the pending tasks, applying any filter.
|
||||||
std::vector <Task> tasks;
|
std::vector <Task> tasks;
|
||||||
context.tdb.lock (context.config.getBoolean ("locking"));
|
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||||
|
@ -819,428 +904,23 @@ int handleReportBurndownDaily (std::string& outs)
|
||||||
context.tdb.commit ();
|
context.tdb.commit ();
|
||||||
context.tdb.unlock ();
|
context.tdb.unlock ();
|
||||||
|
|
||||||
// How much space is there to render in? This chart will occupy the
|
// Create a chart, scan the tasks, then render.
|
||||||
// maximum space, and the width drives various other parameters.
|
Chart chart ('D');
|
||||||
int width = context.getWidth ();
|
chart.scan (tasks);
|
||||||
int height = context.getHeight () - 1; // Allow for new line with prompt.
|
std::map <time_t, Bar>::iterator it;
|
||||||
|
for (it = chart.bars.begin (); it != chart.bars.end (); ++it)
|
||||||
|
std::cout << "# " << Date (it->first).toString ("YMD")
|
||||||
|
<< " [" << it->second.offset << "] "
|
||||||
|
<< it->second.major << "/" << it->second.minor << " "
|
||||||
|
<< it->second.pending << "p "
|
||||||
|
<< it->second.started << "s "
|
||||||
|
<< it->second.done << "d "
|
||||||
|
<< it->second.added << "a "
|
||||||
|
<< it->second.removed << "r\n";
|
||||||
|
|
||||||
// Estimate how many 'bars' can be dsplayed. This will help subset a
|
outs = chart.render ();
|
||||||
// potentially enormous data set.
|
|
||||||
unsigned int estimate = (width - 1 - 14) / 3;
|
|
||||||
Date now;
|
|
||||||
Date cutoff = (now - (estimate * 86400)).startOfDay ();
|
|
||||||
// std::cout << "# cutoff " << cutoff.toString () << "\n";
|
|
||||||
// std::cout << "# now " << now.toString () << "\n";
|
|
||||||
|
|
||||||
time_t epoch;
|
context.hooks.trigger ("post-burndown-command");
|
||||||
foreach (task, tasks)
|
|
||||||
{
|
|
||||||
// The entry date is when the counting starts.
|
|
||||||
Date from = Date (task->get ("entry")).startOfDay ();
|
|
||||||
addGroup[from.toEpoch ()]++;
|
|
||||||
|
|
||||||
// e--> e--s-->
|
|
||||||
// ppp> pppsss>
|
|
||||||
Task::status status = task->getStatus ();
|
|
||||||
if (status == Task::pending ||
|
|
||||||
status == Task::waiting)
|
|
||||||
{
|
|
||||||
if (task->has ("start"))
|
|
||||||
{
|
|
||||||
Date start = Date (task->get ("start")).startOfDay ();
|
|
||||||
while (from < start)
|
|
||||||
{
|
|
||||||
epoch = from.startOfDay ().toEpoch ();
|
|
||||||
groups[epoch] = 0;
|
|
||||||
++pendingGroup[epoch];
|
|
||||||
from++;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (from < now)
|
|
||||||
{
|
|
||||||
epoch = from.startOfDay ().toEpoch ();
|
|
||||||
groups[epoch] = 0;
|
|
||||||
++startedGroup[epoch];
|
|
||||||
from++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
while (from < now)
|
|
||||||
{
|
|
||||||
epoch = from.startOfDay ().toEpoch ();
|
|
||||||
groups[epoch] = 0;
|
|
||||||
++pendingGroup[epoch];
|
|
||||||
from++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// e--C e--s--C
|
|
||||||
// pppd> pppsssd>
|
|
||||||
else if (status == Task::completed)
|
|
||||||
{
|
|
||||||
// Truncate history so it starts at 'cutoff' for completed tasks.
|
|
||||||
Date end = Date (task->get ("end")).startOfDay ();
|
|
||||||
removeGroup[end.toEpoch ()]++;
|
|
||||||
|
|
||||||
if (end < cutoff)
|
|
||||||
{
|
|
||||||
++doneGroup[cutoff.toEpoch ()];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task->has ("start"))
|
|
||||||
{
|
|
||||||
Date start = Date (task->get ("start")).startOfDay ();
|
|
||||||
while (from < start)
|
|
||||||
{
|
|
||||||
epoch = from.startOfDay ().toEpoch ();
|
|
||||||
groups[epoch] = 0;
|
|
||||||
++pendingGroup[epoch];
|
|
||||||
from++;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (from < end)
|
|
||||||
{
|
|
||||||
epoch = from.startOfDay ().toEpoch ();
|
|
||||||
groups[epoch] = 0;
|
|
||||||
++startedGroup[epoch];
|
|
||||||
from++;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (from < now)
|
|
||||||
{
|
|
||||||
epoch = from.startOfDay ().toEpoch ();
|
|
||||||
groups[epoch] = 0;
|
|
||||||
++doneGroup[epoch];
|
|
||||||
from++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Date end = Date (task->get ("end")).startOfDay ();
|
|
||||||
while (from < end)
|
|
||||||
{
|
|
||||||
epoch = from.startOfDay ().toEpoch ();
|
|
||||||
groups[epoch] = 0;
|
|
||||||
++pendingGroup[epoch];
|
|
||||||
from++;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (from < now)
|
|
||||||
{
|
|
||||||
epoch = from.startOfDay ().toEpoch ();
|
|
||||||
groups[epoch] = 0;
|
|
||||||
++doneGroup[epoch];
|
|
||||||
from++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// e--D e--s--D
|
|
||||||
// ppp pppsss
|
|
||||||
else if (status == Task::deleted)
|
|
||||||
{
|
|
||||||
// Skip old deleted tasks.
|
|
||||||
Date end = Date (task->get ("end")).startOfDay ();
|
|
||||||
removeGroup[end.toEpoch ()]++;
|
|
||||||
|
|
||||||
if (end < cutoff)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (task->has ("start"))
|
|
||||||
{
|
|
||||||
Date start = Date (task->get ("start")).startOfDay ();
|
|
||||||
while (from < start)
|
|
||||||
{
|
|
||||||
epoch = from.startOfDay ().toEpoch ();
|
|
||||||
groups[epoch] = 0;
|
|
||||||
++pendingGroup[epoch];
|
|
||||||
from++;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (from < end)
|
|
||||||
{
|
|
||||||
epoch = from.startOfDay ().toEpoch ();
|
|
||||||
groups[epoch] = 0;
|
|
||||||
++startedGroup[epoch];
|
|
||||||
from++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Date end = Date (task->get ("end")).startOfDay ();
|
|
||||||
while (from < end)
|
|
||||||
{
|
|
||||||
epoch = from.startOfDay ().toEpoch ();
|
|
||||||
groups[epoch] = 0;
|
|
||||||
++pendingGroup[epoch];
|
|
||||||
from++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
// TODO Render.
|
|
||||||
foreach (g, groups)
|
|
||||||
{
|
|
||||||
std::stringstream s;
|
|
||||||
s << Date (g->first).toISO () << " "
|
|
||||||
<< pendingGroup[g->first] << "/"
|
|
||||||
<< startedGroup[g->first] << "/"
|
|
||||||
<< doneGroup[g->first] << "\n";
|
|
||||||
outs += s.str ();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (groups.size ())
|
|
||||||
{
|
|
||||||
// Horizontal Breakdown
|
|
||||||
//
|
|
||||||
// >25 | xx ... xx ll pending<
|
|
||||||
// ^ left margin
|
|
||||||
// ^^ max_label
|
|
||||||
// ^ gap
|
|
||||||
// ^ axis
|
|
||||||
// ^^^ gap + bar
|
|
||||||
// ^^^ gap + bar
|
|
||||||
// ^^ gap
|
|
||||||
// ^^ legend swatch
|
|
||||||
// ^ gap
|
|
||||||
// ^^^^^^^ "Pending", "Started" & "Done"
|
|
||||||
// ^ right margin
|
|
||||||
|
|
||||||
// Vertical Breakdown
|
|
||||||
//
|
|
||||||
// v top margin
|
|
||||||
// blank line
|
|
||||||
// | all remaining space
|
|
||||||
// - x axis
|
|
||||||
// 9 day/week/month
|
|
||||||
// 9 month/-/year
|
|
||||||
// blank line
|
|
||||||
// f find rate
|
|
||||||
// f fix rate
|
|
||||||
// ^ bottom margin
|
|
||||||
|
|
||||||
// What is the longest y-axis label? This is tricky. Having
|
|
||||||
// optimistically estimate the number of bars to be shown, then determine
|
|
||||||
// the longest label of the records that lie within the observable range.
|
|
||||||
// It is important to consider that there may be zero -> bars number of
|
|
||||||
// records that match.
|
|
||||||
int max_label = 1;
|
|
||||||
int max_value = 0;
|
|
||||||
|
|
||||||
std::vector <time_t> x_axis;
|
|
||||||
Date now;
|
|
||||||
for (unsigned int i = 0; i < estimate; ++i)
|
|
||||||
{
|
|
||||||
Date x = (now - (i * 86400)).startOfDay ();
|
|
||||||
x_axis.push_back (x.toEpoch ());
|
|
||||||
|
|
||||||
int total = pendingGroup[x.toEpoch ()] +
|
|
||||||
startedGroup[x.toEpoch ()] +
|
|
||||||
doneGroup[x.toEpoch ()];
|
|
||||||
|
|
||||||
if (total > max_value)
|
|
||||||
max_value = total;
|
|
||||||
|
|
||||||
int length = (int) log10 ((double) total) + 1;
|
|
||||||
if (length > max_label)
|
|
||||||
max_label = length;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::sort (x_axis.begin (), x_axis.end ());
|
|
||||||
|
|
||||||
// How many bars can be shown?
|
|
||||||
unsigned int bars = (width - max_label - 14) / 3;
|
|
||||||
int graph_width = width - max_label - 14;
|
|
||||||
|
|
||||||
// Make them match
|
|
||||||
while (bars < x_axis.size ())
|
|
||||||
x_axis.erase (x_axis.begin ());
|
|
||||||
|
|
||||||
// Determine the y-axis.
|
|
||||||
int graph_height = height - 7;
|
|
||||||
|
|
||||||
if (graph_height < 5 ||
|
|
||||||
graph_width < 4)
|
|
||||||
{
|
|
||||||
outs = "Terminal window too small to draw a graph.\n";
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine y-axis labelling.
|
|
||||||
std::vector <int> labels;
|
|
||||||
calculateYAxis (labels, graph_height, max_value);
|
|
||||||
// foreach (i, labels)
|
|
||||||
// std::cout << "# label " << *i << "\n";
|
|
||||||
|
|
||||||
// std::cout << "# estimate " << estimate << " bars\n";
|
|
||||||
// std::cout << "# actually " << bars << " bars\n";
|
|
||||||
// std::cout << "# graph width " << graph_width << "\n";
|
|
||||||
// std::cout << "# graph height " << graph_height << "\n";
|
|
||||||
// std::cout << "# days " << x_axis.size () << "\n";
|
|
||||||
// std::cout << "# max label " << max_label << "\n";
|
|
||||||
// std::cout << "# max value " << max_value << "\n";
|
|
||||||
|
|
||||||
// Determine the start date.
|
|
||||||
Date start = (Date () - ((bars - 1) * 86400)).startOfDay ();
|
|
||||||
// std::cout << "# start " << start.toISO () << "\n";
|
|
||||||
|
|
||||||
// Compose the grid.
|
|
||||||
std::string grid;
|
|
||||||
for (int i = 0; i < height; ++i)
|
|
||||||
grid += std::string (width, ' ') + "\n";
|
|
||||||
|
|
||||||
// Draw legend.
|
|
||||||
grid.replace (LOC (graph_height / 2 - 1, width - 10), 10, "dd Done ");
|
|
||||||
grid.replace (LOC (graph_height / 2, width - 10), 10, "ss Started");
|
|
||||||
grid.replace (LOC (graph_height / 2 + 1, width - 10), 10, "pp Pending");
|
|
||||||
|
|
||||||
// Draw x-axis.
|
|
||||||
grid.replace (LOC (height - 6, max_label + 1), 1, "+");
|
|
||||||
grid.replace (LOC (height - 6, max_label + 2), graph_width, std::string (graph_width, '-'));
|
|
||||||
|
|
||||||
int month = 0;
|
|
||||||
Date d (start);
|
|
||||||
for (unsigned int i = 0; i < bars; ++i)
|
|
||||||
{
|
|
||||||
if (month != d.month ())
|
|
||||||
grid.replace (LOC (height - 4, max_label + 3 + (i * 3)), 3, Date::monthName (d.month ()).substr (0, 3));
|
|
||||||
|
|
||||||
char day [3];
|
|
||||||
sprintf (day, "%02d", d.day ());
|
|
||||||
grid.replace (LOC (height - 5, max_label + 3 + (i * 3)), 2, day);
|
|
||||||
|
|
||||||
month = d.month ();
|
|
||||||
d++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the y-axis.
|
|
||||||
for (int i = 0; i < graph_height; ++i)
|
|
||||||
grid.replace (LOC (i + 1, max_label + 1), 1, "|");
|
|
||||||
|
|
||||||
char label [12];
|
|
||||||
sprintf (label, "%*d", max_label, labels[2]);
|
|
||||||
grid.replace (LOC (1, max_label - strlen (label)), strlen (label), label);
|
|
||||||
sprintf (label, "%*d", max_label, labels[1]);
|
|
||||||
grid.replace (LOC (1 + (graph_height / 2), max_label - strlen (label)), strlen (label), label);
|
|
||||||
grid.replace (LOC (graph_height + 1, max_label - 1), 1, "0");
|
|
||||||
|
|
||||||
// Draw the bars.
|
|
||||||
int last_pending = 0;
|
|
||||||
d = start;
|
|
||||||
for (unsigned int i = 0; i < bars; ++i)
|
|
||||||
{
|
|
||||||
epoch = d.toEpoch ();
|
|
||||||
int pending = (pendingGroup[epoch] * graph_height) / labels[2];
|
|
||||||
int started = (startedGroup[epoch] * graph_height) / labels[2];
|
|
||||||
int done = (doneGroup[epoch] * graph_height) / labels[2];
|
|
||||||
|
|
||||||
// Track the latest pending count, for convergence calculation.
|
|
||||||
last_pending = pendingGroup[epoch] + startedGroup[epoch];
|
|
||||||
|
|
||||||
for (int b = 0; b < pending; ++b)
|
|
||||||
grid.replace (LOC (graph_height - b, max_label + 3 + (i * 3)), 2, "pp");
|
|
||||||
|
|
||||||
for (int b = 0; b < started; ++b)
|
|
||||||
grid.replace (LOC (graph_height - b - pending, max_label + 3 + (i * 3)), 2, "ss");
|
|
||||||
|
|
||||||
for (int b = 0; b < done; ++b)
|
|
||||||
grid.replace (LOC (graph_height - b - pending - started, max_label + 3 + (i * 3)), 2, "dd");
|
|
||||||
|
|
||||||
d++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// std::cout << "# last pending " << last_pending << "\n";
|
|
||||||
|
|
||||||
// Calculate and render the rates.
|
|
||||||
// Calculate 30-day average.
|
|
||||||
int totalAdded30 = 0;
|
|
||||||
int totalRemoved30 = 0;
|
|
||||||
d = (Date () - 30 * 86400).startOfDay ();
|
|
||||||
for (unsigned int i = 0; i < 30; i++)
|
|
||||||
{
|
|
||||||
epoch = d.toEpoch ();
|
|
||||||
|
|
||||||
totalAdded30 += addGroup[epoch];
|
|
||||||
totalRemoved30 += removeGroup[epoch];
|
|
||||||
|
|
||||||
d++;
|
|
||||||
}
|
|
||||||
|
|
||||||
float find_rate30 = 1.0 * totalAdded30 / x_axis.size ();
|
|
||||||
float fix_rate30 = 1.0 * totalRemoved30 / x_axis.size ();
|
|
||||||
|
|
||||||
// Calculate 7-day average.
|
|
||||||
int totalAdded7 = 0;
|
|
||||||
int totalRemoved7 = 0;
|
|
||||||
d = (Date () - 7 * 86400).startOfDay ();
|
|
||||||
for (unsigned int i = 0; i < 7; i++)
|
|
||||||
{
|
|
||||||
epoch = d.toEpoch ();
|
|
||||||
|
|
||||||
totalAdded7 += addGroup[epoch];
|
|
||||||
totalRemoved7 += removeGroup[epoch];
|
|
||||||
|
|
||||||
d++;
|
|
||||||
}
|
|
||||||
|
|
||||||
float find_rate7 = 1.0 * totalAdded7 / x_axis.size ();
|
|
||||||
float fix_rate7 = 1.0 * totalRemoved7 / x_axis.size ();
|
|
||||||
|
|
||||||
// Render rates.
|
|
||||||
char rate[12];
|
|
||||||
sprintf (rate, "%.1f", (find_rate30 + find_rate7) / 2.0);
|
|
||||||
grid.replace (LOC (height - 2, max_label + 3), 13 + strlen (rate), std::string ("Find rate: ") + rate + "/d");
|
|
||||||
|
|
||||||
sprintf (rate, "%.1f", (fix_rate30 + fix_rate7) / 2.0);
|
|
||||||
grid.replace (LOC (height - 1, max_label + 3), 13 + strlen (rate), std::string ("Fix rate: ") + rate + "/d");
|
|
||||||
|
|
||||||
if (last_pending == 0)
|
|
||||||
{
|
|
||||||
; // Do not render an estimated completion date.
|
|
||||||
}
|
|
||||||
else if (find_rate7 < fix_rate7)
|
|
||||||
{
|
|
||||||
int current_pending = pendingGroup[Date ().startOfDay ().toEpoch ()];
|
|
||||||
float days = 2.0 * current_pending / (fix_rate30 + fix_rate7);
|
|
||||||
Date end;
|
|
||||||
end += (int) (days * 86400);
|
|
||||||
std::string formatted = end.toString (context.config.get ("dateformat"));
|
|
||||||
grid.replace (LOC (height - 2, max_label + 27), 22 + formatted.length (), "Estimated completion: " + formatted);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
grid.replace (LOC (height - 2, max_label + 27), 36, "Estimated completion: No convergence");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output the grid.
|
|
||||||
Color color_pending (context.config.get ("color.burndown.pending"));
|
|
||||||
Color color_done (context.config.get ("color.burndown.done"));
|
|
||||||
Color color_started (context.config.get ("color.burndown.started"));
|
|
||||||
|
|
||||||
// Replace dd, ss, pp with colored strings.
|
|
||||||
// TODO Use configurable values.
|
|
||||||
std::string::size_type i;
|
|
||||||
while ((i = grid.find ("pp")) != std::string::npos)
|
|
||||||
grid.replace (i, 2, color_pending.colorize (" "));
|
|
||||||
|
|
||||||
while ((i = grid.find ("ss")) != std::string::npos)
|
|
||||||
grid.replace (i, 2, color_started.colorize (" "));
|
|
||||||
|
|
||||||
while ((i = grid.find ("dd")) != std::string::npos)
|
|
||||||
grid.replace (i, 2, color_done.colorize (" "));
|
|
||||||
|
|
||||||
outs += grid;
|
|
||||||
|
|
||||||
context.hooks.trigger ("post-burndown-command");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
outs = "No matches.\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rc;
|
return rc;
|
||||||
|
@ -1266,16 +946,14 @@ int handleReportBurndownWeekly (std::string& outs)
|
||||||
chart.scan (tasks);
|
chart.scan (tasks);
|
||||||
std::map <time_t, Bar>::iterator it;
|
std::map <time_t, Bar>::iterator it;
|
||||||
for (it = chart.bars.begin (); it != chart.bars.end (); ++it)
|
for (it = chart.bars.begin (); it != chart.bars.end (); ++it)
|
||||||
std::cout << "# bar " << Date (it->first).toString ("YMD")
|
std::cout << "# " << Date (it->first).toString ("YMD")
|
||||||
<< " offset=" << it->second.offset
|
<< " [" << it->second.offset << "] "
|
||||||
<< " major=" << it->second.major
|
<< it->second.major << "/" << it->second.minor << " "
|
||||||
<< " minor=" << it->second.minor
|
<< it->second.pending << "p "
|
||||||
<< " pending=" << it->second.pending
|
<< it->second.started << "s "
|
||||||
<< " started=" << it->second.started
|
<< it->second.done << "d "
|
||||||
<< " done=" << it->second.done
|
<< it->second.added << "a "
|
||||||
<< " added=" << it->second.added
|
<< it->second.removed << "r\n";
|
||||||
<< " removed=" << it->second.removed
|
|
||||||
<< "\n";
|
|
||||||
|
|
||||||
outs = chart.render ();
|
outs = chart.render ();
|
||||||
|
|
||||||
|
@ -1305,16 +983,14 @@ int handleReportBurndownMonthly (std::string& outs)
|
||||||
chart.scan (tasks);
|
chart.scan (tasks);
|
||||||
std::map <time_t, Bar>::iterator it;
|
std::map <time_t, Bar>::iterator it;
|
||||||
for (it = chart.bars.begin (); it != chart.bars.end (); ++it)
|
for (it = chart.bars.begin (); it != chart.bars.end (); ++it)
|
||||||
std::cout << "# bar " << Date (it->first).toString ("YMD")
|
std::cout << "# " << Date (it->first).toString ("YMD")
|
||||||
<< " offset=" << it->second.offset
|
<< " [" << it->second.offset << "] "
|
||||||
<< " major=" << it->second.major
|
<< it->second.major << "/" << it->second.minor << " "
|
||||||
<< " minor=" << it->second.minor
|
<< it->second.pending << "p "
|
||||||
<< " pending=" << it->second.pending
|
<< it->second.started << "s "
|
||||||
<< " started=" << it->second.started
|
<< it->second.done << "d "
|
||||||
<< " done=" << it->second.done
|
<< it->second.added << "a "
|
||||||
<< " added=" << it->second.added
|
<< it->second.removed << "r\n";
|
||||||
<< " removed=" << it->second.removed
|
|
||||||
<< "\n";
|
|
||||||
|
|
||||||
outs = chart.render ();
|
outs = chart.render ();
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue