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