1 ////////////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez.
4 //
5 // Permission is hereby granted, free of charge, to any person obtaining a copy
6 // of this software and associated documentation files (the "Software"), to deal
7 // in the Software without restriction, including without limitation the rights
8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 // copies of the Software, and to permit persons to whom the Software is
10 // furnished to do so, subject to the following conditions:
11 //
12 // The above copyright notice and this permission notice shall be included
13 // in all copies or substantial portions of the Software.
14 //
15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16 // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 // SOFTWARE.
22 //
23 // https://www.opensource.org/licenses/mit-license.php
24 //
25 ////////////////////////////////////////////////////////////////////////////////
26
27 #include <cmake.h>
28 #include <CmdBurndown.h>
29 #include <sstream>
30 #include <map>
31 #include <algorithm>
32 #include <limits>
33 #include <string.h>
34 #include <math.h>
35 #include <Context.h>
36 #include <Filter.h>
37 #include <Datetime.h>
38 #include <Duration.h>
39 #include <main.h>
40 #include <shared.h>
41 #include <format.h>
42
43 // Helper macro.
44 #define LOC(y,x) (((y) * (_width + 1)) + (x))
45
46 ////////////////////////////////////////////////////////////////////////////////
47 class Bar
48 {
49 public:
50 Bar () = default;
51 Bar (const Bar&);
52 Bar& operator= (const Bar&);
53 ~Bar () = default;
54
55 public:
56 int _offset {0}; // from left of chart
57 std::string _major_label {""}; // x-axis label, major (year/-/month)
58 std::string _minor_label {""}; // x-axis label, minor (month/week/day)
59 int _pending {0}; // Number of pending tasks in period
60 int _started {0}; // Number of started tasks in period
61 int _done {0}; // Number of done tasks in period
62 int _added {0}; // Number added in period
63 int _removed {0}; // Number removed in period
64 };
65
66 ////////////////////////////////////////////////////////////////////////////////
Bar(const Bar & other)67 Bar::Bar (const Bar& other)
68 {
69 *this = other;
70 }
71
72 ////////////////////////////////////////////////////////////////////////////////
operator =(const Bar & other)73 Bar& Bar::operator= (const Bar& other)
74 {
75 if (this != &other)
76 {
77 _offset = other._offset;
78 _major_label = other._major_label;
79 _minor_label = other._minor_label;
80 _pending = other._pending;
81 _started = other._started;
82 _done = other._done;
83 _added = other._added;
84 _removed = other._removed;
85 }
86
87 return *this;
88 }
89
90 ////////////////////////////////////////////////////////////////////////////////
91 // Data gathering algorithm:
92 //
93 // e = entry
94 // s = start
95 // C = end/Completed
96 // D = end/Deleted
97 // > = Pending/Waiting
98 //
99 // ID 30 31 01 02 03 04 05 06 07 08 09 10
100 // -- ------------------------------------
101 // 1 e-----s--C
102 // 2 e--s-----D
103 // 3 e-----s-------------->
104 // 4 e----------------->
105 // 5 e----->
106 // -- ------------------------------------
107 // PP 1 2 3 3 2 2 2 3 3 3
108 // SS 2 1 1 1 1 1 1 1
109 // DD 1 1 1 1 1 1 1
110 // -- ------------------------------------
111 //
112 // 5 | SS DD DD DD DD
113 // 4 | SS SS DD DD DD SS SS SS
114 // 3 | PP PP SS SS SS PP PP PP
115 // 2 | PP PP PP PP PP PP PP PP PP
116 // 1 | PP PP PP PP PP PP PP PP PP PP
117 // 0 +-------------------------------------
118 // 30 31 01 02 03 04 05 06 07 08 09 10
119 // Oct Nov
120 //
121 class Chart
122 {
123 public:
124 Chart (char);
125 Chart (const Chart&); // Unimplemented
126 Chart& operator= (const Chart&); // Unimplemented
127 ~Chart () = default;
128
129 void scan (std::vector <Task>&);
130 void scanForPeak (std::vector <Task>&);
131 std::string render ();
132
133 private:
134 void generateBars ();
135 void optimizeGrid ();
136 Datetime quantize (const Datetime&, char);
137
138 Datetime increment (const Datetime&, char);
139 Datetime decrement (const Datetime&, char);
140 void maxima ();
141 void yLabels (std::vector <int>&);
142 void calculateRates ();
143 unsigned round_up_to (unsigned, unsigned);
144 unsigned burndown_size (unsigned);
145
146 public:
147 int _width {}; // Terminal width
148 int _height {}; // Terminal height
149 int _graph_width {}; // Width of plot area
150 int _graph_height {}; // Height of plot area
151 int _max_value {0}; // Largest combined bar value
152 int _max_label {1}; // Longest y-axis label
153 std::vector <int> _labels {}; // Y-axis labels
154 int _estimated_bars {}; // Estimated bar count
155 int _actual_bars {0}; // Calculated bar count
156 std::map <time_t, Bar> _bars {}; // Epoch-indexed set of bars
157 Datetime _earliest {}; // Date of earliest estimated bar
158 int _carryover_done {0}; // Number of 'done' tasks prior to chart range
159 char _period {}; // D, W, M
160 std::string _grid {}; // String representing grid of characters
161 time_t _peak_epoch {}; // Quantized (D) date of highest pending peak
162 int _peak_count {0}; // Corresponding peak pending count
163 int _current_count {0}; // Current pending count
164 float _net_fix_rate {0.0}; // Calculated fix rate
165 std::string _completion {}; // Estimated completion date
166 };
167
168 ////////////////////////////////////////////////////////////////////////////////
Chart(char type)169 Chart::Chart (char type)
170 {
171 // How much space is there to render in? This chart will occupy the
172 // maximum space, and the width drives various other parameters.
173 _width = Context::getContext ().getWidth ();
174 _height = Context::getContext ().getHeight ()
175 - Context::getContext ().config.getInteger ("reserved.lines")
176 - 1; // Allow for new line with prompt.
177 _graph_height = _height - 7;
178 _graph_width = _width - _max_label - 14;
179
180 // Estimate how many 'bars' can be dsplayed. This will help subset a
181 // potentially enormous data set.
182 _estimated_bars = (_width - 1 - 14) / 3;
183
184 _period = type;
185 }
186
187 ////////////////////////////////////////////////////////////////////////////////
188 // Scan all tasks, quantize the dates by day, and find the peak pending count
189 // and corresponding epoch.
scanForPeak(std::vector<Task> & tasks)190 void Chart::scanForPeak (std::vector <Task>& tasks)
191 {
192 std::map <time_t, int> pending;
193 _current_count = 0;
194
195 for (auto& task : tasks)
196 {
197 // The entry date is when the counting starts.
198 Datetime entry (task.get_date ("entry"));
199
200 Datetime end;
201 if (task.has ("end"))
202 end = Datetime (task.get_date ("end"));
203 else
204 ++_current_count;
205
206 while (entry < end)
207 {
208 time_t epoch = quantize (entry.toEpoch (), 'D').toEpoch ();
209 if (pending.find (epoch) != pending.end ())
210 ++pending[epoch];
211 else
212 pending[epoch] = 1;
213
214 entry = increment (entry, 'D');
215 }
216 }
217
218 // Find the peak and peak date.
219 for (auto& count : pending)
220 {
221 if (count.second > _peak_count)
222 {
223 _peak_count = count.second;
224 _peak_epoch = count.first;
225 }
226 }
227 }
228
229 ////////////////////////////////////////////////////////////////////////////////
scan(std::vector<Task> & tasks)230 void Chart::scan (std::vector <Task>& tasks)
231 {
232 generateBars ();
233
234 // Not quantized, so that "while (xxx < now)" is inclusive.
235 Datetime now;
236
237 time_t epoch;
238 auto& config = Context::getContext ().config;
239 bool cumulative;
240 if (config.has ("burndown.cumulative"))
241 {
242 cumulative = config.getBoolean ("burndown.cumulative");
243 }
244 else
245 {
246 cumulative = true;
247 }
248
249 for (auto& task : tasks)
250 {
251 // The entry date is when the counting starts.
252 Datetime from = quantize (Datetime (task.get_date ("entry")), _period);
253 epoch = from.toEpoch ();
254
255 if (_bars.find (epoch) != _bars.end ())
256 ++_bars[epoch]._added;
257
258 // e--> e--s-->
259 // ppp> pppsss>
260 Task::status status = task.getStatus ();
261 if (status == Task::pending ||
262 status == Task::waiting)
263 {
264 if (task.has ("start"))
265 {
266 Datetime start = quantize (Datetime (task.get_date ("start")), _period);
267 while (from < start)
268 {
269 epoch = from.toEpoch ();
270 if (_bars.find (epoch) != _bars.end ())
271 ++_bars[epoch]._pending;
272 from = increment (from, _period);
273 }
274
275 while (from < now)
276 {
277 epoch = from.toEpoch ();
278 if (_bars.find (epoch) != _bars.end ())
279 ++_bars[epoch]._started;
280 from = increment (from, _period);
281 }
282 }
283 else
284 {
285 while (from < now)
286 {
287 epoch = from.toEpoch ();
288 if (_bars.find (epoch) != _bars.end ())
289 ++_bars[epoch]._pending;
290 from = increment (from, _period);
291 }
292 }
293 }
294
295 // e--C e--s--C
296 // pppd> pppsssd>
297 else if (status == Task::completed)
298 {
299 // Truncate history so it starts at 'earliest' for completed tasks.
300 Datetime end = quantize (Datetime (task.get_date ("end")), _period);
301 epoch = end.toEpoch ();
302
303 if (_bars.find (epoch) != _bars.end ())
304 ++_bars[epoch]._removed;
305
306 while (from < end)
307 {
308 epoch = from.toEpoch ();
309 if (_bars.find (epoch) != _bars.end ())
310 ++_bars[epoch]._pending;
311 from = increment (from, _period);
312 }
313
314 if (cumulative)
315 {
316 while (from < now)
317 {
318 epoch = from.toEpoch ();
319 if (_bars.find (epoch) != _bars.end ())
320 ++_bars[epoch]._done;
321 from = increment (from, _period);
322 }
323
324 // Maintain a running total of 'done' tasks that are off the left of the
325 // chart.
326 if (end < _earliest)
327 {
328 ++_carryover_done;
329 continue;
330 }
331 }
332
333 else
334 {
335 epoch = from.toEpoch ();
336 if (_bars.find (epoch) != _bars.end ())
337 ++_bars[epoch]._done;
338 }
339 }
340 }
341
342 // Size the data.
343 maxima ();
344 }
345
346 ////////////////////////////////////////////////////////////////////////////////
347 // Graph should render like this:
348 // +---------------------------------------------------------------------+
349 // | |
350 // | 20 | |
351 // | | DD DD DD DD DD DD DD DD |
352 // | | DD DD DD DD DD DD DD DD DD DD DD DD DD DD |
353 // | | PP PP SS SS SS SS SS SS SS SS SS DD DD DD DD DD DD DD Done |
354 // | 10 | PP PP PP PP PP PP SS SS SS SS SS SS DD DD DD DD DD SS Started|
355 // | | PP PP PP PP PP PP PP PP PP PP PP SS SS SS SS DD DD PP Pending|
356 // | | PP PP PP PP PP PP PP PP PP PP PP PP PP PP PP SS DD |
357 // | | PP PP PP PP PP PP PP PP PP PP PP PP PP PP PP PP PP |
358 // | 0 +---------------------------------------------------- |
359 // | 21 22 23 24 25 26 27 28 29 30 31 01 02 03 04 05 06 |
360 // | July August |
361 // | |
362 // | ADD rate 1.7/d Estimated completion 8/12/2010 |
363 // | Don/Delete rate 1.3/d |
364 // +---------------------------------------------------------------------+
render()365 std::string Chart::render ()
366 {
367 if (_graph_height < 5 || // a 4-line graph is essentially unreadable.
368 _graph_width < 2) // A single-bar graph is useless.
369 {
370 return std::string ("Terminal window too small to draw a graph.\n");
371 }
372
373 else if (_graph_height > 1000 || // each line is a string allloc
374 _graph_width > 1000)
375 {
376 return std::string ("Terminal window too large to draw a graph.\n");
377 }
378
379 if (_max_value == 0)
380 Context::getContext ().footnote ("No matches.");
381
382 // Create a grid, folded into a string.
383 _grid = "";
384 for (int i = 0; i < _height; ++i)
385 _grid += std::string (_width, ' ') + '\n';
386
387 // Title.
388 std::string title = _period == 'D' ? "Daily"
389 : _period == 'W' ? "Weekly"
390 : "Monthly";
391 title += std::string (" Burndown");
392 _grid.replace (LOC (0, (_width - title.length ()) / 2), title.length (), title);
393
394 // Legend.
395 _grid.replace (LOC (_graph_height / 2 - 1, _width - 10), 10, "DD " + leftJustify ("Done", 7));
396 _grid.replace (LOC (_graph_height / 2, _width - 10), 10, "SS " + leftJustify ("Started", 7));
397 _grid.replace (LOC (_graph_height / 2 + 1, _width - 10), 10, "PP " + leftJustify ("Pending", 7));
398
399 // Determine y-axis labelling.
400 std::vector <int> _labels;
401 yLabels (_labels);
402 _max_label = (int) log10 ((double) _labels[2]) + 1;
403
404 // Draw y-axis.
405 for (int i = 0; i < _graph_height; ++i)
406 _grid.replace (LOC (i + 1, _max_label + 1), 1, "|");
407
408 // Draw y-axis labels.
409 char label [12];
410 snprintf (label, 12, "%*d", _max_label, _labels[2]);
411 _grid.replace (LOC (1, _max_label - strlen (label)), strlen (label), label);
412 snprintf (label, 12, "%*d", _max_label, _labels[1]);
413 _grid.replace (LOC (1 + (_graph_height / 2), _max_label - strlen (label)), strlen (label), label);
414 _grid.replace (LOC (_graph_height + 1, _max_label - 1), 1, "0");
415
416 // Draw x-axis.
417 _grid.replace (LOC (_height - 6, _max_label + 1), 1, "+");
418 _grid.replace (LOC (_height - 6, _max_label + 2), _graph_width, std::string (_graph_width, '-'));
419
420 // Draw x-axis labels.
421 std::vector <time_t> bars_in_sequence;
422 for (auto& bar : _bars)
423 bars_in_sequence.push_back (bar.first);
424
425 std::sort (bars_in_sequence.begin (), bars_in_sequence.end ());
426 std::string _major_label;
427 for (auto& seq : bars_in_sequence)
428 {
429 Bar bar = _bars[seq];
430
431 // If it fits within the allowed space.
432 if (bar._offset < _actual_bars)
433 {
434 _grid.replace (LOC (_height - 5, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), bar._minor_label.length (), bar._minor_label);
435
436 if (_major_label != bar._major_label)
437 _grid.replace (LOC (_height - 4, _max_label + 2 + ((_actual_bars - bar._offset - 1) * 3)), bar._major_label.length (), ' ' + bar._major_label);
438
439 _major_label = bar._major_label;
440 }
441 }
442
443 // Draw bars.
444 for (auto& seq : bars_in_sequence)
445 {
446 Bar bar = _bars[seq];
447
448 // If it fits within the allowed space.
449 if (bar._offset < _actual_bars)
450 {
451 int pending = ( bar._pending * _graph_height) / _labels[2];
452 int started = ((bar._pending + bar._started) * _graph_height) / _labels[2];
453 int done = ((bar._pending + bar._started + bar._done + _carryover_done) * _graph_height) / _labels[2];
454
455 for (int b = 0; b < pending; ++b)
456 _grid.replace (LOC (_graph_height - b, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), 2, "PP");
457
458 for (int b = pending; b < started; ++b)
459 _grid.replace (LOC (_graph_height - b, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), 2, "SS");
460
461 for (int b = started; b < done; ++b)
462 _grid.replace (LOC (_graph_height - b, _max_label + 3 + ((_actual_bars - bar._offset - 1) * 3)), 2, "DD");
463 }
464 }
465
466 // Draw rates.
467 calculateRates ();
468 char rate[12];
469 if (_net_fix_rate != 0.0)
470 snprintf (rate, 12, "%.1f/d", _net_fix_rate);
471 else
472 strcpy (rate, "-");
473
474 _grid.replace (LOC (_height - 2, _max_label + 3), 22 + strlen (rate), std::string ("Net Fix Rate: ") + rate);
475
476 // Draw completion date.
477 if (_completion.length ())
478 _grid.replace (LOC (_height - 1, _max_label + 3), 22 + _completion.length (), "Estimated completion: " + _completion);
479
480 optimizeGrid ();
481
482 if (Context::getContext ().color ())
483 {
484 // Colorize the grid.
485 Color color_pending (Context::getContext ().config.get ("color.burndown.pending"));
486 Color color_done (Context::getContext ().config.get ("color.burndown.done"));
487 Color color_started (Context::getContext ().config.get ("color.burndown.started"));
488
489 // Replace DD, SS, PP with colored strings.
490 std::string::size_type i;
491 while ((i = _grid.find ("PP")) != std::string::npos)
492 _grid.replace (i, 2, color_pending.colorize (" "));
493
494 while ((i = _grid.find ("SS")) != std::string::npos)
495 _grid.replace (i, 2, color_started.colorize (" "));
496
497 while ((i = _grid.find ("DD")) != std::string::npos)
498 _grid.replace (i, 2, color_done.colorize (" "));
499 }
500 else
501 {
502 // Replace DD, SS, PP with ./+/X strings.
503 std::string::size_type i;
504 while ((i = _grid.find ("PP")) != std::string::npos)
505 _grid.replace (i, 2, " X");
506
507 while ((i = _grid.find ("SS")) != std::string::npos)
508 _grid.replace (i, 2, " +");
509
510 while ((i = _grid.find ("DD")) != std::string::npos)
511 _grid.replace (i, 2, " .");
512 }
513
514 return _grid;
515 }
516
517 ////////////////////////////////////////////////////////////////////////////////
518 // _grid =~ /\s+$//g
optimizeGrid()519 void Chart::optimizeGrid ()
520 {
521 std::string::size_type ws;
522 while ((ws = _grid.find (" \n")) != std::string::npos)
523 {
524 auto non_ws = ws;
525 while (_grid[non_ws] == ' ')
526 --non_ws;
527
528 _grid.replace (non_ws + 1, ws - non_ws + 1, "\n");
529 }
530 }
531
532 ////////////////////////////////////////////////////////////////////////////////
quantize(const Datetime & input,char period)533 Datetime Chart::quantize (const Datetime& input, char period)
534 {
535 if (period == 'D') return input.startOfDay ();
536 if (period == 'W') return input.startOfWeek ();
537 if (period == 'M') return input.startOfMonth ();
538
539 return input;
540 }
541
542 ////////////////////////////////////////////////////////////////////////////////
increment(const Datetime & input,char period)543 Datetime Chart::increment (const Datetime& input, char period)
544 {
545 // Move to the next period.
546 int d = input.day ();
547 int m = input.month ();
548 int y = input.year ();
549
550 int days;
551
552 switch (period)
553 {
554 case 'D':
555 if (++d > Datetime::daysInMonth (y, m))
556 {
557 d = 1;
558
559 if (++m == 13)
560 {
561 m = 1;
562 ++y;
563 }
564 }
565 break;
566
567 case 'W':
568 d += 7;
569 days = Datetime::daysInMonth (y, m);
570 if (d > days)
571 {
572 d -= days;
573
574 if (++m == 13)
575 {
576 m = 1;
577 ++y;
578 }
579 }
580 break;
581
582 case 'M':
583 d = 1;
584 if (++m == 13)
585 {
586 m = 1;
587 ++y;
588 }
589 break;
590 }
591
592 return Datetime (y, m, d, 0, 0, 0);
593 }
594
595 ////////////////////////////////////////////////////////////////////////////////
decrement(const Datetime & input,char period)596 Datetime Chart::decrement (const Datetime& input, char period)
597 {
598 // Move to the previous period.
599 int d = input.day ();
600 int m = input.month ();
601 int y = input.year ();
602
603 switch (period)
604 {
605 case 'D':
606 if (--d == 0)
607 {
608 if (--m == 0)
609 {
610 m = 12;
611 --y;
612 }
613
614 d = Datetime::daysInMonth (y, m);
615 }
616 break;
617
618 case 'W':
619 d -= 7;
620 if (d < 1)
621 {
622 if (--m == 0)
623 {
624 m = 12;
625 y--;
626 }
627
628 d += Datetime::daysInMonth (y, m);
629 }
630 break;
631
632 case 'M':
633 d = 1;
634 if (--m == 0)
635 {
636 m = 12;
637 --y;
638 }
639 break;
640 }
641
642 return Datetime (y, m, d, 0, 0, 0);
643 }
644
645 ////////////////////////////////////////////////////////////////////////////////
646 // Do '_bars[epoch] = Bar' for every bar that may appear on a chart.
generateBars()647 void Chart::generateBars ()
648 {
649 Bar bar;
650
651 // Determine the last bar date.
652 Datetime cursor;
653 switch (_period)
654 {
655 case 'D': cursor = Datetime ().startOfDay (); break;
656 case 'W': cursor = Datetime ().startOfWeek (); break;
657 case 'M': cursor = Datetime ().startOfMonth (); break;
658 }
659
660 // Iterate and determine all the other bar dates.
661 char str[12];
662 for (int i = 0; i < _estimated_bars; ++i)
663 {
664 // Create the major and minor labels.
665 switch (_period)
666 {
667 case 'D': // month/day
668 {
669 std::string month = Datetime::monthName (cursor.month ());
670 bar._major_label = month.substr (0, 3);
671
672 snprintf (str, 12, "%02d", cursor.day ());
673 bar._minor_label = str;
674 }
675 break;
676
677 case 'W': // year/week
678 snprintf (str, 12, "%d", cursor.year ());
679 bar._major_label = str;
680
681 snprintf (str, 12, "%02d", cursor.week ());
682 bar._minor_label = str;
683 break;
684
685 case 'M': // year/month
686 snprintf (str, 12, "%d", cursor.year ());
687 bar._major_label = str;
688
689 snprintf (str, 12, "%02d", cursor.month ());
690 bar._minor_label = str;
691 break;
692 }
693
694 bar._offset = i;
695 _bars[cursor.toEpoch ()] = bar;
696
697 // Record the earliest date, for use as a cutoff when scanning data.
698 _earliest = cursor;
699
700 // Move to the previous period.
701 cursor = decrement (cursor, _period);
702 }
703 }
704
705 ////////////////////////////////////////////////////////////////////////////////
maxima()706 void Chart::maxima ()
707 {
708 _max_value = 0;
709 _max_label = 1;
710
711 for (auto& bar : _bars)
712 {
713 // Determine _max_label.
714 int total = bar.second._pending +
715 bar.second._started +
716 bar.second._done +
717 _carryover_done;
718
719 // Determine _max_value.
720 if (total > _max_value)
721 _max_value = total;
722
723 int length = (int) log10 ((double) total) + 1;
724 if (length > _max_label)
725 _max_label = length;
726 }
727
728 // How many bars can be shown?
729 _actual_bars = (_width - _max_label - 14) / 3;
730 _graph_width = _width - _max_label - 14;
731 }
732
733 ////////////////////////////////////////////////////////////////////////////////
734 // Given the vertical chart area size (graph_height), the largest value
735 // (_max_value), populate a vector of labels for the y axis.
yLabels(std::vector<int> & labels)736 void Chart::yLabels (std::vector <int>& labels)
737 {
738 // Calculate may Y using a nice algorithm that rounds the data.
739 int high = burndown_size (_max_value);
740 int half = high / 2;
741
742 labels.push_back (0);
743 labels.push_back (half);
744 labels.push_back (high);
745 }
746
747 ////////////////////////////////////////////////////////////////////////////////
calculateRates()748 void Chart::calculateRates ()
749 {
750 // Q: Why is this equation written out as a debug message?
751 // A: People are going to want to know how the rates and the completion date
752 // are calculated. This may also help debugging.
753 std::stringstream peak_message;
754 peak_message << "Chart::calculateRates Maximum of "
755 << _peak_count
756 << " pending tasks on "
757 << (Datetime (_peak_epoch).toISO ())
758 << ", with currently "
759 << _current_count
760 << " pending tasks";
761 Context::getContext ().debug (peak_message.str ());
762
763 // If there are no current pending tasks, then it is meaningless to find
764 // rates or estimated completion date.
765 if (_current_count == 0)
766 return;
767
768 // If there is a net fix rate, and the peak was at least three days ago.
769 Datetime now;
770 Datetime peak (_peak_epoch);
771 if (_peak_count > _current_count &&
772 (now - peak) > 3 * 86400)
773 {
774 // Fixes per second. Not a large number.
775 auto fix_rate = 1.0 * (_peak_count - _current_count) / (now.toEpoch () - _peak_epoch);
776 _net_fix_rate = fix_rate * 86400;
777
778 std::stringstream rate_message;
779 rate_message << "Chart::calculateRates Net reduction is "
780 << (_peak_count - _current_count)
781 << " tasks in "
782 << Duration (now.toEpoch () - _peak_epoch).formatISO ()
783 << " = "
784 << _net_fix_rate
785 << " tasks/d";
786 Context::getContext ().debug (rate_message.str ());
787
788 Duration delta (static_cast <time_t> (_current_count / fix_rate));
789 Datetime end = now + delta.toTime_t ();
790
791 // Prefer dateformat.report over dateformat.
792 std::string format = Context::getContext ().config.get ("dateformat.report");
793 if (format == "")
794 {
795 format = Context::getContext ().config.get ("dateformat");
796 if (format == "")
797 format = "Y-M-D";
798 }
799
800 _completion = end.toString (format)
801 + " ("
802 + delta.formatVague ()
803 + ')';
804
805 std::stringstream completion_message;
806 completion_message << "Chart::calculateRates ("
807 << _current_count
808 << " tasks / "
809 << _net_fix_rate
810 << ") = "
811 << delta.format ()
812 << " --> "
813 << end.toISO ();
814 Context::getContext ().debug (completion_message.str ());
815 }
816 else
817 {
818 _completion = "No convergence";
819 }
820 }
821
822 ////////////////////////////////////////////////////////////////////////////////
round_up_to(unsigned n,unsigned target)823 unsigned Chart::round_up_to (unsigned n, unsigned target)
824 {
825 return n + target - (n % target);
826 }
827
828 ////////////////////////////////////////////////////////////////////////////////
burndown_size(unsigned ntasks)829 unsigned Chart::burndown_size (unsigned ntasks)
830 {
831 // Nearest 2
832 if (ntasks < 20)
833 return round_up_to (ntasks, 2);
834
835 // Nearest 10
836 if (ntasks < 50)
837 return round_up_to (ntasks, 10);
838
839 // Nearest 20
840 if (ntasks < 100)
841 return round_up_to (ntasks, 20);
842
843 // Choose the number from here rounded up to the nearest 10% of the next
844 // highest power of 10 or half of power of 10.
845 const auto count = (unsigned) log10 (static_cast<double>(std::numeric_limits<unsigned>::max ()));
846 unsigned half = 500;
847 unsigned full = 1000;
848
849 // We start at two because we handle 5, 10, 50, and 100 above.
850 for (unsigned i = 2; i < count; ++i)
851 {
852 if (ntasks < half)
853 return round_up_to (ntasks, half / 10);
854
855 if (ntasks < full)
856 return round_up_to (ntasks, full / 10);
857
858 half *= 10;
859 full *= 10;
860 }
861
862 // Round up to max of unsigned.
863 return std::numeric_limits<unsigned>::max ();
864 }
865
866 ////////////////////////////////////////////////////////////////////////////////
CmdBurndownMonthly()867 CmdBurndownMonthly::CmdBurndownMonthly ()
868 {
869 _keyword = "burndown.monthly";
870 _usage = "task <filter> burndown.monthly";
871 _description = "Shows a graphical burndown chart, by month";
872 _read_only = true;
873 _displays_id = false;
874 _needs_gc = true;
875 _uses_context = true;
876 _accepts_filter = true;
877 _accepts_modifications = false;
878 _accepts_miscellaneous = false;
879 _category = Command::Category::graphs;
880 }
881
882 ////////////////////////////////////////////////////////////////////////////////
execute(std::string & output)883 int CmdBurndownMonthly::execute (std::string& output)
884 {
885 int rc = 0;
886
887 // Scan the pending tasks, applying any filter.
888 handleUntil ();
889 handleRecurrence ();
890 Filter filter;
891 std::vector <Task> filtered;
892 filter.subset (filtered);
893
894 // Create a chart, scan the tasks, then render.
895 Chart chart ('M');
896 chart.scanForPeak (filtered);
897 chart.scan (filtered);
898 output = chart.render ();
899 return rc;
900 }
901
902 ////////////////////////////////////////////////////////////////////////////////
CmdBurndownWeekly()903 CmdBurndownWeekly::CmdBurndownWeekly ()
904 {
905 _keyword = "burndown.weekly";
906 _usage = "task <filter> burndown.weekly";
907 _description = "Shows a graphical burndown chart, by week";
908 _read_only = true;
909 _displays_id = false;
910 _needs_gc = true;
911 _uses_context = true;
912 _accepts_filter = true;
913 _accepts_modifications = false;
914 _accepts_miscellaneous = false;
915 _category = Command::Category::graphs;
916 }
917
918 ////////////////////////////////////////////////////////////////////////////////
execute(std::string & output)919 int CmdBurndownWeekly::execute (std::string& output)
920 {
921 int rc = 0;
922
923 // Scan the pending tasks, applying any filter.
924 handleUntil ();
925 handleRecurrence ();
926 Filter filter;
927 std::vector <Task> filtered;
928 filter.subset (filtered);
929
930 // Create a chart, scan the tasks, then render.
931 Chart chart ('W');
932 chart.scanForPeak (filtered);
933 chart.scan (filtered);
934 output = chart.render ();
935 return rc;
936 }
937
938 ////////////////////////////////////////////////////////////////////////////////
CmdBurndownDaily()939 CmdBurndownDaily::CmdBurndownDaily ()
940 {
941 _keyword = "burndown.daily";
942 _usage = "task <filter> burndown.daily";
943 _description = "Shows a graphical burndown chart, by day";
944 _read_only = true;
945 _displays_id = false;
946 _needs_gc = true;
947 _uses_context = true;
948 _accepts_filter = true;
949 _accepts_modifications = false;
950 _accepts_miscellaneous = false;
951 _category = Command::Category::graphs;
952 }
953
954 ////////////////////////////////////////////////////////////////////////////////
execute(std::string & output)955 int CmdBurndownDaily::execute (std::string& output)
956 {
957 int rc = 0;
958
959 // Scan the pending tasks, applying any filter.
960 handleUntil ();
961 handleRecurrence ();
962 Filter filter;
963 std::vector <Task> filtered;
964 filter.subset (filtered);
965
966 // Create a chart, scan the tasks, then render.
967 Chart chart ('D');
968 chart.scanForPeak (filtered);
969 chart.scan (filtered);
970 output = chart.render ();
971 return rc;
972 }
973
974 ////////////////////////////////////////////////////////////////////////////////
975