//////////////////////////////////////////////////////////////////////////////// // // Copyright 2006 - 2014, Paul Beckingham, Federico Hernandez. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // http://www.opensource.org/licenses/mit-license.php // //////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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_label; // x-axis label, major (year/-/month) std::string _minor_label; // x-axis label, minor (month/week/day) int _pending; // Number of pending tasks in period int _started; // Number of started tasks in period int _done; // Number of done tasks in period int _added; // Number added in period int _removed; // Number removed in period }; //////////////////////////////////////////////////////////////////////////////// Bar::Bar () : _offset (0) , _major_label ("") , _minor_label ("") , _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_label = other._major_label; _minor_label = other._minor_label; _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 scan (std::vector &); 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 &); void calculateRates (std::vector &); 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 _labels; // Y-axis labels int _estimated_bars; // Estimated bar count int _actual_bars; // Calculated bar count std::map _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. _max_value = 0; _max_label = 1; _graph_height = _height - 7; _graph_width = _width - _max_label - 14; // Estimate how many 'bars' can be dsplayed. This will help subset a // potentially enormous data set. _estimated_bars = (_width - 1 - 14) / 3; _actual_bars = 0; _period = type; _carryover_done = 0; // Rates are calculated last. _find_rate = 0.0; _fix_rate = 0.0; // Set the title. _title = "("; std::vector ::iterator i; for (i = context.parser.tree ()->_branches.begin (); i != context.parser.tree ()->_branches.end (); ++i) { if (! (*i)->hasTag ("BINARY") && ! (*i)->hasTag ("RC") && ! (*i)->hasTag ("CONFIG") && ! (*i)->hasTag ("CMD") && ! (*i)->hasTag ("TERMINATOR")) { if (_title.length () > 1) _title += " "; _title += (*i)->attribute ("raw"); } } _title += ")"; } //////////////////////////////////////////////////////////////////////////////// Chart::~Chart () { } //////////////////////////////////////////////////////////////////////////////// void Chart::scan (std::vector & tasks) { generateBars (); // Not quantized, so that "while (xxx < now)" is inclusive. Date now; time_t epoch; std::vector ::iterator task; for (task = tasks.begin (); task != tasks.end (); ++task) { // The entry date is when the counting starts. Date from = quantize (Date (task->get_date ("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_date ("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_date ("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_date ("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_date ("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_date ("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_date ("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_date ("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 | // | | // | ADD rate 1.7/d Estimated completion 8/12/2010 | // | Don/Delete 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 std::string (STRING_CMD_BURN_TOO_SMALL) + "\n"; } if (_max_value == 0) context.footnote (STRING_FEEDBACK_NO_MATCH); // Create a grid, folded into a string. _grid = ""; for (int i = 0; i < _height; ++i) _grid += std::string (_width, ' ') + "\n"; // Title. std::string full_title; switch (_period) { case 'D': full_title = STRING_CMD_BURN_DAILY; break; case 'W': full_title = STRING_CMD_BURN_WEEKLY; break; case 'M': full_title = STRING_CMD_BURN_MONTHLY; break; } full_title += std::string (" ") + STRING_CMD_BURN_TITLE; 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 " + leftJustify (STRING_CMD_BURN_DONE, 7)); _grid.replace (LOC (_graph_height / 2, _width - 10), 10, "SS " + leftJustify (STRING_CMD_BURN_STARTED, 7)); _grid.replace (LOC (_graph_height / 2 + 1, _width - 10), 10, "PP " + leftJustify (STRING_CMD_BURN_PENDING, 7)); // Determine y-axis labelling. std::vector _labels; yLabels (_labels); _max_label = (int) log10 ((double) _labels[2]) + 1; // Draw y-axis. for (int i = 0; i < _graph_height; ++i) _grid.replace (LOC (i + 1, _max_label + 1), 1, "|"); // 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 bars_in_sequence; std::map ::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 ::iterator seq; std::string _major_label; 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_label.length (), bar._minor_label); if (_major_label != bar._major_label) _grid.replace (LOC (_height - 4, _max_label + 2 + ((_actual_bars - bar._offset - 1) * 3)), bar._major_label.length (), " " + bar._major_label); _major_label = bar._major_label; } } // 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._pending + bar._started) * _graph_height) / _labels[2]; int done = ((bar._pending + bar._started + 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 = pending; b < started; ++b) _grid.replace (LOC (_graph_height - b, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), 2, "SS"); for (int b = started; b < done; ++b) _grid.replace (LOC (_graph_height - b, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), 2, "DD"); } } // Draw rates. calculateRates (bars_in_sequence); char rate[12]; if (_find_rate != 0.0) sprintf (rate, "%.1f/d", _find_rate); else strcpy (rate, "-"); _grid.replace (LOC (_height - 2, _max_label + 3), 18 + strlen (rate), std::string ("Add rate: ") + rate); if (_fix_rate != 0.0) sprintf (rate, "%.1f/d", _fix_rate); else strcpy (rate, "-"); _grid.replace (LOC (_height - 1, _max_label + 3), 18 + strlen (rate), std::string ("Done/Delete rate: ") + rate); // Draw completion date. if (_completion.length ()) _grid.replace (LOC (_height - 2, _max_label + 32), 22 + _completion.length (), "Estimated completion: " + _completion); optimizeGrid (); if (context.color ()) { // 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 (" ")); } else { // Replace DD, SS, PP with ./+/X strings. std::string::size_type i; while ((i = _grid.find ("PP")) != std::string::npos) _grid.replace (i, 2, " X"); while ((i = _grid.find ("SS")) != std::string::npos) _grid.replace (i, 2, " +"); while ((i = _grid.find ("DD")) != std::string::npos) _grid.replace (i, 2, " ."); } 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_label = month.substr (0, 3); sprintf (str, "%02d", cursor.day ()); bar._minor_label = str; } break; case 'W': // year/week sprintf (str, "%d", cursor.year ()); bar._major_label = str; sprintf (str, "%02d", cursor.weekOfYear (0)); bar._minor_label = str; break; case 'M': // year/month sprintf (str, "%d", cursor.year ()); bar._major_label = str; sprintf (str, "%02d", cursor.month ()); bar._minor_label = 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); } } //////////////////////////////////////////////////////////////////////////////// void Chart::maxima () { _max_value = 0; _max_label = 1; std::map ::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; } //////////////////////////////////////////////////////////////////////////////// // 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 & labels) { // Calculate may Y using a nice algorithm that rounds the data. int high = burndown_size (_max_value); int half = high / 2; labels.push_back (0); labels.push_back (half); labels.push_back (high); } //////////////////////////////////////////////////////////////////////////////// void Chart::calculateRates (std::vector & sequence) { // If there are no current pending tasks, then it is meaningless to find // rates or estimated completion date. if (_bars[sequence.back ()]._pending == 0) return; // Calculate how many items we have. int quantity = (int) sequence.size (); int half = quantity / 2; int quarter = quantity / 4; // If the half and quarter indexes match, then there are too few data points // to generate any meaningful rates. if (half == quantity || half == 0 || quarter == 0) { context.debug ("Chart::calculateRates Insufficient data for rate calc"); return; } // How many days do these sums represent? int half_days = 1; int quarter_days = 1; switch (_period) { case 'D': half_days = half; quarter_days = quarter; break; case 'W': half_days = half * 7; quarter_days = quarter * 7; break; case 'M': half_days = half * 30; quarter_days = quarter * 30; break; } int total_added_50 = 0; int total_added_75 = 0; int total_removed_50 = 0; int total_removed_75 = 0; for (unsigned int i = half; i < sequence.size (); ++i) { total_added_50 += _bars[sequence[i]]._added; total_removed_50 += _bars[sequence[i]]._removed; } for (unsigned int i = half + quarter; i < sequence.size (); ++i) { total_added_75 += _bars[sequence[i]]._added; total_removed_75 += _bars[sequence[i]]._removed; } float find_rate_50 = 1.0 * total_added_50 / half_days; float find_rate_75 = 1.0 * total_added_75 / quarter_days; float fix_rate_50 = 1.0 * total_removed_50 / half_days; float fix_rate_75 = 1.0 * total_removed_75 / quarter_days; // Make configurable. float bias = (float) context.config.getReal ("burndown.bias"); _find_rate = (find_rate_50 * (1.0 - bias) + find_rate_75 * bias); _fix_rate = (fix_rate_50 * (1.0 - bias) + fix_rate_75 * bias); // Q: Why is this equation written out as a debug message? // A: People are going to want to know how the rates and the completion date // are calculated. This may also help debugging. std::stringstream rates; rates << "Chart::calculateRates find rate: " << "(" << total_added_50 << " added / " << half_days << " days) * (1.0 - " << bias << ") + (" << total_added_75 << " added / " << quarter_days << " days) * " << bias << ") = " << _find_rate << "\nChart::calculateRates fix rate: " << "(" << total_removed_50 << " removed / " << half_days << " days) * (1.0 - " << bias << ") + (" << total_removed_75 << " added / " << quarter_days << " days) * " << bias << ") = " << _fix_rate; context.debug (rates.str ()); // Estimate completion if (_fix_rate > _find_rate) { int current_pending = _bars[sequence.back ()]._pending; int remaining_days = (int) (current_pending / (_fix_rate - _find_rate)); Date now; Duration delta (remaining_days * 86400); now += delta; // Prefer dateformat.report over dateformat. std::string format = context.config.get ("dateformat.report"); if (format == "") format = context.config.get ("dateformat"); _completion = now.toString (format) + " (" + delta.format () + ")"; std::stringstream est; est << "Chart::calculateRates Completion: " << current_pending << " tasks / (" << _fix_rate << " - " << _find_rate << ") = " << remaining_days << " days = " << _completion; context.debug (est.str ()); } else { _completion = STRING_CMD_BURN_NO_CONVERGE; } } //////////////////////////////////////////////////////////////////////////////// CmdBurndownMonthly::CmdBurndownMonthly () { _keyword = "burndown.monthly"; _usage = "task burndown.monthly"; _description = STRING_CMD_BURN_USAGE_M; _read_only = true; _displays_id = false; } //////////////////////////////////////////////////////////////////////////////// int CmdBurndownMonthly::execute (std::string& output) { int rc = 0; // Scan the pending tasks, applying any filter. handleRecurrence (); Filter filter; std::vector filtered; filter.subset (filtered); context.tdb2.commit (); // Create a chart, scan the tasks, then render. Chart chart ('M'); chart.scan (filtered); output = chart.render (); return rc; } //////////////////////////////////////////////////////////////////////////////// CmdBurndownWeekly::CmdBurndownWeekly () { _keyword = "burndown.weekly"; _usage = "task burndown.weekly"; _description = STRING_CMD_BURN_USAGE_W; _read_only = true; _displays_id = false; } //////////////////////////////////////////////////////////////////////////////// int CmdBurndownWeekly::execute (std::string& output) { int rc = 0; // Scan the pending tasks, applying any filter. handleRecurrence (); Filter filter; std::vector filtered; filter.subset (filtered); context.tdb2.commit (); // Create a chart, scan the tasks, then render. Chart chart ('W'); chart.scan (filtered); output = chart.render (); return rc; } //////////////////////////////////////////////////////////////////////////////// CmdBurndownDaily::CmdBurndownDaily () { _keyword = "burndown.daily"; _usage = "task burndown.daily"; _description = STRING_CMD_BURN_USAGE_D; _read_only = true; _displays_id = false; } //////////////////////////////////////////////////////////////////////////////// int CmdBurndownDaily::execute (std::string& output) { int rc = 0; // Scan the pending tasks, applying any filter. handleRecurrence (); Filter filter; std::vector filtered; filter.subset (filtered); context.tdb2.commit (); // Create a chart, scan the tasks, then render. Chart chart ('D'); chart.scan (filtered); output = chart.render (); return rc; } ////////////////////////////////////////////////////////////////////////////////