- Now renders (and wraps if necessary) a chart title, with additional filter description. - Removed more redundant code. - Fixed divide-by-zero bug when the filter resulted in zero matches. - Made the burndown commands read-only, so that filters apply.
1034 lines
29 KiB
C++
1034 lines
29 KiB
C++
////////////////////////////////////////////////////////////////////////////////
|
|
// taskwarrior - a command line task list manager.
|
|
//
|
|
// Copyright 2006 - 2010, Paul Beckingham, Federico Hernandez.
|
|
// All rights reserved.
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify it under
|
|
// the terms of the GNU General Public License as published by the Free Software
|
|
// Foundation; either version 2 of the License, or (at your option) any later
|
|
// version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
// details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along with
|
|
// this program; if not, write to the
|
|
//
|
|
// Free Software Foundation, Inc.,
|
|
// 51 Franklin Street, Fifth Floor,
|
|
// Boston, MA
|
|
// 02110-1301
|
|
// USA
|
|
//
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
#include <iostream> // TODO Remove
|
|
#include <sstream>
|
|
#include <algorithm>
|
|
#include <math.h>
|
|
|
|
#include <Context.h>
|
|
#include <Date.h>
|
|
#include <text.h>
|
|
#include <util.h>
|
|
#include <main.h>
|
|
|
|
extern Context context;
|
|
|
|
// Helper macro.
|
|
#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 ()
|
|
{
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// 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
|
|
{
|
|
public:
|
|
Chart (char);
|
|
Chart (const Chart&); // Unimplemented
|
|
Chart& operator= (const Chart&); // Unimplemented
|
|
~Chart ();
|
|
|
|
void description (const std::string&);
|
|
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 ();
|
|
void yLabels (std::vector <int>&);
|
|
|
|
public:
|
|
int width; // Terminal width
|
|
int height; // Terminal height
|
|
int graph_width; // Width of plot area
|
|
int graph_height; // Height of plot area
|
|
int max_value; // Largest combined bar value
|
|
int max_label; // Longest y-axis label
|
|
std::vector <int> labels; // Y-axis labels
|
|
int estimated_bars; // Estimated bar count
|
|
int actual_bars; // Calculated bar count
|
|
std::map <time_t, Bar> bars; // Epoch-indexed set of bars
|
|
Date earliest; // Date of earliest estimated bar
|
|
int carryover_done; // Number of 'done' tasks prior to chart range
|
|
char period; // D, W, M
|
|
std::string title; // Additional description
|
|
std::string grid; // String representing grid of characters
|
|
|
|
float find_rate; // Calculated find rate
|
|
float fix_rate; // Calculated fix rate
|
|
std::string completion; // Estimated completion date
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
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";
|
|
|
|
carryover_done = 0;
|
|
std::cout << "# carryover_done " << carryover_done << "\n";
|
|
|
|
// Rates are calculated last.
|
|
find_rate = 0.0;
|
|
fix_rate = 0.0;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
Chart::~Chart ()
|
|
{
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
void Chart::description (const std::string& value)
|
|
{
|
|
title = value;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
void Chart::scan (std::vector <Task>& tasks)
|
|
{
|
|
generateBars ();
|
|
|
|
// Not quantized, so that "while (xxx < now)" is inclusive.
|
|
Date now;
|
|
|
|
time_t epoch;
|
|
std::vector <Task>::iterator task;
|
|
for (task = tasks.begin (); task != tasks.end (); ++task)
|
|
{
|
|
// 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;
|
|
|
|
// Maintain a running total of 'done' tasks that are off the left of the
|
|
// chart.
|
|
if (end < earliest)
|
|
{
|
|
++carryover_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";
|
|
}
|
|
|
|
if (max_value == 0)
|
|
return "No matches.\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";
|
|
|
|
// Title.
|
|
std::string full_title;
|
|
switch (period)
|
|
{
|
|
case 'D': full_title = "Daily"; break;
|
|
case 'W': full_title = "Weekly"; break;
|
|
case 'M': full_title = "Monthly"; break;
|
|
}
|
|
|
|
full_title += " Burndown";
|
|
|
|
if (title.length ())
|
|
{
|
|
if (full_title.length () + 1 + title.length () < (unsigned) width)
|
|
{
|
|
full_title += " " + title;
|
|
grid.replace (LOC (0, (width - full_title.length ()) / 2), full_title.length (), full_title);
|
|
}
|
|
else
|
|
{
|
|
grid.replace (LOC (0, (width - full_title.length ()) / 2), full_title.length (), full_title);
|
|
grid.replace (LOC (1, (width - title.length ()) / 2), title.length (), title);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
grid.replace (LOC (0, (width - full_title.length ()) / 2), full_title.length (), full_title);
|
|
}
|
|
|
|
// 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, "|");
|
|
|
|
// Determine y-axis labelling.
|
|
std::vector <int> labels;
|
|
yLabels (labels);
|
|
|
|
// 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, '-'));
|
|
|
|
// 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.
|
|
/*
|
|
// 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];
|
|
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 (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 ())
|
|
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;
|
|
|
|
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 +
|
|
carryover_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 (graph_height), the largest value
|
|
// (max_value), populate a vector of labels for the y axis.
|
|
void Chart::yLabels (std::vector <int>& labels)
|
|
{
|
|
/*
|
|
double logarithm = log10 ((double) value);
|
|
|
|
int exponent = (int) logarithm;
|
|
logarithm -= exponent;
|
|
|
|
int divisions = 10;
|
|
double localMaximum = pow (10.0, exponent + 1);
|
|
bool repeat = true;
|
|
|
|
do
|
|
{
|
|
repeat = false;
|
|
double scale = pow (10.0, exponent);
|
|
|
|
while (value < localMaximum - scale)
|
|
{
|
|
localMaximum -= scale;
|
|
--divisions;
|
|
}
|
|
|
|
if (divisions < 3 && exponent > 1)
|
|
{
|
|
divisions *= 10;
|
|
--exponent;
|
|
repeat = true;
|
|
}
|
|
}
|
|
while (repeat);
|
|
|
|
int division_size = localMaximum / divisions;
|
|
for (int i = 0; i <= divisions; ++i)
|
|
labels.push_back (i * division_size);
|
|
*/
|
|
|
|
// For now, simply select 0, n/2 and n, where n is value rounded up to the
|
|
// nearest 10. This is a poor solution.
|
|
int high = max_value;
|
|
int mod = high % 10;
|
|
if (mod)
|
|
high += 10 - mod;
|
|
|
|
int half = high / 2;
|
|
|
|
labels.push_back (0);
|
|
labels.push_back (half);
|
|
labels.push_back (high);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
int handleReportBurndownDaily (std::string& outs)
|
|
{
|
|
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 ('D');
|
|
chart.scan (tasks);
|
|
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";
|
|
|
|
outs = chart.render ();
|
|
|
|
context.hooks.trigger ("post-burndown-command");
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
int handleReportBurndownWeekly (std::string& outs)
|
|
{
|
|
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 << "# " << 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";
|
|
|
|
outs = chart.render ();
|
|
|
|
context.hooks.trigger ("post-burndown-command");
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
int handleReportBurndownMonthly (std::string& outs)
|
|
{
|
|
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');
|
|
|
|
// Use any filter as a title.
|
|
if (context.filter.size ())
|
|
{
|
|
std::string combined = "(";
|
|
|
|
for (unsigned int i = 0; i < context.filter.size (); ++i)
|
|
{
|
|
if (i)
|
|
combined += " ";
|
|
|
|
combined += context.filter[i].name ();
|
|
|
|
if (context.filter[i].mod ().length ())
|
|
combined += "." + context.filter[i].mod ();
|
|
|
|
combined += ":" + context.filter[i].value ();
|
|
}
|
|
|
|
combined += ")";
|
|
chart.description (combined);
|
|
}
|
|
|
|
chart.scan (tasks);
|
|
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";
|
|
|
|
outs = chart.render ();
|
|
|
|
context.hooks.trigger ("post-burndown-command");
|
|
}
|
|
|
|
return rc;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|