mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-08-20 04:13:07 +02:00
Burndown Chart
- Infrastructure rewritten with the following benefits: - greater performance - reduced memory footprint - generalized periods (daily, weekly, monthly) - Currently graphs do not render, daily chart is still the old implementation
This commit is contained in:
parent
652b7d9c8d
commit
c4a5a75fd6
2 changed files with 738 additions and 99 deletions
742
src/burndown.cpp
742
src/burndown.cpp
|
@ -26,40 +26,678 @@
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
#include <iostream> // TODO Remove
|
#include <iostream> // TODO Remove
|
||||||
//#include <iomanip>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
//#include <fstream>
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
//#include <sys/types.h>
|
|
||||||
//#include <stdio.h>
|
|
||||||
//#include <unistd.h>
|
|
||||||
//#include <stdlib.h>
|
|
||||||
//#include <pwd.h>
|
|
||||||
//#include <time.h>
|
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
#include "Context.h"
|
#include <Context.h>
|
||||||
//#include "Directory.h"
|
#include <Date.h>
|
||||||
//#include "File.h"
|
#include <text.h>
|
||||||
#include "Date.h"
|
#include <util.h>
|
||||||
//#include "Duration.h"
|
#include <main.h>
|
||||||
//#include "Table.h"
|
|
||||||
#include "text.h"
|
|
||||||
#include "util.h"
|
|
||||||
#include "main.h"
|
|
||||||
|
|
||||||
//#ifdef HAVE_LIBNCURSES
|
|
||||||
//#include <ncurses.h>
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
extern Context context;
|
extern Context context;
|
||||||
|
|
||||||
// Helper macro.
|
// Helper macro.
|
||||||
#define LOC(y,x) (((y) * (width + 1)) + (x))
|
#define LOC(y,x) (((y) * (width + 1)) + (x))
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
class Bar
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Bar ();
|
||||||
|
Bar (const Bar&);
|
||||||
|
Bar& operator= (const Bar&);
|
||||||
|
~Bar ();
|
||||||
|
|
||||||
|
public:
|
||||||
|
int offset; // from left of chart
|
||||||
|
std::string major; // x-axis label, major (year/-/month)
|
||||||
|
std::string minor; // x-axis label, minor (month/week/day)
|
||||||
|
int pending; // Number of pending task in period
|
||||||
|
int started; // Number of started task in period
|
||||||
|
int done; // Number of done task in period
|
||||||
|
int added; // Number added in period
|
||||||
|
int removed; // Number removed in period
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Bar::Bar ()
|
||||||
|
: offset (0)
|
||||||
|
, major ("")
|
||||||
|
, minor ("")
|
||||||
|
, pending (0)
|
||||||
|
, started (0)
|
||||||
|
, done (0)
|
||||||
|
, added (0)
|
||||||
|
, removed (0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Bar::Bar (const Bar& other)
|
||||||
|
{
|
||||||
|
*this = other;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Bar& Bar::operator= (const Bar& other)
|
||||||
|
{
|
||||||
|
if (this != &other)
|
||||||
|
{
|
||||||
|
offset = other.offset;
|
||||||
|
major = other.major;
|
||||||
|
minor = other.minor;
|
||||||
|
pending = other.pending;
|
||||||
|
started = other.started;
|
||||||
|
done = other.done;
|
||||||
|
added = other.added;
|
||||||
|
removed = other.removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Bar::~Bar ()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
class Chart
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Chart (char);
|
||||||
|
Chart (const Chart&);
|
||||||
|
Chart& operator= (const Chart&);
|
||||||
|
~Chart ();
|
||||||
|
|
||||||
|
void scan (std::vector <Task>&);
|
||||||
|
std::string render ();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void generateBars ();
|
||||||
|
void optimizeGrid ();
|
||||||
|
Date quantize (const Date&);
|
||||||
|
|
||||||
|
Date increment (const Date&);
|
||||||
|
Date decrement (const Date&);
|
||||||
|
void maxima ();
|
||||||
|
|
||||||
|
public:
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
int graph_width;
|
||||||
|
int graph_height;
|
||||||
|
int max_value;
|
||||||
|
int max_label;
|
||||||
|
std::vector <int> labels;
|
||||||
|
int estimated_bars;
|
||||||
|
int actual_bars;
|
||||||
|
std::map <time_t, Bar> bars;
|
||||||
|
Date earliest;
|
||||||
|
char period; // D, W, M.
|
||||||
|
std::string grid;
|
||||||
|
|
||||||
|
float find_rate;
|
||||||
|
float fix_rate;
|
||||||
|
std::string completion;
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Chart::Chart (char type)
|
||||||
|
{
|
||||||
|
// How much space is there to render in? This chart will occupy the
|
||||||
|
// maximum space, and the width drives various other parameters.
|
||||||
|
width = context.getWidth ();
|
||||||
|
height = context.getHeight () - 1; // Allow for new line with prompt.
|
||||||
|
std::cout << "# width " << width << "\n";
|
||||||
|
std::cout << "# height " << height << "\n";
|
||||||
|
|
||||||
|
max_value = 0;
|
||||||
|
std::cout << "# max_value " << max_value << "\n";
|
||||||
|
max_label = 1;
|
||||||
|
std::cout << "# max_label " << max_label << "\n";
|
||||||
|
|
||||||
|
graph_height = height - 7;
|
||||||
|
std::cout << "# graph_height " << graph_height << "\n";
|
||||||
|
graph_width = width - max_label - 14;
|
||||||
|
std::cout << "# graph_width " << graph_width << "\n";
|
||||||
|
|
||||||
|
// Estimate how many 'bars' can be dsplayed. This will help subset a
|
||||||
|
// potentially enormous data set.
|
||||||
|
estimated_bars = (width - 1 - 14) / 3;
|
||||||
|
std::cout << "# estimated_bars " << estimated_bars << "\n";
|
||||||
|
|
||||||
|
actual_bars = 0;
|
||||||
|
std::cout << "# actual_bars " << actual_bars << "\n";
|
||||||
|
|
||||||
|
period = type;
|
||||||
|
std::cout << "# period " << period << "\n";
|
||||||
|
|
||||||
|
// Rates are calculated last.
|
||||||
|
find_rate = 0.0;
|
||||||
|
fix_rate = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Chart::Chart (const Chart& other)
|
||||||
|
{
|
||||||
|
*this = other;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Chart& Chart::operator= (const Chart& other)
|
||||||
|
{
|
||||||
|
if (this != &other)
|
||||||
|
{
|
||||||
|
width = other.width;
|
||||||
|
height = other.height;
|
||||||
|
graph_width = other.graph_width;
|
||||||
|
graph_height = other.graph_height;
|
||||||
|
max_value = other. max_value;
|
||||||
|
max_label = other.max_label;
|
||||||
|
labels = other.labels;
|
||||||
|
estimated_bars = other.estimated_bars;
|
||||||
|
actual_bars = other.actual_bars;
|
||||||
|
bars = other.bars;
|
||||||
|
earliest = other.earliest;
|
||||||
|
period = other.period;
|
||||||
|
grid = other.grid;
|
||||||
|
find_rate = other.find_rate;
|
||||||
|
fix_rate = other.fix_rate;
|
||||||
|
completion = other.completion;
|
||||||
|
}
|
||||||
|
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Chart::~Chart ()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
void Chart::scan (std::vector <Task>& tasks)
|
||||||
|
{
|
||||||
|
generateBars ();
|
||||||
|
|
||||||
|
std::cout << "# loaded " << tasks.size () << " tasks\n";
|
||||||
|
|
||||||
|
// Not quantized, so that "while (xxx < now)" is inclusive.
|
||||||
|
Date now;
|
||||||
|
|
||||||
|
time_t epoch;
|
||||||
|
foreach (task, tasks)
|
||||||
|
{
|
||||||
|
// The entry date is when the counting starts.
|
||||||
|
Date from = quantize (Date (task->get ("entry")));
|
||||||
|
epoch = from.toEpoch ();
|
||||||
|
|
||||||
|
if (bars.find (epoch) != bars.end ())
|
||||||
|
++bars[epoch].added;
|
||||||
|
|
||||||
|
// e--> e--s-->
|
||||||
|
// ppp> pppsss>
|
||||||
|
Task::status status = task->getStatus ();
|
||||||
|
if (status == Task::pending ||
|
||||||
|
status == Task::waiting)
|
||||||
|
{
|
||||||
|
if (task->has ("start"))
|
||||||
|
{
|
||||||
|
Date start = quantize (Date (task->get ("start")));
|
||||||
|
while (from < start)
|
||||||
|
{
|
||||||
|
epoch = from.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ()) ++bars[epoch].pending;
|
||||||
|
from = increment (from);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (from < now)
|
||||||
|
{
|
||||||
|
epoch = from.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ()) ++bars[epoch].started;
|
||||||
|
from = increment (from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
while (from < now)
|
||||||
|
{
|
||||||
|
epoch = from.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ()) ++bars[epoch].pending;
|
||||||
|
from = increment (from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// e--C e--s--C
|
||||||
|
// pppd> pppsssd>
|
||||||
|
else if (status == Task::completed)
|
||||||
|
{
|
||||||
|
// Truncate history so it starts at 'earliest' for completed tasks.
|
||||||
|
Date end = quantize (Date (task->get ("end")));
|
||||||
|
epoch = end.toEpoch ();
|
||||||
|
|
||||||
|
if (bars.find (epoch) != bars.end ())
|
||||||
|
++bars[epoch].removed;
|
||||||
|
|
||||||
|
if (end < earliest)
|
||||||
|
{
|
||||||
|
epoch = earliest.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ())
|
||||||
|
++bars[epoch].done;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task->has ("start"))
|
||||||
|
{
|
||||||
|
Date start = quantize (Date (task->get ("start")));
|
||||||
|
while (from < start)
|
||||||
|
{
|
||||||
|
epoch = from.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ()) ++bars[epoch].pending;
|
||||||
|
from = increment (from);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (from < end)
|
||||||
|
{
|
||||||
|
epoch = from.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ()) ++bars[epoch].started;
|
||||||
|
from = increment (from);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (from < now)
|
||||||
|
{
|
||||||
|
epoch = from.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ()) ++bars[epoch].done;
|
||||||
|
from = increment (from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Date end = quantize (Date (task->get ("end")));
|
||||||
|
while (from < end)
|
||||||
|
{
|
||||||
|
epoch = from.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ()) ++bars[epoch].pending;
|
||||||
|
from = increment (from);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (from < now)
|
||||||
|
{
|
||||||
|
epoch = from.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ()) ++bars[epoch].done;
|
||||||
|
from = increment (from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// e--D e--s--D
|
||||||
|
// ppp pppsss
|
||||||
|
else if (status == Task::deleted)
|
||||||
|
{
|
||||||
|
// Skip old deleted tasks.
|
||||||
|
Date end = quantize (Date (task->get ("end")));
|
||||||
|
epoch = end.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ())
|
||||||
|
++bars[epoch].removed;
|
||||||
|
|
||||||
|
if (end < earliest)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (task->has ("start"))
|
||||||
|
{
|
||||||
|
Date start = quantize (Date (task->get ("start")));
|
||||||
|
while (from < start)
|
||||||
|
{
|
||||||
|
epoch = from.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ()) ++bars[epoch].pending;
|
||||||
|
from = increment (from);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (from < end)
|
||||||
|
{
|
||||||
|
epoch = from.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ()) ++bars[epoch].started;
|
||||||
|
from = increment (from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Date end = quantize (Date (task->get ("end")));
|
||||||
|
while (from < end)
|
||||||
|
{
|
||||||
|
epoch = from.toEpoch ();
|
||||||
|
if (bars.find (epoch) != bars.end ()) ++bars[epoch].pending;
|
||||||
|
from = increment (from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size the data.
|
||||||
|
maxima ();
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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 |
|
||||||
|
// +---------------------------------------------------------------------+
|
||||||
|
std::string Chart::render ()
|
||||||
|
{
|
||||||
|
if (graph_height < 5 || // a 4-line graph is essentially unreadable.
|
||||||
|
graph_width < 2) // A single-bar graph is useless.
|
||||||
|
{
|
||||||
|
return "Terminal window too small to draw a graph.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a grid, folded into a string.
|
||||||
|
// TODO Upgrade grid to a vector of strings, for simpler optimization.
|
||||||
|
grid = "";
|
||||||
|
for (int i = 0; i < height; ++i)
|
||||||
|
grid += std::string (width, ' ') + "\n";
|
||||||
|
|
||||||
|
// 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 y-axis.
|
||||||
|
for (int i = 0; i < graph_height; ++i)
|
||||||
|
grid.replace (LOC (i + 1, max_label + 1), 1, "|");
|
||||||
|
|
||||||
|
// TODO Draw y-axis labels.
|
||||||
|
// 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 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, '-'));
|
||||||
|
|
||||||
|
// TODO Draw x-axis labels.
|
||||||
|
|
||||||
|
// Draw rates.
|
||||||
|
char rate[12];
|
||||||
|
sprintf (rate, "%.1f", find_rate);
|
||||||
|
grid.replace (LOC (height - 2, max_label + 3), 13 + strlen (rate), std::string ("Find rate: ") + rate + "/d");
|
||||||
|
|
||||||
|
sprintf (rate, "%.1f", fix_rate);
|
||||||
|
grid.replace (LOC (height - 1, max_label + 3), 13 + strlen (rate), std::string ("Fix rate: ") + rate + "/d");
|
||||||
|
|
||||||
|
// Draw completion date.
|
||||||
|
if (completion.length ())
|
||||||
|
grid.replace (LOC (height - 2, max_label + 27), 22 + completion.length (), "Estimated completion: " + completion);
|
||||||
|
|
||||||
|
optimizeGrid ();
|
||||||
|
|
||||||
|
// Colorize 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.
|
||||||
|
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 (" "));
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// grid =~ /\s+$//g
|
||||||
|
void Chart::optimizeGrid ()
|
||||||
|
{
|
||||||
|
std::string::size_type ws;
|
||||||
|
while ((ws = grid.find (" \n")) != std::string::npos)
|
||||||
|
{
|
||||||
|
std::string::size_type non_ws = ws;
|
||||||
|
while (grid[non_ws] == ' ')
|
||||||
|
--non_ws;
|
||||||
|
|
||||||
|
// std::cout << "# WS at EOL " << non_ws + 1 << "-" << ws << "\n";
|
||||||
|
grid.replace (non_ws + 1, ws - non_ws + 1, "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Date Chart::quantize (const Date& input)
|
||||||
|
{
|
||||||
|
if (period == 'D') return input.startOfDay ();
|
||||||
|
if (period == 'W') return input.startOfWeek ();
|
||||||
|
if (period == 'M') return input.startOfMonth ();
|
||||||
|
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Date Chart::increment (const Date& input)
|
||||||
|
{
|
||||||
|
// Move to the next period.
|
||||||
|
int d = input.day ();
|
||||||
|
int m = input.month ();
|
||||||
|
int y = input.year ();
|
||||||
|
|
||||||
|
int days;
|
||||||
|
|
||||||
|
switch (period)
|
||||||
|
{
|
||||||
|
case 'D':
|
||||||
|
if (++d > Date::daysInMonth (m, y))
|
||||||
|
{
|
||||||
|
d = 1;
|
||||||
|
|
||||||
|
if (++m == 13)
|
||||||
|
{
|
||||||
|
m = 1;
|
||||||
|
++y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'W':
|
||||||
|
d += 7;
|
||||||
|
days = Date::daysInMonth (m, y);
|
||||||
|
if (d > days)
|
||||||
|
{
|
||||||
|
d -= days;
|
||||||
|
|
||||||
|
if (++m == 13)
|
||||||
|
{
|
||||||
|
m = 1;
|
||||||
|
++y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'M':
|
||||||
|
d = 1;
|
||||||
|
if (++m == 13)
|
||||||
|
{
|
||||||
|
m = 1;
|
||||||
|
++y;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Date (m, d, y, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
Date Chart::decrement (const Date& input)
|
||||||
|
{
|
||||||
|
// Move to the previous period.
|
||||||
|
int d = input.day ();
|
||||||
|
int m = input.month ();
|
||||||
|
int y = input.year ();
|
||||||
|
|
||||||
|
switch (period)
|
||||||
|
{
|
||||||
|
case 'D':
|
||||||
|
if (--d == 0)
|
||||||
|
{
|
||||||
|
if (--m == 0)
|
||||||
|
{
|
||||||
|
m = 12;
|
||||||
|
--y;
|
||||||
|
}
|
||||||
|
|
||||||
|
d = Date::daysInMonth (m, y);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'W':
|
||||||
|
d -= 7;
|
||||||
|
if (d < 1)
|
||||||
|
{
|
||||||
|
if (--m == 0)
|
||||||
|
{
|
||||||
|
m = 12;
|
||||||
|
y--;
|
||||||
|
}
|
||||||
|
|
||||||
|
d += Date::daysInMonth (m, y);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'M':
|
||||||
|
d = 1;
|
||||||
|
if (--m == 0)
|
||||||
|
{
|
||||||
|
m = 12;
|
||||||
|
--y;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Date (m, d, y, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Do 'bars[epoch] = Bar' for every bar that may appear on a chart.
|
||||||
|
void Chart::generateBars ()
|
||||||
|
{
|
||||||
|
Bar bar;
|
||||||
|
|
||||||
|
// Determine the last bar date.
|
||||||
|
Date cursor;
|
||||||
|
switch (period)
|
||||||
|
{
|
||||||
|
case 'D': cursor = Date ().startOfDay (); break;
|
||||||
|
case 'W': cursor = Date ().startOfWeek (); break;
|
||||||
|
case 'M': cursor = Date ().startOfMonth (); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate and determine all the other bar dates.
|
||||||
|
char str[12];
|
||||||
|
for (int i = 0; i < estimated_bars; ++i)
|
||||||
|
{
|
||||||
|
// Create the major and minor labels.
|
||||||
|
switch (period)
|
||||||
|
{
|
||||||
|
case 'D': // month/day
|
||||||
|
{
|
||||||
|
std::string month = Date::monthName (cursor.month ());
|
||||||
|
bar.major = month.substr (0, 3);
|
||||||
|
|
||||||
|
sprintf (str, "%02d", cursor.day ());
|
||||||
|
bar.minor = str;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'W': // year/week
|
||||||
|
sprintf (str, "%d", cursor.year ());
|
||||||
|
bar.major = str;
|
||||||
|
|
||||||
|
sprintf (str, "%02d", cursor.weekOfYear (0));
|
||||||
|
bar.minor = str;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'M': // year/month
|
||||||
|
sprintf (str, "%d", cursor.year ());
|
||||||
|
bar.major = str;
|
||||||
|
|
||||||
|
sprintf (str, "%02d", cursor.month ());
|
||||||
|
bar.minor = str;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bar.offset = i;
|
||||||
|
bars[cursor.toEpoch ()] = bar;
|
||||||
|
|
||||||
|
// Record the earliest date, for use as a cutoff when scanning data.
|
||||||
|
earliest = cursor;
|
||||||
|
|
||||||
|
// Move to the previous period.
|
||||||
|
cursor = decrement (cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "# Bar count " << bars.size () << "\n";
|
||||||
|
std::cout << "# earliest " << earliest.toString ("YMD") << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
void Chart::maxima ()
|
||||||
|
{
|
||||||
|
max_value = 0;
|
||||||
|
max_label = 1;
|
||||||
|
|
||||||
|
std::map <time_t, Bar>::iterator it;
|
||||||
|
for (it = bars.begin (); it != bars.end (); it++)
|
||||||
|
{
|
||||||
|
// Determine max_label.
|
||||||
|
int total = it->second.pending +
|
||||||
|
it->second.started +
|
||||||
|
it->second.done;
|
||||||
|
|
||||||
|
// Determine max_value.
|
||||||
|
if (total > max_value)
|
||||||
|
max_value = total;
|
||||||
|
|
||||||
|
int length = (int) log10 ((double) total) + 1;
|
||||||
|
if (length > max_label)
|
||||||
|
max_label = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// How many bars can be shown?
|
||||||
|
actual_bars = (width - max_label - 14) / 3;
|
||||||
|
graph_width = width - max_label - 14;
|
||||||
|
|
||||||
|
std::cout << "# max_value " << max_value << "\n";
|
||||||
|
std::cout << "# max_label " << max_label << "\n";
|
||||||
|
std::cout << "# actual_bars " << actual_bars << "\n";
|
||||||
|
std::cout << "# graph_width " << graph_width << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Given the vertical chart area size (height), the largest value (value),
|
// Given the vertical chart area size (height), the largest value (value),
|
||||||
// populate a vector of labels for the y axis.
|
// populate a vector of labels for the y axis.
|
||||||
|
// TODO Make this a member of Chart.
|
||||||
void calculateYAxis (std::vector <int>& labels, int height, int value)
|
void calculateYAxis (std::vector <int>& labels, int height, int value)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
|
@ -613,6 +1251,37 @@ int handleReportBurndownWeekly (std::string& outs)
|
||||||
{
|
{
|
||||||
int rc = 0;
|
int rc = 0;
|
||||||
|
|
||||||
|
if (context.hooks.trigger ("pre-burndown-command"))
|
||||||
|
{
|
||||||
|
// Scan the pending tasks, applying any filter.
|
||||||
|
std::vector <Task> tasks;
|
||||||
|
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||||
|
handleRecurrence ();
|
||||||
|
context.tdb.load (tasks, context.filter);
|
||||||
|
context.tdb.commit ();
|
||||||
|
context.tdb.unlock ();
|
||||||
|
|
||||||
|
// Create a chart, scan the tasks, then render.
|
||||||
|
Chart chart ('W');
|
||||||
|
chart.scan (tasks);
|
||||||
|
std::map <time_t, Bar>::iterator it;
|
||||||
|
for (it = chart.bars.begin (); it != chart.bars.end (); ++it)
|
||||||
|
std::cout << "# bar " << Date (it->first).toString ("YMD")
|
||||||
|
<< " offset=" << it->second.offset
|
||||||
|
<< " major=" << it->second.major
|
||||||
|
<< " minor=" << it->second.minor
|
||||||
|
<< " pending=" << it->second.pending
|
||||||
|
<< " started=" << it->second.started
|
||||||
|
<< " done=" << it->second.done
|
||||||
|
<< " added=" << it->second.added
|
||||||
|
<< " removed=" << it->second.removed
|
||||||
|
<< "\n";
|
||||||
|
|
||||||
|
outs = chart.render ();
|
||||||
|
|
||||||
|
context.hooks.trigger ("post-burndown-command");
|
||||||
|
}
|
||||||
|
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -621,6 +1290,37 @@ int handleReportBurndownMonthly (std::string& outs)
|
||||||
{
|
{
|
||||||
int rc = 0;
|
int rc = 0;
|
||||||
|
|
||||||
|
if (context.hooks.trigger ("pre-burndown-command"))
|
||||||
|
{
|
||||||
|
// Scan the pending tasks, applying any filter.
|
||||||
|
std::vector <Task> tasks;
|
||||||
|
context.tdb.lock (context.config.getBoolean ("locking"));
|
||||||
|
handleRecurrence ();
|
||||||
|
context.tdb.load (tasks, context.filter);
|
||||||
|
context.tdb.commit ();
|
||||||
|
context.tdb.unlock ();
|
||||||
|
|
||||||
|
// Create a chart, scan the tasks, then render.
|
||||||
|
Chart chart ('M');
|
||||||
|
chart.scan (tasks);
|
||||||
|
std::map <time_t, Bar>::iterator it;
|
||||||
|
for (it = chart.bars.begin (); it != chart.bars.end (); ++it)
|
||||||
|
std::cout << "# bar " << Date (it->first).toString ("YMD")
|
||||||
|
<< " offset=" << it->second.offset
|
||||||
|
<< " major=" << it->second.major
|
||||||
|
<< " minor=" << it->second.minor
|
||||||
|
<< " pending=" << it->second.pending
|
||||||
|
<< " started=" << it->second.started
|
||||||
|
<< " done=" << it->second.done
|
||||||
|
<< " added=" << it->second.added
|
||||||
|
<< " removed=" << it->second.removed
|
||||||
|
<< "\n";
|
||||||
|
|
||||||
|
outs = chart.render ();
|
||||||
|
|
||||||
|
context.hooks.trigger ("post-burndown-command");
|
||||||
|
}
|
||||||
|
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,30 +25,13 @@
|
||||||
//
|
//
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
//#include <iostream>
|
|
||||||
//#include <iomanip>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
//#include <fstream>
|
|
||||||
//#include <sys/types.h>
|
|
||||||
//#include <stdio.h>
|
|
||||||
//#include <unistd.h>
|
|
||||||
//#include <stdlib.h>
|
|
||||||
//#include <pwd.h>
|
|
||||||
//#include <time.h>
|
|
||||||
|
|
||||||
#include "Context.h"
|
#include <Context.h>
|
||||||
//#include "Directory.h"
|
#include <Table.h>
|
||||||
//#include "File.h"
|
#include <text.h>
|
||||||
//#include "Date.h"
|
#include <util.h>
|
||||||
//#include "Duration.h"
|
#include <main.h>
|
||||||
#include "Table.h"
|
|
||||||
#include "text.h"
|
|
||||||
#include "util.h"
|
|
||||||
#include "main.h"
|
|
||||||
|
|
||||||
//#ifdef HAVE_LIBNCURSES
|
|
||||||
//#include <ncurses.h>
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
extern Context context;
|
extern Context context;
|
||||||
|
|
||||||
|
@ -84,21 +67,14 @@ int handleReportHistoryMonthly (std::string& outs)
|
||||||
groups[epoch] = 0;
|
groups[epoch] = 0;
|
||||||
|
|
||||||
// Every task has an entry date.
|
// Every task has an entry date.
|
||||||
if (addedGroup.find (epoch) != addedGroup.end ())
|
++addedGroup[epoch];
|
||||||
addedGroup[epoch] = addedGroup[epoch] + 1;
|
|
||||||
else
|
|
||||||
addedGroup[epoch] = 1;
|
|
||||||
|
|
||||||
// All deleted tasks have an end date.
|
// All deleted tasks have an end date.
|
||||||
if (task->getStatus () == Task::deleted)
|
if (task->getStatus () == Task::deleted)
|
||||||
{
|
{
|
||||||
epoch = end.startOfMonth ().toEpoch ();
|
epoch = end.startOfMonth ().toEpoch ();
|
||||||
groups[epoch] = 0;
|
groups[epoch] = 0;
|
||||||
|
++deletedGroup[epoch];
|
||||||
if (deletedGroup.find (epoch) != deletedGroup.end ())
|
|
||||||
deletedGroup[epoch] = deletedGroup[epoch] + 1;
|
|
||||||
else
|
|
||||||
deletedGroup[epoch] = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All completed tasks have an end date.
|
// All completed tasks have an end date.
|
||||||
|
@ -106,11 +82,7 @@ int handleReportHistoryMonthly (std::string& outs)
|
||||||
{
|
{
|
||||||
epoch = end.startOfMonth ().toEpoch ();
|
epoch = end.startOfMonth ().toEpoch ();
|
||||||
groups[epoch] = 0;
|
groups[epoch] = 0;
|
||||||
|
++completedGroup[epoch];
|
||||||
if (completedGroup.find (epoch) != completedGroup.end ())
|
|
||||||
completedGroup[epoch] = completedGroup[epoch] + 1;
|
|
||||||
else
|
|
||||||
completedGroup[epoch] = 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,21 +229,14 @@ int handleReportHistoryAnnual (std::string& outs)
|
||||||
groups[epoch] = 0;
|
groups[epoch] = 0;
|
||||||
|
|
||||||
// Every task has an entry date.
|
// Every task has an entry date.
|
||||||
if (addedGroup.find (epoch) != addedGroup.end ())
|
++addedGroup[epoch];
|
||||||
addedGroup[epoch] = addedGroup[epoch] + 1;
|
|
||||||
else
|
|
||||||
addedGroup[epoch] = 1;
|
|
||||||
|
|
||||||
// All deleted tasks have an end date.
|
// All deleted tasks have an end date.
|
||||||
if (task->getStatus () == Task::deleted)
|
if (task->getStatus () == Task::deleted)
|
||||||
{
|
{
|
||||||
epoch = end.startOfYear ().toEpoch ();
|
epoch = end.startOfYear ().toEpoch ();
|
||||||
groups[epoch] = 0;
|
groups[epoch] = 0;
|
||||||
|
++deletedGroup[epoch];
|
||||||
if (deletedGroup.find (epoch) != deletedGroup.end ())
|
|
||||||
deletedGroup[epoch] = deletedGroup[epoch] + 1;
|
|
||||||
else
|
|
||||||
deletedGroup[epoch] = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All completed tasks have an end date.
|
// All completed tasks have an end date.
|
||||||
|
@ -279,11 +244,7 @@ int handleReportHistoryAnnual (std::string& outs)
|
||||||
{
|
{
|
||||||
epoch = end.startOfYear ().toEpoch ();
|
epoch = end.startOfYear ().toEpoch ();
|
||||||
groups[epoch] = 0;
|
groups[epoch] = 0;
|
||||||
|
++completedGroup[epoch];
|
||||||
if (completedGroup.find (epoch) != completedGroup.end ())
|
|
||||||
completedGroup[epoch] = completedGroup[epoch] + 1;
|
|
||||||
else
|
|
||||||
completedGroup[epoch] = 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,21 +388,14 @@ int handleReportGHistoryMonthly (std::string& outs)
|
||||||
groups[epoch] = 0;
|
groups[epoch] = 0;
|
||||||
|
|
||||||
// Every task has an entry date.
|
// Every task has an entry date.
|
||||||
if (addedGroup.find (epoch) != addedGroup.end ())
|
++addedGroup[epoch];
|
||||||
addedGroup[epoch] = addedGroup[epoch] + 1;
|
|
||||||
else
|
|
||||||
addedGroup[epoch] = 1;
|
|
||||||
|
|
||||||
// All deleted tasks have an end date.
|
// All deleted tasks have an end date.
|
||||||
if (task->getStatus () == Task::deleted)
|
if (task->getStatus () == Task::deleted)
|
||||||
{
|
{
|
||||||
epoch = end.startOfMonth ().toEpoch ();
|
epoch = end.startOfMonth ().toEpoch ();
|
||||||
groups[epoch] = 0;
|
groups[epoch] = 0;
|
||||||
|
++deletedGroup[epoch];
|
||||||
if (deletedGroup.find (epoch) != deletedGroup.end ())
|
|
||||||
deletedGroup[epoch] = deletedGroup[epoch] + 1;
|
|
||||||
else
|
|
||||||
deletedGroup[epoch] = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All completed tasks have an end date.
|
// All completed tasks have an end date.
|
||||||
|
@ -449,11 +403,7 @@ int handleReportGHistoryMonthly (std::string& outs)
|
||||||
{
|
{
|
||||||
epoch = end.startOfMonth ().toEpoch ();
|
epoch = end.startOfMonth ().toEpoch ();
|
||||||
groups[epoch] = 0;
|
groups[epoch] = 0;
|
||||||
|
++completedGroup[epoch];
|
||||||
if (completedGroup.find (epoch) != completedGroup.end ())
|
|
||||||
completedGroup[epoch] = completedGroup[epoch] + 1;
|
|
||||||
else
|
|
||||||
completedGroup[epoch] = 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -640,21 +590,14 @@ int handleReportGHistoryAnnual (std::string& outs)
|
||||||
groups[epoch] = 0;
|
groups[epoch] = 0;
|
||||||
|
|
||||||
// Every task has an entry date.
|
// Every task has an entry date.
|
||||||
if (addedGroup.find (epoch) != addedGroup.end ())
|
++addedGroup[epoch];
|
||||||
addedGroup[epoch] = addedGroup[epoch] + 1;
|
|
||||||
else
|
|
||||||
addedGroup[epoch] = 1;
|
|
||||||
|
|
||||||
// All deleted tasks have an end date.
|
// All deleted tasks have an end date.
|
||||||
if (task->getStatus () == Task::deleted)
|
if (task->getStatus () == Task::deleted)
|
||||||
{
|
{
|
||||||
epoch = end.startOfYear ().toEpoch ();
|
epoch = end.startOfYear ().toEpoch ();
|
||||||
groups[epoch] = 0;
|
groups[epoch] = 0;
|
||||||
|
++deletedGroup[epoch];
|
||||||
if (deletedGroup.find (epoch) != deletedGroup.end ())
|
|
||||||
deletedGroup[epoch] = deletedGroup[epoch] + 1;
|
|
||||||
else
|
|
||||||
deletedGroup[epoch] = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// All completed tasks have an end date.
|
// All completed tasks have an end date.
|
||||||
|
@ -662,11 +605,7 @@ int handleReportGHistoryAnnual (std::string& outs)
|
||||||
{
|
{
|
||||||
epoch = end.startOfYear ().toEpoch ();
|
epoch = end.startOfYear ().toEpoch ();
|
||||||
groups[epoch] = 0;
|
groups[epoch] = 0;
|
||||||
|
++completedGroup[epoch];
|
||||||
if (completedGroup.find (epoch) != completedGroup.end ())
|
|
||||||
completedGroup[epoch] = completedGroup[epoch] + 1;
|
|
||||||
else
|
|
||||||
completedGroup[epoch] = 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue