1 /**This file contains all of the plots types in Plot2kill, which can be
2  * drawn on screen using plot2kill.figure.Figure.
3  *
4  * Copyright (C) 2010-2012 David Simcha
5  *
6  * License:
7  *
8  * Boost Software License - Version 1.0 - August 17th, 2003
9  *
10  * Permission is hereby granted, free of charge, to any person or organization
11  * obtaining a copy of the software and accompanying documentation covered by
12  * this license (the "Software") to use, reproduce, display, distribute,
13  * execute, and transmit the Software, and to prepare derivative works of the
14  * Software, and to permit third-parties to whom the Software is furnished to
15  * do so, all subject to the following:
16  *
17  * The copyright notices in the Software and this entire statement, including
18  * the above license grant, this restriction and the following disclaimer,
19  * must be included in all copies of the Software, in whole or in part, and
20  * all derivative works of the Software, unless such copies or derivative
21  * works are solely in the form of machine-executable object code generated by
22  * a source language processor.
23  *
24  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26  * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
27  * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
28  * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
29  * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
30  * DEALINGS IN THE SOFTWARE.
31  */
32 
33 module plot2kill.plot;
34 
35 import plot2kill.figure, plot2kill.util, plot2kill.hierarchical;
36 import std.random, std.typetuple;
37 
38 version(dfl) {
39     public import plot2kill.dflwrapper;
40 } else {
41     public import plot2kill.gtkwrapper;
42 }
43 
44 /**Abstract base class for all types of plot objects.*/
45 abstract class Plot {
46 protected:
47     // Top of the plot.
48     double upperLim = -double.infinity;
49 
50     // Bottom of the plot.
51     double lowerLim = double.infinity;
52 
53     // Leftmost limit of the plot.
54     double leftLim = double.infinity;
55 
56     // Rightmost limit of the plot.
57     double rightLim = -double.infinity;
58 
59     string _legendText;
60     
61     // Hook in case the plot needs to reset its limits after seeing 
62     // the figure it's about to be drawn to.
63     void resetLimits(Figure figure) {}
64 
65 package:
66     PlotSize measureLegend(Font legendFont, FigureBase fig) {
67         if(!legendText().length) return PlotSize(0, 0);
68         auto ret = fig.measureText(legendText(), legendFont);
69         ret.width += legendSymbolSize + legendSymbolTextSpace;
70         ret.height = max(ret.height, legendSymbolSize);
71         return PlotSize(ret.width, ret.height);
72     }
73 
74 public:
75     // This should be package but for some reason package functions can't
76     // be abstract.
77     abstract void drawLegendSymbol(FigureBase fig, PlotRect where);
78 
79     /* Draw the plot on Figure using the rectangular area described by the
80      * integer parameters.
81      */
82     abstract void drawPlot(Figure, double, double, double, double);
83 
84     /**
85     Returns true if this plot type supports legends, otherwise false.
86     */
87     bool hasLegend() {
88         return true;
89     }
90 
91     /**Convenience method that instantiates a Figure object with this plot.
92      * Useful for creating single-plot figures w/o a lot of boilerplate.
93      *
94      * Examples:
95      * ---
96      * auto hist = Histogram([1,2,3,4,5], 3).toFigure;
97      * hist.showAsMain();
98      * ---
99      */
100     Figure toFigure()  {
101         return new Figure(this);
102     }
103 
104     /**Instantiates a Figure object with this plot and also places the default
105      * axes and tick labeling and title for the plot type, if any, on the
106      * Figure.  If a plot type has no default labeling, simply forwards to
107      * toFigure().
108      */
109     Figure toLabeledFigure()  {
110         return toFigure;
111     }
112 
113     /**The leftmost point on the plot.*/
114     double leftMost()  {
115         return leftLim;
116     }
117 
118     /**The rightmost point on the plot.*/
119     double rightMost()  {
120         return rightLim;
121     }
122 
123     /**The topmost point on the plot.*/
124     double topMost()  {
125         return upperLim;
126     }
127 
128     /**The bottommost point on the plot.*/
129     double bottomMost()  {
130         return lowerLim;
131     }
132 
133     ///
134     string legendText()() {
135         return _legendText;
136     }
137 
138     ///
139     This legendText(this This)(string newText) {
140         _legendText = newText;
141         return cast(This) this;
142     }
143 }
144 
145 /**A basic bar plot.*/
146 class BarPlot : Plot {
147 private:
148     double[] _centers;
149     double[] _heights;
150     double[] _lowerErrors;
151     double[] _upperErrors;
152     BarPlot _stackOn;
153     bool _outlineBar = true;
154 
155     // Return the bottom of the bar at a given index.  This is zero unless
156     // we have a stacked plot.
157     double bottom(size_t index) pure nothrow {
158         return (_stackOn is null) ? 0 :
159             _stackOn.bottom(index) + _stackOn._heights[index];
160     }
161 
162     void fixBounds() {
163         // We should never have both error bars and stacking.  The API doesn't
164         // allow this, but double-check here.
165         enforce(_stackOn is null ||
166             (_lowerErrors.length == 0 && upperErrors.length == 0));
167 
168         this.leftLim = reduce!min(double.infinity, this.centers) - width / 2;
169         this.rightLim = reduce!max(-double.infinity, this.centers) + width / 2;
170         lowerLim = double.infinity;
171         upperLim = -double.infinity;
172 
173         foreach(i, height; _heights) {
174             if(lowerErrors.length > 0) {
175                 lowerLim = min(height - lowerErrors[i], lowerLim);
176             } else {
177                 lowerLim = min(height + bottom(i), lowerLim);
178             }
179 
180             if(upperErrors.length > 0) {
181                 upperLim = max(height + upperErrors[i], upperLim);
182             } else {
183                 upperLim = max(height + bottom(i), upperLim);
184             }
185         }
186 
187         // Don't blend error bars into axes.  Handle the case where a boundary
188         // is 0 as a special case, since it would look worse to have the axis
189         // not be exactly zero if all bars are positive or all are negative than
190         // to blend an error bar into an axis.
191         if(upperErrors.length || lowerErrors.length) {
192             immutable pad = 0.01 * (upperLim - lowerLim);
193             if(upperErrors.length && upperLim != 0) {
194                 upperLim += pad;
195             }
196             if(lowerErrors.length && lowerLim != 0) {
197                 lowerLim -= pad;
198             }
199         }
200 
201         if(lowerLim > 0) {
202             lowerLim = 0;
203         }
204 
205         if(upperLim < 0) {
206             upperLim = 0;
207         }
208     }
209 
210 
211     this(double[] centers, double[] heights, double width) {
212         this._centers = centers;
213         this._heights = heights;
214         this.width = width;
215         _barColor = getColor(0, 0, 255);
216         fixBounds();
217     }
218 
219 protected:
220     override void drawPlot(
221         Figure form,
222         double leftMargin,
223         double topMargin,
224         double plotWidth,
225         double plotHeight
226     ) {
227 
228         mixin(toPixels);
229         mixin(drawErrorMixin);
230 
231         immutable multiplier = plotHeight / (this.upperLim - this.lowerLim);
232         auto brush = form.getBrush(_barColor);
233         scope(exit) doneWith(brush);
234 
235         auto blackPen = form.getPen(getColor(0, 0, 0));
236         scope(exit) doneWith(blackPen);
237 
238         foreach(i, center; centers) {
239             immutable zeroPoint = toPixelsY(bottom(i));
240             immutable height = heights[i];
241             immutable left = center - width / 2;
242             immutable right = center + width / 2;
243             immutable leftPixels = toPixelsX(left);
244             immutable rightPixels = toPixelsX(right);
245             immutable widthPixels = rightPixels - leftPixels;
246 
247             immutable heightPixels = roundTo!int(abs(height) * multiplier);
248 
249             immutable startAt = (height > 0) ?
250                                 zeroPoint - heightPixels :
251                                 zeroPoint;
252             form.fillClippedRectangle(brush, leftPixels,
253                 startAt, widthPixels, heightPixels);
254 
255             if(_outlineBar) {
256                 form.drawClippedRectangle(blackPen, leftPixels,
257                     startAt, widthPixels, heightPixels);
258             }
259 
260             // Do error bars.
261             if(lowerErrors.length) {
262                 drawErrorBar(blackPen,
263                     center, height, height - lowerErrors[i], width / 2);
264             }
265             if(upperErrors.length) {
266                 drawErrorBar(blackPen,
267                     center, height, height + upperErrors[i], width / 2);
268             }
269         }
270 
271         if(lowerLim < 0) {
272             immutable zeroPoint = toPixelsY(0);
273             auto pen = form.getPen(getColor(0, 0, 0), 2);
274             // Draw line across figure at the zero point.
275             form.drawClippedLine(pen,
276                 PlotPoint(toPixelsX(leftLim), zeroPoint),
277                 PlotPoint(toPixelsX(rightLim), zeroPoint)
278             );
279         }
280     }
281 
282     override void drawLegendSymbol(FigureBase fig, PlotRect where) {
283         drawFillLegend(_barColor, fig, where);
284     }
285 
286     Color _barColor;
287 
288 public:
289     /**Controls the color of the bar.  Defaults to blue.*/
290     final Color barColor()() {
291         return _barColor;
292     }
293 
294     /// Setter
295     final This barColor(this This)(Color newColor) {
296         _barColor = newColor;
297         return cast(This) this;
298     }
299 
300     /**Controls whether each bar is outlined in a black rectange.*/
301     final bool outlineBar()() {
302         return _outlineBar;
303     }
304 
305     final This outlineBar(this This)(bool shouldOutline) {
306         _outlineBar = shouldOutline;
307         return cast(This) this;
308     }
309 
310     ///
311     immutable double width;
312 
313     /**Create a BarPlot.  centers and heights must be input ranges with elements
314      * implicitly convertible to double.  width determines the width of each
315      * bar relative to the X-axis scale and must be greater than 0.
316      */
317     static BarPlot opCall(R1, R2)(R1 centers, R2 heights, double width)
318     if(isInputRange!R1 && is(ElementType!R1 : double) &&
319        isInputRange!R2 && is(ElementType!R2 : double)) {
320 
321         auto c = toDoubleArray(centers);
322         auto h = toDoubleArray(heights);
323 
324         enforce(c.length == h.length,
325             "Centers and heights must be same length for bar plot.");
326 
327         enforce(width > 0, "Width must be >0 for bar plot.");
328         auto ret = new typeof(return)(c, h, width);
329         return ret;
330     }
331 
332     /**
333     Create a BarPlot with error bars.  lowerErrors and upperErrors
334      must be input ranges with elements implicitly convertible to double for
335      error bars to be shown.  Any other value, such as null or 0, will result
336      in no error bars being shown.  Therefore, to only show, for example,
337      upper erros, simply pass in null or 0 for the lower errors.
338 
339      To draw symmetric error bars, simply pass in the same range for
340      lowerErrors and upperErrors.  However, note that if you do this,
341      the range will need to be a forward range, not an input range.
342      */
343      static BarPlot opCall(R1, R2, R3, R4)
344      (R1 centers, R2 heights, double width, R3 lowerErrors, R4 upperErrors)
345      if(isInputRange!R1 && is(ElementType!R1 : double) &&
346         isInputRange!R2 && is(ElementType!R2 : double)) {
347 
348         auto ret = opCall(centers, heights, width);
349         static if(isForwardRange!R3) {
350             ret._lowerErrors = toDoubleArray(lowerErrors.save);
351         } else static if(isInputRange!R3) {
352             ret._lowerErrors = toDoubleArray(lowerErrors);
353         }
354 
355         static if(isForwardRange!R4) {
356             ret._upperErrors = toDoubleArray(upperErrors.save);
357         } else static if(isInputRange!R4) {
358             ret._upperErrors = toDoubleArray(upperErrors);
359         }
360 
361         enforce(ret.upperErrors.length == 0 || ret.upperErrors.length ==
362             ret.centers.length, "Length of upperErrors must equal number of bars.");
363         enforce(ret.lowerErrors.length == 0 || ret.lowerErrors.length ==
364             ret.centers.length, "Length of lowerErrors must equal number of bars.");
365 
366         ret.fixBounds();
367         return ret;
368     }
369 
370     /**
371     Create a BarPlot that is to be stacked on top of this one.  For a
372     convenience function for creating such a plot, see stackedBar().
373 
374     Warning:  Creating a cycle (two BarPlots mutually on top of each other)
375               will cause infinite loops, stack overflows and otherwise bad
376               things.
377     */
378     BarPlot stack(R)(R heights) {
379         auto ret = new typeof(return)
380             (_centers, toDoubleArray(heights), width);
381         ret._stackOn = this;
382         ret.fixBounds();
383         return ret;
384     }
385 
386     /**Scale this object by a factor of scaleFactor in the X direction.
387      * This is useful for getting it onto the same scale as another plot.
388      */
389     void scaleCenters(double scaleFactor) {
390         enforce(isFinite(scaleFactor), "Can't scale by infinity or NaN.");
391         _centers[] *= scaleFactor;
392         fixBounds();
393     }
394 
395     /**Scale this object by a factor of scaleFactor in the Y direction.
396      * This is useful for getting it onto the same scale as another plot.*/
397     void scaleHeights(double scaleFactor) {
398         enforce(isFinite(scaleFactor), "Can't scale by infinity or NaN.");
399         _heights[] *= scaleFactor;
400         fixBounds();
401     }
402 
403     /**Shift this graph by shiftBy units in the X direction.
404      * This is useful for getting it onto the same scale as another plot.
405      */
406     void shiftCenters(double shiftBy) {
407         enforce(isFinite(shiftBy), "Can't shift by infinity or NaN.");
408         _centers[] += shiftBy;
409         fixBounds();
410     }
411 
412     /**Get the centers of the bars.*/
413     final const(double)[] centers()  {
414         return _centers;
415     }
416 
417     /**Get the heights of the bars.*/
418     final const(double)[] heights()  {
419         return _heights;
420     }
421 
422     ///
423     final const(double)[] lowerErrors()  {
424         return _lowerErrors;
425     }
426 
427     ///
428     final const(double)[] upperErrors()  {
429         return _upperErrors;
430     }
431 
432     /**The default labeling includes each center receiving its own x tick
433      * label if there are <= 10 bars on the graph.
434      */
435     override Figure toLabeledFigure()  {
436         auto ret = toFigure;
437         if(centers.length <= 10) {
438             ret.xTickLabels(centers);
439         }
440 
441         return ret;
442     }
443 }
444 
445 private template isRoR(T) {
446     enum isRoR = isInputRange!T && isInputRange!(ElementType!T);
447 }
448 
449 /**
450 Create an array of bar plots that, when inserted into a Figure, will effectively
451 become a grouped bar plot.
452 
453 Parameters:
454 
455 centers:
456 An input range of the overall center of each group of bars.
457 
458 data:
459 A range of ranges, with one range for each bar color.  This range should have
460 one number for each group.
461 
462 width:
463 The combined width of all of the bars for each group.
464 
465 legendText:
466 An array of strings, one for each bar color, or null if no legend is desired.
467 
468 colors:
469 An array of colors, one for each bar.  If none is provided, the default
470 colors are used.  These are, in order, blue, red, green, black, orange,
471 and purple.  If more colors are needed, they are generated using a random
472 number generator with a deterministic seed.
473 
474 Examples:
475 ---
476 
477 // Make a plot with three groups:  One for "In Meeting", one for "On Phone",
478 // and one for "Coding".  The plot will also have two bar colors:  One for
479 // "Without Caffeine" and one for "With Caffeine".
480 auto withoutCaffeine = [8, 6, 3];
481 auto withCaffeine = [5, 3, 1];
482 auto sleepinessPlot = groupedBar(
483     iota(3), [withoutCaffeine, withCaffeine], 0.6,
484     ["W/o Caffeine", "W/ Caffeine"],
485     [getColor(64, 64, 255), getColor(255, 64, 64)]
486 );
487 auto sleepinessFig = Figure(sleepinessPlot)
488     .title("Sleepiness Survey")
489     .yLabel("Sleepiness Rating")
490     .xLabel("Activity")
491     .legendLocation(LegendLocation.right)
492     .horizontalGrid(true)
493     .xTickLabels(
494         iota(3),
495         ["In Meeting", "On Phone", "Coding"]
496     );
497 ---
498 */
499 BarPlot[] groupedBar(R1, R2)(
500     R1 centers,
501     R2 data,
502     double width,
503     string[] legendText = null,
504     Color[] colors = null
505 )
506 if(isInputRange!R1 && isRoR!R2) {
507      return groupedBar(centers, data, (double[][]).init, (double[][]).init,
508         width, legendText, colors);
509 }
510 
511 /**
512 Create a grouped bar plot with error bars.  lowerErrors and upperErrors
513 must either have the same dimensions as data or be empty.
514 */
515 BarPlot[] groupedBar(R1, R2, R3, R4)(
516     R1 centers,
517     R2 data,
518     R3 lowerErrors,
519     R4 upperErrors,
520     double width,
521     string[] legendText = null,
522     Color[] colors = null
523 )
524 if(isInputRange!R1 && isRoR!R2 && isRoR!R3 && isRoR!R4) {
525     return groupStackImpl(CompoundBar.group, centers, data, lowerErrors,
526         upperErrors, width, legendText, colors);
527 }
528 
529 /**
530 Create a stacked bar plot.  The usage mechanics are identical to those of the
531 no error bar overload of groupedBar().
532 
533 Examples:
534 ---
535 // Stack coffee and tea consumption on top of each other.
536 Figure(
537     stackedBar(iota(3), [[5, 3, 1], [1, 2, 3]], 0.6,
538         ["Coffee", "Tea"]
539     )
540 ).legendLocation(LegendLocation.right)
541     .title("Caffeine Consumption")
542     .xLabel("Time of Day")
543     .xTickLabels(iota(3), ["Morning", "Afternoon", "Evening"])
544     .yLabel("Beverages")
545     .showAsMain();
546 ---
547 */
548 BarPlot[] stackedBar(R1, R2)(
549     R1 centers,
550     R2 data,
551     double width,
552     string[] legendText = null,
553     Color[] colors = null
554 )
555 if(isInputRange!R1 && isRoR!R2) {
556     return groupStackImpl(CompoundBar.stack, centers, data, (double[][]).init,
557         (double[][]).init, width, legendText, colors);
558 }
559 
560 private enum CompoundBar {
561     group,
562     stack
563 }
564 
565 private BarPlot[] groupStackImpl(R1, R2, R3, R4)(
566     CompoundBar whatToDo,
567     R1 centers,
568     R2 data,
569     R3 lowerErrors,
570     R4 upperErrors,
571     double width,
572     string[] legendText = null,
573     Color[] colors = null
574 ) {
575     if(whatToDo == CompoundBar.stack) {
576         enforce(lowerErrors.length == 0 && upperErrors.length == 0);
577     }
578 
579     auto centerArr = toDoubleArray(centers);
580     auto dataArr = array(map!toDoubleArray(data));
581     auto lerrArr = array(map!toDoubleArray(lowerErrors));
582     auto uerrArr = array(map!toDoubleArray(upperErrors));
583 
584     foreach(elem; tuple(lerrArr, uerrArr, legendText, colors)) {
585         enforce(elem.empty || elem.length == dataArr.length,
586             "Range length mismatch in groupedBar.");
587     }
588 
589     if(!colors.length) {
590         colors = [
591             getColor(0, 0, 255), getColor(255, 0, 0), getColor(0, 255, 0),
592             getColor(0, 0, 0), getColor(255, 128, 0), getColor(255, 0, 255)
593         ];
594 
595         // If we need more colors, generate them "randomly" but from a
596         // deterministic seed.
597         auto gen = Random(31415);
598         while(colors.length < dataArr.length) {
599             colors ~= getColor(
600                 uniform!"[]"(cast(ubyte) 0, cast(ubyte) 255, gen),
601                 uniform!"[]"(cast(ubyte) 0, cast(ubyte) 255, gen),
602                 uniform!"[]"(cast(ubyte) 0, cast(ubyte) 255, gen)
603             );
604         }
605     }
606 
607     BarPlot[] ret;
608     foreach(int groupIndex, group; dataArr) {
609         BarPlot plot;
610 
611         enforce(group.length == centerArr.length,
612             "Each group's length must be equal to centers.length for "
613             ~ "grouped and stacked bar plots."
614         );
615 
616         if(whatToDo == CompoundBar.group) {
617             immutable groupWidth = width / dataArr.length;
618 
619             // Shift groupCenters over from overall centers.
620             auto groupCenters = centerArr.dup;
621             immutable offset = (groupIndex + 0.5) * groupWidth;
622             groupCenters[] += offset - width / 2;
623 
624             if(lowerErrors.length && upperErrors.length) {
625                 plot = BarPlot(NoCopy(groupCenters), NoCopy(group), groupWidth,
626                     NoCopy(lerrArr[groupIndex]), NoCopy(uerrArr[groupIndex])
627                 );
628             } else {
629                 plot = BarPlot(NoCopy(groupCenters), NoCopy(group), groupWidth);
630             }
631         } else if(whatToDo == CompoundBar.stack) {
632             if(groupIndex == 0) {
633                 plot = BarPlot(NoCopy(centerArr), NoCopy(group), width);
634             } else {
635                 plot = ret[$ - 1].stack(NoCopy(group));
636             }
637         } else {
638             assert(0);
639         }
640 
641         plot.barColor(colors[groupIndex]);
642 
643         if(legendText.length) {
644             plot.legendText(legendText[groupIndex]);
645         }
646 
647         ret ~= plot;
648     }
649 
650     return ret;
651 }
652 
653 /**Determine behavior for elements outside of a fixed-border histogram's bounds.
654  */
655 enum OutOfBounds {
656      /** Throw throws an exception.*/
657     throwException,
658 
659     /**Ignore simply skips the number.*/
660     ignore,
661     
662     // For backwards compatibility, will eventually be removed.
663     Throw = throwException,
664     Ignore=ignore
665 }
666 
667 /**Controls whether a histogram plots counts or probability.*/
668 enum HistType {
669     /// The Y-axis should be counts.
670     counts,
671 
672     /// The Y-axis should be probabilities.
673     probability,
674     
675     // For backwards compatibility, will eventually be removed.
676     Probability = probability,
677     Counts = counts
678 }
679 
680 /**A class for plotting regular (equal-width) histograms.*/
681 class Histogram : Plot {
682 
683     private double binWidth;
684 
685     private uint[] binCounts;
686     private uint nElem;
687 
688     private HistType countsOrProbs;
689 
690     private this() {
691         _barColor = getColor(0, 0, 255);
692     }
693 
694     private bool isCumulative = false;
695 
696     protected override void drawPlot(
697         Figure form,
698         double leftMargin,
699         double topMargin,
700         double plotWidth,
701         double plotHeight
702     ) {
703         uint[] binCounts = this.binCounts;
704         if(isCumulative) {
705             binCounts = binCounts.dup;
706             foreach(i; 1..binCounts.length) {
707                 binCounts[i] += binCounts[i - 1];
708             }
709         }
710 
711         immutable maxCount = reduce!max(0U, binCounts);
712         immutable multiplier = plotHeight / cast(double) maxCount;
713 
714         immutable binWidth = cast(double) plotWidth / nBin;
715         immutable bottom = plotHeight + topMargin;
716 
717         auto blackPen = form.getPen(getColor(0, 0, 0), 1);
718         scope(exit) doneWith(blackPen);
719 
720         auto brush = form.getBrush(_barColor);
721         scope(exit) doneWith(brush);
722 
723         double horizPos = leftMargin;
724         double lastPosPixels = leftMargin;
725         foreach(i, count; binCounts) {
726             // Most of the complexity of this loop body is for making the bin
727             // boundaries accurate in the context of having to round to
728             // pixels.
729             immutable barHeight = multiplier * count;
730             immutable horizPixels = min(roundTo!int(horizPos), lastPosPixels);
731             immutable stopAt = horizPos + binWidth;
732             immutable thisBinWidth = max(1.0, stopAt - horizPixels);
733 
734             form.fillClippedRectangle(brush, lastPosPixels,
735                 bottom - barHeight, thisBinWidth, barHeight);
736             form.drawClippedRectangle(blackPen, lastPosPixels,
737                 bottom - barHeight, thisBinWidth, barHeight);
738             horizPos += binWidth;
739             lastPosPixels = horizPixels + thisBinWidth;
740         }
741     }
742 
743     protected override void drawLegendSymbol(FigureBase fig, PlotRect where) {
744         drawFillLegend(_barColor, fig, where);
745     }
746 
747     private void fixBounds() {
748         if(isCumulative) {
749             if(countsOrProbs == HistType.Probability) {
750                 upperLim = 1;
751             } else {
752                 upperLim = nElem;
753             }
754         } else{
755             if(countsOrProbs == HistType.Probability) {
756                 upperLim = reduce!max(0U, binCounts) / cast(double) nElem;
757             } else {
758                 upperLim = reduce!max(0U, binCounts);
759             }
760         }
761     }
762 
763     private OutOfBounds outOfBoundsBehavior = OutOfBounds.Throw;
764 
765     private Color _barColor;
766 
767     /**Controls the color of the bar.  Defaults to blue.*/
768     final Color barColor()() {
769         return _barColor;
770     }
771 
772     /// Setter
773     final This barColor(this This)(Color newColor) {
774         _barColor = newColor;
775         return cast(This) this;
776     }
777 
778 
779     /// The number of bins this histogram contains.
780     final uint nBin() const pure nothrow  {
781         return cast(uint) binCounts.length;
782     }
783 
784     /**Factory method to instantiate this class.  nums must be a forward range
785      * with elements implicitly convertible to double.  nBin specifies how many
786      * bins the histogram should contain.
787      */
788     static Histogram opCall(R)(R nums, uint nBin)
789     if(isForwardRange!R && is(ElementType!R : double)) {
790         double leftLim = double.infinity, rightLim = -double.infinity;
791         foreach(num; nums.save) {
792             leftLim = min(leftLim, num);
793             rightLim = max(rightLim, num);
794         }
795 
796         return Histogram(nums, nBin, leftLim, rightLim);
797     }
798 
799     /**Factory method to instantiate this class with predetermined limits.
800      * This allows nums to be an input range instead of a forward range, since
801      * no pass is necessary to compute the limits.
802      *
803      * This function both obeys and permanently sets whatever bounds behavior
804      * is specified (throwing or ignoring on out of bounds numbers).  The
805      * default behavior is to throw.  Rationale:  Errors should only pass
806      * silently if explicitly silenced.
807      */
808     static Histogram opCall(R)(
809         R nums,
810         uint nBin,
811         double leftLim,
812         double rightLim,
813         OutOfBounds outOfBoundsBehavior = OutOfBounds.throwException
814     ) if(isInputRange!R && is(ElementType!R : double)) {
815         auto ret = Histogram(nBin, leftLim, rightLim, outOfBoundsBehavior);
816 
817         foreach(num; nums) {
818             ret.put(num);
819         }
820 
821         return ret;
822     }
823 
824     /**Create an empty histogram with pre-specified bounds, which will be
825      * filled with data using the put method.
826      *
827      * Note:  The only reason this is a template is because of bugs in
828      * overloading non-templated functions agsinst templated functions.
829      */
830     static Histogram opCall(I)(
831         I nBin,
832         double leftLim,
833         double rightLim,
834         OutOfBounds outOfBoundsBehavior = OutOfBounds.Throw
835     ) if(isIntegral!I) {
836         auto ret = new Histogram;
837         ret.outOfBoundsBehavior = outOfBoundsBehavior;
838 
839         enforce(rightLim > leftLim,
840             "Cannot create a histogram w/ upper lim <= lower lim.");
841         enforce(nBin > 1, "Cannot create a histogram w/ <2 bins.");
842 
843         immutable binWidth = (rightLim - leftLim) / nBin;
844         ret.binWidth = binWidth;
845 
846         ret.leftLim = leftLim;
847         ret.rightLim = rightLim;
848         ret.binCounts.length = to!uint(nBin);
849         ret.lowerLim = 0;
850         ret.upperLim = 0;
851 
852         return ret;
853     }
854 
855     /**Add a number to the histogram on the fly.*/
856     This put(this This)(double num) {
857         if(outOfBoundsBehavior == OutOfBounds.Throw) {
858             enforce(num >= leftLim && num <= rightLim, text(
859                 "Number out of bounds for histogram.  Got:  ", num,
860                 ", expected between ", leftLim, " and ", rightLim, "."));
861         } else {
862             if(!(num >= leftLim && num <= rightLim)) {
863                 return cast(This) this;
864             }
865         }
866 
867         uint bin;
868         bin = to!uint((num - leftLim) / binWidth);
869         if(bin == nBin) {  // Edge case.
870             bin--;
871         }
872 
873         binCounts[bin]++;
874         nElem++;
875 
876         if(countsOrProbs == HistType.Counts) {
877             if(isCumulative) {
878                 upperLim = nElem;
879             } else if(binCounts[bin] > upperLim) {
880                 upperLim = binCounts[bin];
881             }
882         } else if(!isCumulative) {
883             immutable binProb = binCounts[bin] / cast(double) nElem;
884             if(binProb > upperLim) {
885                 upperLim = binProb;
886             }
887         }
888 
889         return cast(This) this;
890     }
891 
892     /**Add the contents of another Histogram to this one.  The boundaries and
893      * numbers of bins must be the same.  This histogram's settings are
894      * retained.
895      */
896     This put(this This)(const Histogram rhs) {
897         if(rhs is null) {
898             return cast(This) this;
899         }
900 
901         enforce(rhs.leftLim == this.leftLim && rhs.rightLim == this.rightLim,
902             "Boundaries must be the same to combine histograms.");
903         binCounts[] += rhs.binCounts[];
904         nElem += rhs.nElem;
905 
906         fixBounds();
907         return cast(This) this;
908     }
909 
910     /**Assumes the LineGraph input is a plot of a PDF that this histogram is
911      * supposed to approximate, and scales the Y axis of the LineGraph
912      * accordingly so that both appear on the same scale.
913      *
914      * If this Histogram is cumulative, assumes that the input LineGraph is
915      * a CDF instead.
916      */
917     This scaleDistributionFunction(this This)(LineGraph g) {
918         if(isCumulative) {
919             if(countsOrProbs == HistType.Counts) {
920                 g.scaleY(nElem);
921             }
922         // Don't need to do anything if this is a probability histogram.
923         } else {
924             double scaleFactor = (rightLim - leftLim) / nBin;
925             if(countsOrProbs == HistType.Counts) {
926                 scaleFactor *= nElem;
927             }
928 
929             g.scaleY(scaleFactor);
930         }
931 
932         return cast(This) this;
933     }
934 
935     /**Determine whether this object throws or ignores if it receives a number
936      * outside its bounds via put.
937      */
938     This boundsBehavior(this This)(OutOfBounds behavior)  {
939         outOfBoundsBehavior = behavior;
940         return cast(This) this;
941     }
942 
943     /**Set whether this histogram displays counts or probabilities.*/
944     This histType(this This)(HistType newType) {
945         countsOrProbs = newType;
946         fixBounds();
947         return cast(This) this;
948     }
949 
950     /**Determines whether this histogram is cumulative.*/
951     This cumulative(this This)(bool newVal) {
952         isCumulative = newVal;
953         fixBounds();
954         return cast(This) this;
955     }
956 }
957 
958 /**Creates a histogram with equal frequency binning instead of equal width
959  * binning.  The scale of a FrequencyHistogram is the probability density scale.
960  *
961  * Note that equal frequency binning doesn't work with discrete
962  * distributions where probability density may be infinite at a point.
963  * Therefore, if a probability density is calculated to be infinite, this
964  * class will throw.
965  *
966  *
967  */
968 class FrequencyHistogram : Plot {
969     private double[] binWidths;
970     private double elemsPerBin;
971 
972     private Color _barColor;
973 
974     this() {
975         _barColor = getColor(0, 0, 255);
976     }
977 
978     protected override void drawPlot(
979         Figure form,
980         double leftMargin,
981         double topMargin,
982         double plotWidth,
983         double plotHeight
984     ) {
985 
986         mixin(toPixels);
987 
988         immutable multiplier = plotHeight / (this.upperLim - this.lowerLim);
989         immutable zeroPoint = toPixelsY(0);
990         auto brush = form.getBrush(_barColor);
991         scope(exit) doneWith(brush);
992         auto pen = form.getPen(_barColor, 1);
993         scope(exit) doneWith(pen);
994 
995         auto blackPen = form.getPen(getColor(0, 0, 0));
996         scope(exit) doneWith(blackPen);
997 
998         double xStart = leftLim;
999         immutable totalWidth = rightLim - leftLim;
1000         foreach(width; binWidths) {
1001             scope(exit) xStart += width;
1002             immutable height = 1.0 / (binWidths.length * width);
1003             immutable leftPixels = toPixelsX(xStart);
1004             immutable rightPixels = toPixelsX(xStart + width);
1005             immutable widthPixels = rightPixels - leftPixels;
1006             immutable heightPixels = height * multiplier;
1007 
1008             immutable startAt = zeroPoint - heightPixels;
1009 
1010             form.fillClippedRectangle(brush, leftPixels,
1011                 startAt, widthPixels, heightPixels);
1012 
1013             form.drawClippedRectangle(pen, leftPixels,
1014                 startAt, widthPixels, heightPixels);
1015 
1016             // Don't outline rectangle because for equal-frequency
1017             // histograms this is more distracting than readable.
1018         }
1019     }
1020 
1021     override void drawLegendSymbol(FigureBase fig, PlotRect where) {
1022         drawFillLegend(_barColor, fig, where);
1023     }
1024 
1025     /**Controls the color of the bar.  Defaults to blue.*/
1026     final Color barColor()() {
1027         return _barColor;
1028     }
1029 
1030     /// Setter
1031     final This barColor(this This)(Color newColor) {
1032         _barColor = newColor;
1033         return cast(This) this;
1034     }
1035 
1036     /**Create a FrequencyHistogram.  R must be an input range with elements
1037      * implicitly convertible to double.  nBin must be > 0 and <= nums.length.
1038      *
1039      * Throws:  Exception if the density for any bin is infinite.
1040      */
1041     static FrequencyHistogram opCall(R)(R nums, uint nBin) {
1042         auto numArr = toDoubleArray(nums);
1043         enforce(numArr.length >= nBin,
1044             "Can't create an equal-frequency histogram w/ < nBin elements.");
1045 
1046         sort(numArr);
1047         auto binWidths = new double[nBin];
1048 
1049         auto ret = new typeof(return);
1050         ret.leftLim = numArr[0];
1051         ret.rightLim = numArr[$ - 1];
1052         ret.lowerLim = 0;
1053 
1054         // Use linear interpolation to deal w/ non-integer elements/bin.
1055         immutable elemsPerBin = numArr.length / cast(double) nBin;
1056         double lastBinStop = numArr[0];
1057         foreach(i; 0..nBin) {
1058             immutable floatIndex = elemsPerBin * (i + 1);
1059             immutable indexFract = floatIndex - floor(floatIndex);
1060             size_t lowerIndex = to!size_t(floatIndex);
1061             size_t upperIndex = to!size_t(ceil(floatIndex));
1062 
1063             if(upperIndex >= numArr.length) {
1064                 upperIndex = numArr.length - 1;
1065             }
1066             if(lowerIndex >= numArr.length) {
1067                 lowerIndex = numArr.length - 1;
1068             }
1069 
1070             immutable diff = numArr[upperIndex] - numArr[lowerIndex];
1071             immutable curBinStop = numArr[lowerIndex] + diff * indexFract;
1072 
1073             binWidths[i] = curBinStop - lastBinStop;
1074             enforce(binWidths[i] > 0,
1075                 "Can't create an equal-frequency histogram when some bin " ~
1076                 "widths are 0."
1077             );
1078             lastBinStop = curBinStop;
1079         }
1080 
1081         ret.binWidths = binWidths;
1082         ret.elemsPerBin = elemsPerBin;
1083         immutable totalWidth = ret.rightLim - ret.leftLim;
1084         ret.upperLim = 1.0 / (binWidths.length * reduce!min(binWidths));
1085         return ret;
1086     }
1087 
1088     override Figure toLabeledFigure()  {
1089         auto ret = toFigure;
1090         ret.yLabel = "Probability Density";
1091         return ret;
1092     }
1093 }
1094 
1095 /**Creates a histogram in which every unique value gets its own bin.
1096  * Useful for histograms where the distribution is known to be discrete over
1097  * a small set of values.
1098  *
1099  * Hint:  Since this class inherits from BarPlot, BarPlot.centers will provide
1100  * a list of the unique values found.  This can be used to label the X
1101  * axis.
1102  */
1103 class UniqueHistogram : BarPlot {
1104 
1105     /**The total count of this histogram.*/
1106     immutable uint nElem;
1107 
1108     private HistType countsOrProbs = HistType.counts;
1109 
1110     private this(double[] x, double[] y, double width, uint nElem) {
1111         this.nElem = nElem;
1112         super(x, y, width);
1113     }
1114 
1115     /**Create a UniqueHistogram.  R must be an input range with elements
1116      * implicitly convertible to double.  The width of each bin will be
1117      * widthFactor times the minimum distance between unique values. widthFactor
1118      * must be > 0 and <= 1.
1119      */
1120     static UniqueHistogram opCall(R)(R nums, double widthFactor = 0.8)
1121     if(isInputRange!R && isNumeric!(ElementType!R)) {
1122         enforce(widthFactor > 0 && widthFactor <= 1,
1123             "widthFactor must be > 0 and <= 1.");
1124 
1125         alias ElementType!R E;
1126 
1127         uint nElem;
1128         uint[double] counts;
1129         foreach(num; nums) {
1130             auto ptr = num in counts;
1131             if(ptr is null) {
1132                 counts[num] = 1;
1133             } else {
1134                 (*ptr)++;
1135             }
1136             nElem++;
1137         }
1138 
1139         auto possibleValues = counts.keys;
1140         sort(possibleValues);
1141         double minDiff = double.infinity;
1142         foreach(i; 1..possibleValues.length) {
1143             minDiff = min(minDiff, possibleValues[i] - possibleValues[i - 1]);
1144         }
1145 
1146         auto heights = new double[possibleValues.length];
1147         foreach(i, val; possibleValues) {
1148             heights[i] = counts[val];
1149         }
1150 
1151         return new typeof(this)
1152             (possibleValues, heights, minDiff * widthFactor, nElem);
1153     }
1154 
1155     /**Set whether this histogram displays counts or probabilities.*/
1156     void histType(HistType newType)  {
1157         if(newType == countsOrProbs) {
1158             return;
1159         } else if(newType == HistType.Counts) {
1160             scaleHeights(nElem);
1161         } else if(newType == HistType.Probability) {
1162             scaleHeights(1.0 / nElem);
1163         } else {
1164             assert(0);
1165         }
1166 
1167         countsOrProbs = newType;
1168     }  
1169 }
1170 
1171 /**Class for drawing a heat map.*/
1172 class HeatMap : Plot {
1173     private double[][] values;
1174     private double minVal;
1175     private double maxVal;
1176     private uint _nRows;
1177     private uint _nCols;
1178     private Color[] _colors;
1179 
1180     enum string deprecatedMsg = "hotColor() and coldColor() are schedules " ~
1181         "for deprecation.  Use colors() instead.";
1182 
1183     private this() {
1184         // Set default colors.
1185         _colors = [
1186             getColor(255, 255, 255),
1187             getColor(0, 0, 0)
1188         ];
1189     }
1190 
1191     private Color getCellColor(double val)
1192     in {
1193         assert(_colors.length > 1);
1194         assert(val >= minVal);
1195         assert(val <= maxVal);
1196     } body {
1197         immutable diff = maxVal - minVal;
1198         immutable stride = diff / (_colors.length - 1.0);
1199         val -= minVal;
1200         val /= stride;
1201 
1202         immutable index1 = to!size_t(floor(val));
1203         immutable index2 = min(_colors.length - 1, to!size_t(ceil(val)));
1204         auto hotColor = _colors[index2];
1205         auto coldColor = _colors[index1];
1206 
1207         immutable fract = val - floor(val);
1208         immutable compl = 1.0 - fract;
1209 
1210         // Bug 4445:  roundTo!ubyte(255.0) throws.
1211         immutable red = cast(ubyte) roundTo!uint(
1212             coldColor.r * compl + hotColor.r * fract);
1213         immutable green = cast(ubyte) roundTo!uint(
1214             coldColor.g * compl + hotColor.g * fract);
1215         immutable blue = cast(ubyte) roundTo!uint(
1216             coldColor.b * compl + hotColor.b * fract);
1217 
1218         return getColor(red, green, blue);
1219     }
1220 
1221     protected void heatMapDefaultBounds() {
1222         enforceRectangular();
1223 
1224         _nRows = cast(uint) values.length;
1225         if(values.length > 0) {
1226             _nCols = cast(uint) values[0].length;
1227         }
1228 
1229         leftLim = 0.5;
1230         rightLim = nCols + 0.5;
1231         lowerLim = 0.5;
1232         upperLim = nRows + 0.5;
1233     }
1234 
1235     protected void setMinMax() {
1236         minVal = double.infinity;
1237         maxVal = -double.infinity;
1238 
1239         foreach(row; values) foreach(val; row) {
1240             minVal = min(minVal, val);
1241             maxVal = max(maxVal, val);
1242         }
1243     }
1244 
1245     protected override void drawLegendSymbol(FigureBase fig, PlotRect where) {
1246         enforce(0, "Heat maps don't have legends.");
1247     }
1248 
1249     protected void enforceRectangular() {
1250         foreach(row; values) {
1251             enforce(row.length == values[0].length,
1252                 "HeatMap matrices must be rectangular.");
1253         }
1254     }
1255 
1256     protected override void drawPlot(
1257         Figure form,
1258         double leftMargin,
1259         double topMargin,
1260         double plotWidth,
1261         double plotHeight
1262     ) {
1263         immutable cellWidth = (rightLim - leftLim) / nCols;
1264         immutable cellHeight = (upperLim - lowerLim) / nRows;
1265 
1266         if(nRows < 1 || nCols < 1) {
1267             return;
1268         }
1269 
1270         mixin(toPixels);
1271         double lastRowStop = toPixelsY(upperLim);
1272         foreach(row; 0..nRows) {
1273             immutable rowStop = toPixelsY(upperLim - cellHeight * (row + 1));
1274             scope(exit) lastRowStop = rowStop;
1275 
1276             double lastColStop = toPixelsX(leftLim);
1277             foreach(col; 0..nCols) {
1278                 immutable colStop = toPixelsX(cellWidth * (col + 1) + leftLim);
1279                 scope(exit) lastColStop = colStop;
1280 
1281                 auto color = getCellColor(values[row][col]);
1282                 auto rect = PlotRect(lastColStop, lastRowStop,
1283                     colStop - lastColStop, rowStop - lastRowStop);
1284 
1285                 auto brush = form.getBrush(color);
1286                 scope(exit) doneWith(brush);
1287                 auto pen = form.getPen(color, 1);
1288                 scope(exit) doneWith(pen);
1289 
1290                 form.fillClippedRectangle(brush, rect);
1291                 form.drawClippedRectangle(pen, rect);
1292             }
1293         }
1294     }
1295 
1296     private Color _coldColor;
1297     private Color _hotColor;
1298 
1299     override bool hasLegend() {
1300         return false;
1301     }
1302 
1303     final Color coldColor()() {
1304         pragma(msg, deprecatedMsg);
1305         return _colors[0];
1306     }
1307 
1308     final This coldColor(this This)(Color newColor) {
1309         pragma(msg, deprecatedMsg);
1310         _colors[0] = newColor;
1311         return cast(This) this;
1312     }
1313 
1314     final Color hotColor()() {
1315         pragma(msg, deprecatedMsg);
1316         return _colors[1];
1317     }
1318 
1319     final This hotColor(this This)(Color newColor) {
1320         pragma(msg, deprecatedMsg);
1321         _colors[1] = newColor;
1322         return cast(This) this;
1323     }
1324 
1325     ///
1326     final Color[] colors()() {
1327         return _colors;
1328     }
1329 
1330     /**
1331     Set the colors to be used for this HeatMap as an array from coldest
1332     (smallest value) to warmest (largest value).  newColors.length must
1333     be >= 2.
1334     */
1335     final This colors(this This)(Color[] newColors) {
1336         enforce(newColors.length >= 2,
1337             "Need at least 2 colors for a HeatMap.");
1338         this._colors = newColors;
1339         return cast(This) this;
1340     }
1341 
1342     /**Create a heat map from a matrix represented as a range of ranges.
1343      * The matrix must be rectangular.  The elements of the ranges must
1344      * be implicitly convertible to double.
1345      */
1346     static HeatMap opCall(R)(R data)
1347     if(isInputRange!R && isInputRange!(ElementType!R) &&
1348        is(ElementType!(ElementType!(R)) : double)) {
1349         auto ret = new typeof(this);
1350         foreach(row; data) {
1351             ret.values ~= toDoubleArray(row);
1352         }
1353 
1354         ret.heatMapDefaultBounds();
1355         ret.setMinMax();
1356         return ret;
1357     }
1358 
1359     /**Create a heat map w/o copying data.*/
1360     static HeatMap noCopy(double[][] mat) {
1361         auto ret = new typeof(this);
1362         ret.values = mat;
1363         ret.heatMapDefaultBounds();
1364         ret.setMinMax();
1365         return ret;
1366     }
1367 
1368     ///
1369     final uint nRows()  {
1370         return _nRows;
1371     }
1372 
1373     ///
1374     final uint nCols()  {
1375         return _nCols;
1376     }
1377 
1378     /// Throws an exception.  Heat maps aren't allowed to have legends.
1379     override This legendText(this This)(string ignored) {
1380         enforce(0, "Heat maps can't have legend text.");
1381     }
1382 }
1383 
1384 /**
1385 Convenience function that creates a hierarchically clustered heat map and,
1386 if provided, rearranges your row and column names according to the clustering.
1387 
1388 The distance and linkage aliases control the distance and linkage functions
1389 for hierarchical clustering.  See plot2kill.hierarchical.hierarchicalCluster()
1390 for details.
1391 */
1392 HeatMap hierarchicalHeatMap(alias distance = euclidean, alias linkage = mean, R)
1393 (R data, string[] rowNames = null, string[] colNames = null)
1394 if(isInputRange!R && isInputRange!(ElementType!R) &&
1395 is(ElementType!(ElementType!(R)) : double)) {
1396 
1397     auto matrix = array(map!(toDoubleArray)(data));
1398     enforce(matrix.length > 0 && matrix[0].length > 0,
1399         "Cannot produce a heat map w/ zero elements.");
1400 
1401     foreach(i; 0..matrix.length) {
1402         enforce(matrix[i].length == matrix[0].length,
1403             "data must be rectangular for hierarchicalHeatMap.");
1404     }
1405 
1406     auto rowClusters = hierarchicalCluster!(distance, linkage)
1407         (matrix, ClusterBy.rows, rowNames);
1408 
1409     auto rowPermApp = appender!(size_t[])();
1410     size_t nameIndex = 0;
1411     foreach(c; *rowClusters) {
1412         rowPermApp.put(c.index);
1413         if(rowNames.length) rowNames[nameIndex++] = c.name;
1414     }
1415     rowClusters = null;
1416 
1417     auto colClusters = hierarchicalCluster!(distance, linkage)
1418         (matrix, ClusterBy.columns, colNames);
1419 
1420     auto colPermApp = appender!(size_t[])();
1421     nameIndex = 0;
1422     foreach(c; *colClusters) {
1423         colPermApp.put(c.index);
1424         if(colNames.length) colNames[nameIndex++] = c.name;
1425     }
1426     colClusters = null;
1427 
1428     static T[] byPerm(T)(T[] input, const size_t[] perm) {
1429         assert(perm.length == input.length);
1430 
1431         auto ret = new T[input.length];
1432         foreach(i, p; perm) {
1433             ret[i] = input[p];
1434         }
1435 
1436         return ret;
1437     }
1438 
1439     assert(rowPermApp.data.length == matrix.length, text(
1440         rowPermApp.data.length, ' ', matrix.length));
1441     matrix = byPerm(matrix, rowPermApp.data);
1442 
1443     // This fixes some weirdness caused by different conventions between HeatMap
1444     // and most other plots.
1445     reverse(matrix);
1446 
1447     foreach(ref row; matrix) {
1448         row = byPerm(row, colPermApp.data);
1449     }
1450 
1451     return HeatMap.noCopy(matrix);
1452 }
1453 
1454 /**Creates a heat map representing the density of a 2-d probability
1455  * distribution.  This is useful when you want to visualize a joint
1456  * probability distribution but the sample size is so large that a
1457  * scatter plot would have an overwhelming number of points.
1458  */
1459 class HeatScatter : HeatMap {
1460     private double cellWidth;
1461     private double cellHeight;
1462 
1463     private OutOfBounds outOfBoundsBehavior = OutOfBounds.Throw;
1464 
1465     /**Create a HeatScatter.  x, y must be forward ranges with elements
1466      * implicitly convertible to double, and must have the same lengths.
1467      */
1468     static
1469     HeatScatter opCall(R1, R2)(R1 x, R2 y, uint nRows = 10, uint nCols = 10)
1470     if(isForwardRange!R1 && isForwardRange!R2 &&
1471        is(ElementType!R1 : double) && is(ElementType!R2 : double)) {
1472         double xMin = double.infinity;
1473         double xMax = -double.infinity;
1474         uint xLen;
1475         foreach(num; x.save) {
1476             xLen++;
1477             xMin = min(num, xMin);
1478             xMax = max(num, xMax);
1479         }
1480 
1481         double yMin = double.infinity;
1482         double yMax = -double.infinity;
1483         uint yLen;
1484         foreach(num; y.save) {
1485             yLen++;
1486             yMin = min(num, yMin);
1487             yMax = max(num, yMax);
1488         }
1489 
1490         enforce(xLen == yLen,
1491             "Can't make HeatScatter when x.length != y.length.");
1492         return opCall(x, y, nRows, nCols, xMin, xMax, yMin, yMax);
1493     }
1494 
1495     /**Create a HeatScatter with pre-specified bounds.  x, y must be forward
1496      * ranges with elements implicitly convertible to double, and must have the
1497      * same lengths.
1498      */
1499     static
1500     HeatScatter opCall(R1, R2)(R1 x, R2 y, uint nRows, uint nCols,
1501     double xMin, double xMax, double yMin, double yMax,
1502     OutOfBounds boundsBehavior = OutOfBounds.Throw)
1503     if(isForwardRange!R1 && isForwardRange!R2 &&
1504        is(ElementType!R1 : double) && is(ElementType!R2 : double)) {
1505 
1506         auto ret = opCall(nRows, nCols, xMin, xMax, yMin, yMax, boundsBehavior);
1507 
1508         double maxVal = 0;
1509         while(!x.empty && !y.empty) {
1510             scope(exit) {
1511                 x.popFront();
1512                 y.popFront();
1513             }
1514 
1515             ret.put(x.front(), y.front());
1516         }
1517 
1518         enforce(x.empty && y.empty,
1519             "Can't make HeatScatter when x.length != y.length.");
1520         return ret;
1521     }
1522 
1523     /**Create a blank HeatScatter to fill in using the put() method.
1524      *
1525      * Note:  This is a template only because templates can't be overloaded
1526      * against regular functions.
1527      */
1528     static
1529     HeatScatter opCall(I)(I nRows, uint nCols,
1530         double xMin, double xMax, double yMin, double yMax,
1531         OutOfBounds boundsBehavior = OutOfBounds.Throw) {
1532         enforce(nRows > 0 && nCols > 0,
1533             "Can't make a heat scatter with 0 rows or columns.");
1534 
1535         enforce(xMax > xMin, "xMax must be > xMin.");
1536         enforce(yMax > yMin, "yMax must be > yMin.");
1537 
1538         auto grid = new double[][](nRows, nCols);
1539         foreach(row; grid) foreach(ref elem; row) {
1540             elem = 0;
1541         }
1542 
1543         auto ret = new typeof(return);
1544         ret.boundsBehavior = boundsBehavior;
1545         immutable cellWidth = (xMax - xMin) / nCols;
1546         immutable cellHeight = (yMax - yMin) / nRows;
1547         ret.cellWidth = cellWidth;
1548         ret.cellHeight = cellHeight;
1549         ret.values = grid;
1550         ret._nRows = nRows;
1551         ret._nCols = nCols;
1552         ret.minVal = 0;
1553         ret.maxVal = 0;
1554         ret.leftLim = xMin;
1555         ret.rightLim = xMax;
1556         ret.lowerLim = yMin;
1557         ret.upperLim = yMax;
1558         return ret;
1559     }
1560 
1561     /**Add an element to the plot.*/
1562     This put(this This)(double x, double y) {
1563         bool inBounds() {
1564             return (x >= leftLim && x <= rightLim && y >= lowerLim &&
1565             y <= upperLim);
1566         }
1567 
1568         if(outOfBoundsBehavior == OutOfBounds.Throw) {
1569             enforce(inBounds(), "Point out of bounds in HeatScatter.");
1570         } else if(!inBounds()) {
1571             return cast(This) this;
1572         }
1573 
1574         uint xCoord = to!uint((x - leftLim) / cellWidth);
1575         if(xCoord == nCols) {
1576             xCoord--;
1577         }
1578 
1579         uint yCoord = nRows - to!uint((y - lowerLim) / cellHeight) - 1;
1580         if(yCoord == uint.max) {
1581             yCoord = 0;
1582         }
1583 
1584         values[yCoord][xCoord]++;
1585         maxVal = max(maxVal, values[yCoord][xCoord]);
1586 
1587         return cast(This) this;
1588     }
1589 
1590     /**Add another HeatScatter's data to this.  The boundaries and row and
1591      * column counts must be the same.  The settings from this HeatScatter are
1592      * preserved.
1593      */
1594     This put(this This)(const HeatScatter rhs) {
1595         if(rhs is null) {
1596             return cast(This) this;
1597         }
1598         enforce(this.leftLim == rhs.leftLim && this.rightLim == rhs.rightLim &&
1599                 this.upperLim == rhs.upperLim && this.lowerLim == rhs.lowerLim
1600                 && this._nRows == rhs._nRows && this._nCols == rhs._nCols,
1601         "Cannot combine two HeatScatters w/ different bounds or row/column #s.");
1602 
1603         foreach(row; 0.._nRows) foreach(col; 0.._nCols) {
1604             this.values[row][col] += rhs.values[row][col];
1605             this.maxVal = max(this.values[row][col], this.maxVal);
1606         }
1607 
1608         return cast(This) this;
1609     }
1610 
1611     /**Determine whether this object throws or ignores if it receives a number
1612      * outside its bounds via put.
1613      */
1614     void boundsBehavior(OutOfBounds behavior) {
1615         outOfBoundsBehavior = behavior;
1616     }
1617 }
1618 
1619 /**Class for drawing a scatter plot.*/
1620 class ScatterPlot : Plot {
1621     private double[] x;
1622     private double[] y;
1623 
1624     protected override void drawLegendSymbol(FigureBase fig, PlotRect where) {
1625         drawTextLegend(_pointSymbol, _pointColor, fig, where);
1626     }
1627 
1628     protected override void drawPlot(
1629         Figure form,
1630         double leftMargin,
1631         double topMargin,
1632         double plotWidth,
1633         double plotHeight
1634     ) {
1635         enforce(x.length == y.length);  // Should have already been checked.
1636 
1637         if(x.length < 1) {
1638             return;
1639         }
1640 
1641         mixin(toPixels);
1642 
1643         auto font = getFont(plot2kill.util.defaultFont,
1644             _pointSize + Figure.fontSizeAdjust);
1645         scope(exit) doneWith(font);
1646 
1647         auto pointDrawer = ScatterCharDrawer
1648             (_pointSymbol, font, _pointColor, form);
1649 
1650         pointDrawer.initialize;
1651         scope(exit) pointDrawer.restore();
1652 
1653         foreach(i; 0..x.length) {
1654             immutable curX = toPixelsX(x[i]);
1655             immutable curY = toPixelsY(y[i]);
1656             pointDrawer.draw(PlotPoint(curX, curY));
1657         }
1658     }
1659 
1660     private Color _pointColor;
1661     private char _pointSymbol = 'x';
1662     private int _pointSize = 10;
1663 
1664     /**The color of each point on the plot.*/
1665     final Color pointColor()() {
1666         return _pointColor;
1667     }
1668 
1669     /// Setter.
1670     final This pointColor(this This)(Color newColor) {
1671         _pointColor = newColor;
1672         return cast(This) this;
1673     }
1674 
1675     /**The symbol that should be used on the plot.  x and o work pretty well.
1676      * The default is x.
1677      */
1678     final char pointSymbol()() {
1679         return _pointSymbol;
1680     }
1681 
1682     /// Setter
1683     final This pointSymbol(this This)(char newSymbol) {
1684         _pointSymbol = newSymbol;
1685         return cast(This) this;
1686     }
1687 
1688     /// The size of a point.  (Default 10).
1689     final int pointSize()() {
1690         return _pointSize;
1691     }
1692 
1693     /// Setter
1694     final This pointSize(this This)(int newSize) {
1695         this._pointSize = newSize;
1696         return cast(This) this;
1697     }
1698 
1699 
1700     this() {
1701         _pointColor = getColor(0, 0, 0);
1702     }
1703 
1704 
1705     /**Factory method for creating a ScatterPlot.  x and y must both be
1706      * input ranges of the same length, with elements implicitly convertible
1707      * to doubles.  Note that they are copied inside the factory, so changes
1708      * to the original ranges after calling this factory will not affect the
1709      * plot.
1710      */
1711     static ScatterPlot opCall(R1, R2)(R1 x, R2 y)
1712     if(isInputRange!R1 && is(ElementType!R1 : double) &&
1713        isInputRange!R2 && is(ElementType!R2 : double)) {
1714 
1715         auto ret = new ScatterPlot;
1716         constructXYGraph(x, y, ret);
1717 
1718         fixXYGraphBounds(ret);
1719         addFudgeFactors(ret);
1720         return ret;
1721     }
1722 
1723     /**Convenience factory that produces a ScatterPlot with a default X
1724      * axis numbered 1, 2, ..., N where N is the number of points.  Mostly
1725      * useful for quick and dirty plots.
1726      */
1727     static ScatterPlot opCall(R)(R y)
1728     if(isInputRange!R && is(ElementType!R : double)) {
1729         auto ret = new ScatterPlot;
1730         ret.y = toDoubleArray(y);
1731         ret.x = new double[ret.y.length];
1732 
1733         foreach(i, ref elem; ret.x) {
1734             elem = i + 1;
1735         }
1736 
1737         fixXYGraphBounds(ret);
1738         addFudgeFactors(ret);
1739         return ret;
1740     }
1741 }
1742 
1743 /**Class for drawing a line graph, i.e. a set of points connected by lines.*/
1744 class LineGraph : Plot {
1745     private double[] x;
1746     private double[] y;
1747     private double[] lowerErrors;
1748     private double[] upperErrors;
1749 
1750 
1751     private enum defaultErrorWidth = 0.05;
1752     private double _errorWidth = defaultErrorWidth;
1753 
1754     private char _pointSymbol = ' ';
1755     private Color _pointColor;
1756     private int _pointSize = 20;
1757 
1758     protected void fixBounds() {
1759         this.leftLim = reduce!min(double.infinity, this.x);
1760         this.rightLim = reduce!max(-double.infinity, this.x);
1761 
1762         this.upperLim = -double.infinity;
1763         this.lowerLim = double.infinity;
1764         foreach(i, yCoord; this.y) {
1765             if(this.lowerErrors.length) {
1766                 this.lowerLim = min(this.lowerLim, yCoord - this.lowerErrors[i]);
1767             } else {
1768                 this.lowerLim = min(this.lowerLim, yCoord);
1769             }
1770             if(this.upperErrors.length) {
1771                 this.upperLim = max(this.upperLim, yCoord + this.upperErrors[i]);
1772             } else {
1773                 this.upperLim = max(this.upperLim, yCoord);
1774             }
1775         }
1776 
1777         if(this.lowerErrors.length || this.upperErrors.length) {
1778             // Dont' cut off error bars.
1779             immutable yPad = (this.upperLim - this.lowerLim) * 0.01;
1780             immutable xPad = (this.rightLim - this.leftLim) * _errorWidth;
1781             this.leftLim -= xPad;
1782             this.rightLim += xPad;
1783             this.upperLim += yPad;
1784             this.lowerLim -= yPad;
1785         }
1786 
1787         if(_pointSymbol != ' ') {
1788  	        addFudgeFactors(this);
1789         }
1790     }
1791 
1792     protected override void drawLegendSymbol(FigureBase fig, PlotRect where) {
1793         auto pen = fig.getPen(_lineColor, _lineWidth);
1794         scope(exit) doneWith(pen);
1795         drawLineLegend(pen, fig, where);
1796 
1797         if(_pointSymbol != ' ') {
1798             drawTextLegend(_pointSymbol, _pointColor, fig, where);
1799         }
1800     }
1801 
1802     protected override void drawPlot(
1803         Figure form,
1804         double leftMargin,
1805         double topMargin,
1806         double plotWidth,
1807         double plotHeight
1808     ) {
1809         enforce(x.length == y.length);  // Should have already been checked.
1810 
1811         if(x.length < 2) {
1812             return;
1813         }
1814 
1815         mixin(toPixels);
1816         mixin(drawErrorMixin);
1817 
1818         immutable absErrorWidth = _errorWidth * (rightLim - leftLim);
1819         auto errorPen = form.getPen(getColor(0, 0, 0));
1820         scope(exit) doneWith(errorPen);
1821 
1822         double lastX = toPixelsX(x[0]);
1823         double lastY = toPixelsY(y[0]);
1824 
1825         void doErrors(size_t index) {
1826             if(lowerErrors.length) {
1827                 drawErrorBar(
1828                     errorPen, x[index], y[index],
1829                         y[index] - lowerErrors[index], absErrorWidth
1830                 );
1831             }
1832             if(upperErrors.length) {
1833                 drawErrorBar(
1834                     errorPen, x[index], y[index],
1835                         y[index] + upperErrors[index], absErrorWidth
1836                 );
1837             }
1838         }
1839 
1840         auto pen = form.getPen(_lineColor, _lineWidth);
1841         scope(exit) doneWith(pen);
1842 
1843         foreach(i; 1..x.length) {
1844             immutable curX = toPixelsX(x[i]);
1845             immutable curY = toPixelsY(y[i]);
1846 
1847             form.drawClippedLine(pen, PlotPoint(lastX, lastY), PlotPoint(curX, curY));
1848             lastX = curX;
1849             lastY = curY;
1850         }
1851 
1852         foreach(i; 0..x.length) {
1853             doErrors(i);
1854         }
1855 
1856         if(_pointSymbol == ' ') return;
1857 
1858         auto font = getFont(plot2kill.util.defaultFont,
1859             _pointSize + Figure.fontSizeAdjust);
1860         scope(exit) doneWith(font);
1861 
1862         auto pointDrawer = ScatterCharDrawer
1863             (_pointSymbol, font, _pointColor, form);
1864 
1865         pointDrawer.initialize;
1866         scope(exit) pointDrawer.restore();
1867 
1868         foreach(i; 0..x.length) {
1869             immutable curX = toPixelsX(x[i]);
1870             immutable curY = toPixelsY(y[i]);
1871             pointDrawer.draw(PlotPoint(curX, curY));
1872         }
1873     }
1874 
1875     private Color _lineColor;
1876     private uint _lineWidth = 1;
1877 
1878     /**The color of the line.  The default is black.*/
1879     final Color lineColor()() {
1880         return _lineColor;
1881     }
1882 
1883     /// Setter
1884     final This lineColor(this This)(Color newColor) {
1885         _lineColor = newColor;
1886         return cast(This) this;
1887     }
1888 
1889     /**The width of the line.  The default is 1.*/
1890     final uint lineWidth()() {
1891         return _lineWidth;
1892     }
1893 
1894     /// Setter
1895     final This lineWidth(this This)(uint newWidth) {
1896         _lineWidth = newWidth;
1897         return cast(This) this;
1898     }
1899 
1900     /**Error bar width, relative to the total width of the plot.  Must be
1901      * between 0 and 1.  If it's out of bounds, it will be set to the default
1902      * of 0.05.  If no error bars are to be drawn, this option is ignored.
1903      */
1904     final double errorWidth()() {
1905         return _errorWidth;
1906     }
1907 
1908     /// Setter
1909     final This errorWidth(this This)(double newWidth) {
1910         if(!(newWidth >= 0 && newWidth <= 1)) {
1911             _errorWidth = defaultErrorWidth;
1912         } else {
1913             _errorWidth = newWidth;
1914         }
1915 
1916         fixBounds();
1917         return cast(This) this;
1918     }
1919 
1920     /**
1921      The symbol that should be used to denote a data point.  Setting this
1922      to ' ' (a space) turns off point symbols and plots the line only.
1923      By default the point symbol is a space, so no point symbol is plotted..
1924      */
1925     final char pointSymbol()() {
1926         return _pointSymbol;
1927     }
1928 
1929     /// Setter
1930     final This pointSymbol(this This)(char newSymbol) {
1931         _pointSymbol = newSymbol;
1932         fixBounds();
1933         return cast(This) this;
1934     }
1935 
1936     /**The color of each point on the plot.*/
1937     final Color pointColor()() {
1938         return _pointColor;
1939     }
1940 
1941     /// Setter.
1942     final This pointColor(this This)(Color newColor) {
1943         _pointColor = newColor;
1944         return cast(This) this;
1945     }
1946 
1947     /// The size of a point.  (Default 20).
1948     final int pointSize()() {
1949         return _pointSize;
1950     }
1951 
1952     /// Setter
1953     final This pointSize(this This)(int newSize) {
1954         this._pointSize = newSize;
1955         fixBounds();
1956         return cast(This) this;
1957     }
1958 
1959     private this() {
1960         _lineColor = getColor(0, 0, 0);
1961         _pointColor = getColor(0, 0, 0);
1962     }
1963 
1964     /**Factory method for creating a LineGraph.  x and y must both be
1965      * input ranges of the same length, with elements implicitly convertible
1966      * to doubles.  Note that they are copied inside the factory, so changes
1967      * to the original ranges after calling this factory will not affect the
1968      * plot.
1969      */
1970     static LineGraph opCall(R1, R2)(R1 x, R2 y)
1971     if(isInputRange!R1 && is(ElementType!R1 : double) &&
1972        isInputRange!R2 && is(ElementType!R2 : double)) {
1973 
1974         auto ret = new LineGraph;
1975         constructXYGraph(x, y, ret);
1976         ret.fixBounds();
1977         return ret;
1978     }
1979 
1980     /**Convenience factory method that produces a LineGraph with a default
1981      * X axis numbered 1, 2, ..., N, where N is the number of data points,
1982      * and no error bars.  This is mostly useful for quick and dirty plots.
1983      */
1984     static LineGraph opCall(R)(R y)
1985     if(isInputRange!R && is(ElementType!R : double)) {
1986         auto ret = new LineGraph;
1987         ret.y = toDoubleArray(y);
1988         ret.x = new double[ret.y.length];
1989 
1990         foreach(i, ref elem; ret.x) {
1991             elem = i + 1;
1992         }
1993         ret.fixBounds();
1994         return ret;
1995     }
1996 
1997     /**Create a LineGraph with error bars.  lowerErrors and upperErrors
1998      * must be input ranges with elements implicitly convertible to double for
1999      * error bars to be shown.  Any other value, such as null or 0, will result
2000      * in no error bars being shown.  Therefore, to only show, for example,
2001      * upper erros, simply pass in null or 0 for the lower errors.
2002      *
2003      * To draw symmetric error bars, simply pass in the same range for
2004      * lowerErrors and upperErrors.  However, note that if you do this,
2005      * the range will need to be a forward range, not an input range.
2006      */
2007     static LineGraph opCall(R1, R2, R3, R4)
2008     (R1 x, R2 y, R3 lowerErrors, R4 upperErrors) {
2009         auto ret = new typeof(return);
2010         ret.x = toDoubleArray(x);
2011         ret.y = toDoubleArray(y);
2012 
2013         enforce(ret.x.length == ret.y.length,
2014             "x, y must have same length for line/scatter graph.");
2015 
2016         static if(isForwardRange!R3) {
2017             ret.lowerErrors = toDoubleArray(lowerErrors.save);
2018         } else static if(isInputRange!R3) {
2019             ret.lowerErrors = toDoubleArray(lowerErrors);
2020         }
2021 
2022         static if(isForwardRange!R4) {
2023             ret.upperErrors = toDoubleArray(upperErrors.save);
2024         } else static if(isInputRange!R4) {
2025             ret.upperErrors = toDoubleArray(upperErrors);
2026         }
2027 
2028         enforce(ret.upperErrors.length == 0 || ret.upperErrors.length ==
2029             ret.x.length, "Length of upperErrors must equal number of points.");
2030         enforce(ret.lowerErrors.length == 0 || ret.lowerErrors.length ==
2031             ret.x.length, "Length of lowerErrors must equal number of points.");
2032 
2033         if(ret.lowerErrors.length == 0 && ret.upperErrors.length == 0) {
2034             sort!"a[0] < b[0]"(zip(ret.x, ret.y));
2035         } else if(ret.lowerErrors.length == 0) {
2036             sort!"a[0] < b[0]"(zip(ret.x, ret.y, ret.upperErrors));
2037         } else if(ret.upperErrors.length == 0) {
2038             sort!"a[0] < b[0]"(zip(ret.x, ret.y, ret.lowerErrors));
2039         } else {
2040             sort!"a[0] < b[0]"(
2041                 zip(ret.x, ret.y, ret.lowerErrors, ret.upperErrors));
2042         }
2043 
2044         ret.fixBounds();
2045         return ret;
2046     }
2047 
2048     /**Scale this object by a factor of scaleFactor in the X direction.
2049      * This is useful for getting it onto the same scale as another plot.
2050      */
2051     void scaleX(double scaleFactor) {
2052         enforce(isFinite(scaleFactor), "Can't scale by infinity or NaN.");
2053         x[] *= scaleFactor;
2054         fixBounds();
2055     }
2056 
2057     /**Scale this object by a factor of scaleFactor in the Y direction.
2058      * This is useful for getting it onto the same scale as another plot.*/
2059     void scaleY(double scaleFactor) {
2060         enforce(isFinite(scaleFactor), "Can't scale by infinity or NaN.");
2061         y[] *= scaleFactor;
2062         fixBounds();
2063     }
2064 
2065     /**Shift this graph by shiftBy units in the X direction.
2066      * This is useful for getting it onto the same scale as another plot.
2067      */
2068     void shiftX(double shiftBy) {
2069         enforce(isFinite(shiftBy), "Can't shift by infinity or NaN.");
2070         x[] += shiftBy;
2071         fixBounds();
2072     }
2073 
2074     /**Shift this graph by shiftBy units in the Y direction.
2075      * This is useful for getting it onto the same scale as another plot.
2076      */
2077     void shiftY(double shiftBy) {
2078         enforce(isFinite(shiftBy), "Can't shift by infinity or NaN.");
2079         y[] += shiftBy;
2080         fixBounds();
2081     }
2082 }
2083 
2084 /**
2085 This class creates a best-fit line, and is useful for adding to scatter
2086 plot figures.  The default line color is red instead of black and the default
2087 width is two instead of one to make the best-fit line stand out against the
2088 default scatter plot settings.
2089 */
2090 class LinearFit : LineGraph {
2091     private double _alpha, _beta, _cor;
2092     
2093     final @property const pure nothrow @safe {
2094         /** 
2095         The intercept term of the fit line.
2096         */
2097         double alpha() { return _alpha; }
2098         
2099         /**
2100         The slope of the fit line.
2101         */
2102         double beta() { return _beta; }
2103         
2104         /**
2105         The correlation between the predictions made by the regression and
2106         the actual values.
2107         */
2108         double cor() { return _cor; }
2109         
2110         /**
2111         The R^2 value of the fit line.
2112         */
2113         double r2() { return _cor ^^ 2; }
2114     }
2115 
2116     private this() {
2117         super();
2118         _lineColor = getColor(255, 0, 0);
2119         _lineWidth = 2;
2120     }
2121 
2122     /**
2123     Compute the regression coefficients and create a best-fit line, where
2124     x is the independent variable and y is the dependent variable.
2125     lowerLim and upperLim control how far the best-fit line is extended in
2126     each direction along the X-axis.  By default the line is extended between 
2127     the minimum and maximum values of x.
2128     */
2129     static LinearFit opCall(R1, R2)
2130     (R1 x, R2 y, double lower = double.nan, double upper = double.nan)
2131     if(isInputRange!R1 && is(ElementType!R1 : double) &&
2132        isInputRange!R2 && is(ElementType!R2 : double)
2133     ) {
2134         
2135         static if(isForwardRange!R1) {
2136             alias x xx;
2137         } else {
2138             auto xx = toDoubleArray(x);
2139         }
2140         
2141         static if(isForwardRange!R2) {
2142             alias y yy;
2143         } else {
2144             auto yy = toDoubleArray(y);
2145         }
2146         
2147         import std.numeric;
2148         
2149         // This is somewhat inefficient, etc. because all the info has
2150         // to fit neatly on a scatter plot, so we can't be dealing with
2151         // millions of points, and because I don't want to add a dependency
2152         // to do it more properly.
2153         immutable xLen = walkLength(xx);
2154         immutable yLen = walkLength(yy);
2155         enforce(xLen == yLen, "x, y must be same length for LinearFit.");
2156         enforce(xLen > 1, "Need at least two elements for LinearFit.");
2157         
2158         immutable xMean = reduce!"a + b"(0.0, xx.save) / xLen;       
2159         immutable yMean = reduce!"a + b"(0.0, yy.save) / yLen;
2160     
2161         double xDotX = 0, xDotY = 0, yDotY = 0;
2162         foreach(xElem, yElem; lockstep(xx.save, yy.save)) {
2163             immutable xCenter = xElem - xMean;
2164             immutable yCenter = yElem - yMean;
2165             xDotX += xCenter ^^ 2;
2166             yDotY += yCenter ^^ 2;
2167             xDotY += xCenter * yCenter;
2168         }
2169         
2170         auto ret = new LinearFit();
2171         ret._beta = xDotY / xDotX;
2172         ret._alpha = yMean - ret._beta * xMean;
2173         
2174         ret._cor = xDotY / sqrt(xDotX) / sqrt(yDotY);
2175         
2176         if(isNaN(lower) || isNaN(upper)) {
2177             auto minMaxX = reduce!(min, max)(xx.save);
2178             lower = minMaxX[0];
2179             upper = minMaxX[1];
2180         }
2181         
2182         ret.x = [lower, upper];
2183         ret.y.length = 2;
2184         ret.y[] = ret.x[] * ret._beta + ret.alpha;
2185         ret.fixBounds();
2186         
2187         return ret;
2188     }
2189 }
2190 
2191 unittest {
2192     auto x = [8, 6, 7, 5, 3, 0, 9];    
2193     auto y = [3, 1, 4, 1, 5, 9, 2];
2194     
2195     // Make sure regression values are right.
2196     auto fit = LinearFit(x, y);
2197     assert(approxEqual(fit.cor, -0.7567996));
2198     assert(approxEqual(fit.beta, -0.6881));
2199     assert(approxEqual(fit.alpha, 7.3069));
2200     assert(approxEqual(fit.r2, 0.5727));
2201 }
2202 
2203 private void constructXYGraph(T, R1, R2)(R1 x, R2 y, T ret) {
2204     ret.x = toDoubleArray(x);
2205     ret.y = toDoubleArray(y);
2206 
2207     enforce(ret.x.length == ret.y.length,
2208         "x, y must have same length for line/scatter graph.");
2209 
2210     sort!"a[0] < b[0]"(zip(ret.x, ret.y));
2211     fixXYGraphBounds(ret);
2212 }
2213 
2214 private void fixXYGraphBounds(G)(G graph) {
2215     graph.lowerLim = reduce!min(double.infinity, graph.y);
2216     graph.upperLim = reduce!max(-double.infinity, graph.y);
2217     graph.leftLim = reduce!min(double.infinity, graph.x);
2218     graph.rightLim = reduce!max(-double.infinity, graph.x);
2219 
2220     /* Weird things happen when a graph has zero width or height.  Add fudge
2221      * factors to correct this.
2222      */
2223     with(graph) {
2224         if(lowerLim == upperLim) {
2225             lowerLim = nextafter(lowerLim, -double.infinity);
2226             upperLim = nextafter(upperLim, double.infinity);
2227         }
2228 
2229         if(leftLim == rightLim) {
2230             leftLim = nextafter(leftLim, -double.infinity);
2231             rightLim = nextafter(rightLim, double.infinity);
2232         }
2233     }
2234 }
2235 
2236 /**Plot a callable object on a range of values.*/
2237 class ContinuousFunction : LineGraph {
2238     private enum string setupClass = q{
2239         auto ret = new ContinuousFunction;
2240 
2241         enforce(upperLim > lowerLim,
2242             "Can't do a function plot if upperLim <= lowerLim.");
2243         enforce(nEvals >= 2, "Can't do function plot with < 2 evals.");
2244 
2245         immutable diff = upperLim - lowerLim;
2246         immutable width = diff / (nEvals - 1);
2247         auto x = new double[nEvals];
2248         auto y = new double[nEvals];
2249 
2250         foreach(i; 0..nEvals) {
2251             x[i] = i * width + lowerLim;
2252             y[i] = callable(x[i]);
2253         }
2254 
2255         ret.x = x;
2256         ret.y = y;
2257         fixXYGraphBounds(ret);
2258         return ret;
2259     };
2260 
2261     /**Create a ContinuousFunction.  C is any callable type mapping a floating
2262      * point number to a number.  lowerLim is the lower limit of the plot.
2263      * upperLim is the upper limit of the plot.  nEvals is the number of
2264      * evalutations to perform.  The default is 1000.  More evaluations means
2265      * more accuracy but more computational intensity.
2266      */
2267     static ContinuousFunction opCall(C)(
2268         scope C callable,
2269         double lowerLim,
2270         double upperLim,
2271         uint nEvals = 1000
2272     ) if(is(typeof(C.init(2.0)) : double)) {
2273         // For some reason doing return opCall!callable doesn't work.
2274         // Using mixin instead.
2275         mixin(setupClass);
2276     }
2277 
2278     /**Create a ContinuousFunction using a template alias parameter instead of
2279      * a callable object.
2280      *
2281      * Note:  This function is given an name instead of opCall because DMD
2282      * thinks you're trying to instantiate a class if you do
2283      * ContinuousFunction!fun(...).
2284      */
2285     static ContinuousFunction fromAlias(alias callable)(
2286         double lowerLim,
2287         double upperLim,
2288         uint nEvals = 1000
2289     ) if(is(typeof(callable(2.0)) : double)) {
2290         mixin(setupClass);
2291     }
2292 
2293 }
2294 
2295 /**Plot a callable object on a range of values.*/
2296 class DiscreteFunction : BarPlot {
2297 
2298     private this(double[] x, double[] y) {
2299         super(x, y, 1);
2300     }
2301 
2302     /**Create a DiscreteFunction. C is any callable type mapping an integer
2303      * to a number.  lowerLim is the lower limit of the plot.
2304      * upperLim is the upper limit of the plot.
2305      */
2306     static DiscreteFunction opCall(C)(
2307         scope C callable,
2308         int lowerLim,
2309         int upperLim,
2310     ) if(is(typeof(C.init(2)) : double)) {
2311         enforce(upperLim > lowerLim,
2312             "Can't do a function plot if upperLim <= lowerLim.");
2313 
2314         static if(is(typeof(C.opCall))) {
2315             alias ParameterTypeTuple!(C.opCall)[0] I;
2316         } else {
2317             alias ParameterTypeTuple!(C)[0] I;
2318         }
2319 
2320         I input = to!I(lowerLim);
2321         double[] x = new double[upperLim - lowerLim + 1];
2322         double[] y = new double[upperLim - lowerLim + 1];
2323 
2324         foreach(ulIndex; 0..x.length) {
2325             immutable index = cast(int) ulIndex;
2326             x[index] = lowerLim + index;
2327             y[index] = callable(input);
2328             input++;
2329         }
2330 
2331         auto ret = new typeof(return)(x, y);
2332         ret.fixBounds();
2333         return ret;
2334     }
2335 
2336     /**Create a DiscreteFunction using a template alias parameter instead of
2337      * a callable object.
2338      *
2339      * Note:  This function is given an name instead of opCall because DMD
2340      * thinks you're trying to instantiate a class if you do
2341      * ContinuousFunction!fun(...).
2342      */
2343     static DiscreteFunction fromAlias(alias callable)(
2344         int lowerLim,
2345         int upperLim,
2346     ) if(is(typeof(callable(2)) : double)) {
2347         enforce(upperLim > lowerLim,
2348             "Can't do a function plot if upperLim <= lowerLim.");
2349 
2350         int input = lowerLim;
2351         double[] x = new double[upperLim - lowerLim + 1];
2352         double[] y = new double[upperLim - lowerLim + 1];
2353 
2354         foreach(int index; 0..x.length) {
2355             x[index] = lowerLim + index;
2356             y[index] = callable(input);
2357             input++;
2358         }
2359 
2360         auto ret = new typeof(return)(x, y);
2361         ret.fixBounds();
2362         return ret;
2363     }
2364 }
2365 
2366 // Above was relatively basic plots.  Below is more specialized statistical
2367 // plots.
2368 
2369 /**Plots a ROC curve, or a curve with sensitivity on the Y-axis
2370  * and 1 - specificity on the X-axis.  This is a useful metric for
2371  * determining how well a test statistic discriminates between two classes.
2372  * The following assumptions are made in this implementation:
2373  *
2374  * 1.  For some cutoff value c and test statistic T, your decision rule is of
2375  *     the form "Class A if T larger than c, Class B if T smaller than c".
2376  *
2377  * 2.  In the case of ties, i.e. if class A and class B both have an identical
2378  *     value, linear interpolation is used.  This is because changing the
2379  *     value of c infinitesimally will change both sensitivity and specificity
2380  *     in these cases.
2381  */
2382 class RocCurve : LineGraph {
2383 private:
2384     double _auroc;
2385 
2386     this() {
2387         super();
2388     }
2389 
2390 public:
2391     /**Create a RocCurve.  classATs are the test statistics that are
2392      * "supposed" to be bigger, classBTs are the test statistics that are
2393      * "supposed to be smaller.  Both R1 and R2 must be input ranges with
2394      * elements implicitly convertible to double.
2395      */
2396     static RocCurve opCall(R1, R2)(R1 classATs, R2 classBTs)
2397     if(isNumeric!(ElementType!R1) && isNumeric!(ElementType!R2)) {
2398         // Shamelessly cut-and-pasted and modified from dstats.
2399 
2400         auto classA = array(classATs);
2401         auto classB = array(classBTs);
2402         sort(classA);
2403         sort(classB);
2404 
2405         // Start cutoff at -infinity, such that we get everything in class A, i.e.
2406         // perfect specificity, zero sensitivity.  We arbitrarily define class B
2407         // as our "positive" class.
2408         double tp = 0, tn = classA.length, fp = 0, fn = classB.length;
2409         double[2] lastPoint = 0;
2410 
2411         Unqual!(CommonType!(ElementType!R1, ElementType!R2)) currentVal;
2412 
2413         ElementType!R1 popA() {
2414             tn--;
2415             fp++;
2416             auto ret = classA.front();
2417             classA.popFront();
2418             return ret;
2419         }
2420 
2421         ElementType!R2 popB() {
2422             fn--;
2423             tp++;
2424             auto ret = classB.front();
2425             classB.popFront();
2426             return ret;
2427         }
2428 
2429         double area = 0;
2430         double[] x = [0.0];
2431         double[] y = [0.0];
2432         while(!classA.empty && !classB.empty) {
2433             if(classA.front() < classB.front()) {
2434                 currentVal = popA();
2435             } else {
2436                 currentVal = popB();
2437             }
2438 
2439             // Handle ties.
2440             while(!classA.empty && classA.front() == currentVal) {
2441                 popA();
2442             }
2443 
2444             while(!classB.empty && classB.front() == currentVal) {
2445                 popB();
2446             }
2447 
2448             double[2] curPoint;
2449             curPoint[0] = 1.0 - tn / (fp + tn);
2450             curPoint[1] = tp / (tp + fn);
2451 
2452             x ~= curPoint[0];
2453             y ~= curPoint[1];
2454 
2455             immutable xDist = curPoint[0] - lastPoint[0];
2456             area += xDist * lastPoint[1];  // Rectangular part.
2457             area += xDist * 0.5 * (curPoint[1] - lastPoint[1]);  // Triangular part.
2458             lastPoint[] = curPoint[];
2459         }
2460 
2461         if(classA.length > 0 && classB.length == 0) {
2462             // Then we already have a sensitivity of 1, move straight to the right
2463             // to the point (1, 1).
2464 
2465             immutable xDist = 1 - lastPoint[0];
2466             area += xDist * lastPoint[1];  // Rectangular part.
2467             area += xDist * 0.5 * (1 - lastPoint[1]);  // Triangular part.
2468         }
2469 
2470         x ~= 1;
2471         y ~= 1;
2472 
2473         auto ret = new typeof(return)();
2474         ret._auroc = area;
2475         ret.x = x;
2476         ret.y = y;
2477         fixXYGraphBounds(ret);
2478         return ret;
2479     }
2480 
2481     /**Returns the area under the ROC curve.*/
2482     final double auroc() const pure nothrow  {
2483         return _auroc;
2484     }
2485 
2486     /**Default title is the area under the curve.  Default x label is
2487      * "1 - Specificity".  Default y label is "Sensitivity".
2488      */
2489     override Figure toLabeledFigure()  {
2490         auto ret = toFigure;
2491         ret.title = "AUROC = " ~ to!string(auroc);
2492         ret.xLabel = "1 - Specificity";
2493         ret.yLabel = "Sensitivity";
2494         return ret;
2495     }
2496 }
2497 
2498 unittest {
2499     // Values worked out by hand on paper.  If you don't believe me, work
2500     // them out yourself.
2501     auto foo = RocCurve([4,5,6], [1,2,3]);
2502     assert(foo.auroc == 1);
2503 
2504     foo = RocCurve([8,6,7,5,3,0,9], [3,6,2,4,3,6]);
2505     assert(approxEqual(foo.auroc, 0.6904762));
2506 
2507     foo = RocCurve([2,7,1,8,2,8,1,8], [3,1,4,1,5,9,2,6]);
2508     assert(approxEqual(foo.auroc, 0.546875));
2509 }
2510 
2511 /**Plots the quantiles of a set of data on the Y axis against the theoretical
2512  * qualtiles or the quantiles of another set of data on the X axis.
2513  */
2514 class QQPlot : ScatterPlot {
2515     private this() {
2516         super();
2517         _lineColor = getColor(255, 0, 0);
2518     }
2519 
2520     protected override void drawPlot(
2521         Figure form,
2522         double leftMargin,
2523         double topMargin,
2524         double plotWidth,
2525         double plotHeight
2526     ) {
2527         super.drawPlot(
2528             form, leftMargin, topMargin, plotWidth, plotHeight
2529         );
2530 
2531         // Draw line that indicates identical distributions.
2532         if(max(lowerLim, leftLim) < min(upperLim, rightLim)) {
2533             mixin(toPixels);
2534 
2535             auto pen = form.getPen(_lineColor, _lineWidth);
2536             scope(exit) doneWith(pen);
2537 
2538             immutable lowerX = toPixelsX(max(lowerLim, leftLim));
2539             immutable lowerY = toPixelsY(max(lowerLim, leftLim));
2540             immutable upperX = toPixelsX(min(upperLim, rightLim));
2541             immutable upperY = toPixelsY(min(upperLim, rightLim));
2542 
2543             form.drawClippedLine(
2544                 pen,
2545                 PlotPoint(lowerX, lowerY),
2546                 PlotPoint(upperX, upperY)
2547             );
2548         }
2549     }
2550 
2551     private Color _lineColor;
2552 
2553     /**The color of the y = x line that indicates identical distributions.
2554      * The default is red.
2555      */
2556     final Color lineColor()() {
2557         return _lineColor;
2558     }
2559 
2560     /// Setter
2561     final This lineColor(this This)(Color newColor) {
2562         _lineColor = newColor;
2563         return cast(This) this;
2564     }
2565 
2566     private double _lineWidth = 2;
2567 
2568     /**The width of the line that indicates identical distributions.
2569      * The default is 2.
2570      */
2571     final double lineWidth()() {
2572         return _lineWidth;
2573     }
2574 
2575     /// Setter.
2576     final This lineWidth(this This)(double newWidth) {
2577         _lineWidth = newWidth;
2578         return cast(This) this;
2579     }
2580 
2581     /**Create a QQPlot.  dataRange must be an input range with elements
2582      * implicitly convertible to doubles.  quantileFunction must be a
2583      * callable (function pointer, delegate, functor, etc.) mapping any
2584      * number between 0 and 1 to its quantile.
2585      *
2586      * Examples:
2587      * ---
2588      * auto norms = randArray!rNorm(100, 0, 1);
2589      * auto theoretical = paramFunctor!invNormalCDF(0, 1);
2590      * auto fig = new Figure(
2591      *     QQPlot(norms, theoretical)
2592      * );
2593      * fig.showAsMain();
2594      * ---
2595      */
2596     static QQPlot opCall(R, C)(R dataRange, C quantileFunction)
2597     if(is(typeof(C.init(2.0)) : double) && isInputRange!R &&
2598     is(ElementType!R : double)) {
2599         auto ret = new QQPlot;
2600 
2601         ret.y = toDoubleArray(dataRange);
2602         sort(ret.y);
2603         immutable double N = ret.y.length + 1.0;
2604 
2605         ret.x = new double[ret.y.length];
2606         foreach(i, ref elem; ret.x) {
2607             elem = quantileFunction((i + 1) / N);
2608         }
2609 
2610         fixXYGraphBounds(ret);
2611         addFudgeFactors(ret);
2612         return ret;
2613     }
2614 
2615     /**Default x label is "Theoretical Quantiles".  Default y label is
2616      * "Empirical Quantiles".
2617      */
2618     override Figure toLabeledFigure() {
2619         auto ret = toFigure;
2620         ret.xLabel = "Theoretical Quantiles";
2621         ret.yLabel = "Empirical Quantiles";
2622         return ret;
2623     }
2624 }
2625 
2626 /**
2627 Draw a basic box-and-whisker plot.  The plots are drawn centered at Y
2628 coordinates [0, 1, ..., N) unless the offset property is set to something else
2629 (which is useful for putting multiple box pots with different color boxes on
2630 the same figure).
2631 */
2632 class BoxPlot : Plot {
2633 private:
2634     double[] medians;
2635     double[] boxBottoms;
2636     double[] boxTops;
2637     double[] whiskerBottoms;
2638     double[] whiskerTops;
2639     double[][] outliers;
2640     double whiskerPercentile;
2641     double _offset = 0;
2642     Color _color;
2643 
2644     this() {
2645         _color = getColor(0, 0, 0);  // Default color.
2646     }
2647 
2648     void updateBounds() {
2649         if(medians.length == 0) return;
2650 
2651         upperLim = reduce!max(-double.infinity,
2652             chain(whiskerTops, join(outliers))
2653         );
2654 
2655         lowerLim = reduce!min(double.infinity,
2656             chain(whiskerBottoms, join(outliers))
2657         );
2658 
2659         immutable diff = upperLim - lowerLim;
2660         upperLim += 0.01 * diff;
2661         lowerLim -= 0.01 * diff;
2662 
2663         leftLim = -0.6;
2664         rightLim = medians.length - 0.4;
2665 
2666         leftLim += _offset;
2667         rightLim += _offset;
2668 
2669     }
2670 
2671     void addDataImpl(R)(R range) {
2672         auto doubles = toDoubleArray(range);
2673         enforce(doubles.length,
2674             "Cannot add a zero-length range to a box and whisker plot.");
2675         sort(doubles);
2676 
2677         if(doubles.length & 1) {
2678             medians ~= doubles[$ / 2];
2679         } else {
2680             medians ~= 0.5 * doubles[$ / 2] + 0.5 * doubles[$ / 2 + 1];
2681         }
2682 
2683         double doInterp(double percentile) {
2684             immutable floatIndex = percentile * (doubles.length - 1);
2685             immutable floored = to!size_t(floatIndex);
2686             immutable fract = floatIndex - floored;
2687 
2688             if(fract == 1 || floored == doubles.length - 1) {
2689                 return doubles[floored];
2690             } else {
2691                 return (1 - fract) * doubles[floored] +
2692                     fract * doubles[floored + 1];
2693             }
2694         }
2695 
2696         boxBottoms ~= doInterp(0.25);
2697         boxTops ~= doInterp(0.75);
2698         whiskerBottoms ~= doInterp(whiskerPercentile);
2699         whiskerTops ~= doInterp(1 - whiskerPercentile);
2700 
2701         immutable whiskerTopIndex = to!size_t(
2702             ceil((1 - whiskerPercentile) * (doubles.length - 1)) + 1
2703         );
2704         immutable whiskerBottomIndex = to!size_t(
2705             whiskerPercentile * (doubles.length - 1)
2706         );
2707 
2708         auto outlierRange = chain(
2709             doubles[0..whiskerBottomIndex],
2710             doubles[whiskerTopIndex..$]
2711         );
2712 
2713         outliers.length += 1;
2714 
2715         foreach(outlier; outlierRange) {
2716             outliers[$ - 1] ~= outlier;
2717         }
2718 
2719         // We're absolutely sure we don't need doubles anymore, and may
2720         // be working with huge datasets.
2721         delete doubles;
2722 
2723         updateBounds();
2724     }
2725 
2726 protected:
2727     protected override void drawPlot(
2728         Figure form,
2729         double leftMargin,
2730         double topMargin,
2731         double plotWidth,
2732         double plotHeight
2733     ) {
2734         mixin(toPixels);
2735         immutable nBoxes = medians.length;
2736         immutable boxWidth = 1;
2737 
2738         auto pen = form.getPen(_color, 1);
2739         auto font = getFont(plot2kill.util.defaultFont,
2740             10 + Figure.fontSizeAdjust);
2741         auto outlierDrawer = ScatterCharDrawer('o', font, _color, form);
2742 
2743         foreach(boxIndex, med; medians) {
2744             immutable left = boxWidth * boxIndex + 0.15 - 0.5 + _offset;
2745             immutable center = boxWidth * boxIndex + _offset;
2746             immutable right = boxWidth * boxIndex + 0.85 - 0.5 + _offset;
2747             immutable wLeft = boxWidth * boxIndex + 0.35 - 0.5 + _offset;
2748             immutable wRight = boxWidth * boxIndex + 0.65 - 0.5 + _offset;
2749 
2750             immutable leftPixels = toPixelsX(left);
2751             immutable centerPixels = toPixelsX(center);
2752             immutable rightPixels = toPixelsX(right);
2753             immutable wLeftPixels = toPixelsX(wLeft);
2754             immutable wRightPixels = toPixelsX(wRight);
2755             immutable boxTopPixels = toPixelsY(boxTops[boxIndex]);
2756             immutable boxBotPixels = toPixelsY(boxBottoms[boxIndex]);
2757             immutable wTopPixels = toPixelsY(whiskerTops[boxIndex]);
2758             immutable wBotPixels = toPixelsY(whiskerBottoms[boxIndex]);
2759 
2760             // Draw box.
2761             form.drawClippedRectangle(pen,
2762                 leftPixels, boxTopPixels,
2763                 rightPixels - leftPixels,
2764                 boxBotPixels - boxTopPixels
2765             );
2766 
2767             // Draw median lines.
2768             form.drawClippedLine(
2769                 pen,
2770                 PlotPoint(leftPixels, toPixelsY(med)),
2771                 PlotPoint(rightPixels, toPixelsY(med))
2772             );
2773 
2774 
2775             // Draw whiskers.
2776             form.drawClippedLine(
2777                 pen,
2778                 PlotPoint(centerPixels, boxTopPixels),
2779                 PlotPoint(centerPixels, wTopPixels)
2780             );
2781 
2782             form.drawClippedLine(
2783                 pen,
2784                 PlotPoint(centerPixels, boxBotPixels),
2785                 PlotPoint(centerPixels, wBotPixels)
2786             );
2787 
2788             form.drawClippedLine(
2789                 pen,
2790                 PlotPoint(wLeftPixels, wBotPixels),
2791                 PlotPoint(wRightPixels, wBotPixels)
2792             );
2793 
2794             form.drawClippedLine(
2795                 pen,
2796                 PlotPoint(wLeftPixels, wTopPixels),
2797                 PlotPoint(wRightPixels, wTopPixels)
2798             );
2799 
2800             if(whiskerPercentile == 0) continue;
2801 
2802             // Draw outliers.
2803             outlierDrawer.initialize();
2804             scope(exit) outlierDrawer.restore();
2805 
2806             foreach(outlier; outliers[boxIndex]) {
2807                 outlierDrawer.draw(
2808                     PlotPoint(centerPixels, toPixelsY(outlier))
2809                 );
2810             }
2811         }
2812     }
2813 
2814     override void drawLegendSymbol(FigureBase fig, PlotRect where) {
2815         auto pen = fig.getPen(_color, 1);
2816         scope(exit) doneWith(pen);
2817         drawLineLegend(pen, fig, where);
2818     }
2819 
2820 public:
2821 
2822     /**
2823     Create a BoxPlot.  whiskerPercentile controls the percentile at which
2824     data points are considered outliers and plotted as individual points.
2825     For any x, a percentile of x is equivalent to a percentile of 1 - x.
2826     For example, if whiskerPercentile is either 0 or 1, no data is plotted as
2827     individual points and the whiskers extend all the way to the extrema.
2828     */
2829     static BoxPlot opCall(double whiskerPercentile = 0) {
2830         enforce(whiskerPercentile >= 0 && whiskerPercentile <= 1,
2831             "whiskerPercentile must be between 0 and 1.");
2832         auto ret = new BoxPlot;
2833         ret.whiskerPercentile = min(whiskerPercentile, 1 - whiskerPercentile);
2834         return ret;
2835     }
2836 
2837     /**
2838     Add data to the box plot.  data may be any combination of ranges of
2839     ranges and individual ranges.
2840     */
2841     This addData(this This, R...)(R data)
2842     if(allSatisfy!(isInputRange, R)) {
2843         foreach(r; data) {
2844             static if(isInputRange!(ElementType!(typeof(r)))) {
2845                 foreach(rr; r) addDataImpl(rr);
2846             } else {
2847                 addDataImpl(r);
2848             }
2849         }
2850 
2851         return cast(This) this;
2852     }
2853 
2854     /// The offset from zero at which the first box is centered.
2855     double offset()() {
2856         return _offset;
2857     }
2858 
2859     /// Setter
2860     This offset(this This)(double newOffset) {
2861         this._offset = newOffset;
2862         updateBounds();
2863         return cast(This) this;
2864     }
2865 
2866     /// The color that the boxes, whiskers and outliers are drawn in.
2867     Color color()() {
2868         return _color;
2869     }
2870 
2871     /// Setter.
2872     This color(this This)(Color newColor) {
2873         this._color = newColor;
2874         return cast(This) this;
2875     }
2876 
2877     /// The number of boxes currently on the plot.
2878     int nBoxes() {
2879         // If you have more than a few billion boxes, you have bigger
2880         // problems than integer overflow.
2881         return cast(int) medians.length;
2882     }
2883 }
2884 
2885 /**
2886 A dendrogram is a tree of hierarchically clustered data such that the height
2887 of each split is proportional to the distance between clusters.
2888 */
2889 class Dendrogram : Plot {
2890 private:
2891     Cluster* clusters;
2892     immutable int nLeaves;
2893     string[] _names;
2894     
2895     // Make min leaf lines 10% of the difference between max, min distance.
2896     enum leafLineFactor = 0.1;
2897     immutable double minLeafLineLength;
2898     
2899     this(Cluster* clusters) {       
2900         this.clusters = clusters; 
2901         this.leftLim = -0.5;
2902         int nLeaves = 0;
2903         
2904         foreach(leaf; *clusters) {
2905             _names ~= leaf.name;
2906             nLeaves++;
2907         }
2908         
2909         this.nLeaves = nLeaves;
2910         
2911         double minDist = double.infinity, maxDist = -double.infinity;
2912         void doDist(Cluster* node) {
2913             if(node.isLeaf) return;
2914             minDist = min(node.distance, minDist);
2915             maxDist = max(node.distance, maxDist);
2916             
2917             doDist(node.left);
2918             doDist(node.right);
2919         }
2920         
2921         doDist(clusters);
2922         
2923         this.rightLim = nLeaves - 0.5;
2924         this.upperLim = maxDist;
2925     
2926         auto dummyForm = new Figure();
2927     
2928         // Find lower limit by adding in the space taken by the leaf names.
2929         this.minLeafLineLength = leafLineFactor * (maxDist - minDist);
2930         this.lowerLim = minDist - minLeafLineLength;
2931     }
2932     
2933 protected:
2934     override void drawPlot(
2935         Figure form,
2936         double leftMargin,
2937         double topMargin,
2938         double plotWidth,
2939         double plotHeight
2940     ) {
2941         mixin(toPixels);
2942         int curLeafIndex = 0;
2943         
2944         // TODO:  Make these settings customizable.
2945         auto pen = form.getPen(getColor(0, 0, 0), 1);
2946         
2947         double drawLeaf(double parentDistance) {
2948             immutable parentDistPixels = toPixelsY(parentDistance);
2949             immutable lineBottomPixels = toPixelsY(lowerLim);
2950             immutable xPixels = toPixelsX(curLeafIndex);
2951 
2952             form.drawClippedLine(pen,
2953                 PlotPoint(xPixels, lineBottomPixels),
2954                 PlotPoint(xPixels, parentDistPixels)
2955             );
2956             
2957             immutable double ret = curLeafIndex;
2958             curLeafIndex++;
2959             return ret;
2960         }          
2961         
2962         // Returns:  The center of the just-drawn sub-dendrogram.
2963         double drawImpl(Cluster* node) {
2964             double leftCenter, rightCenter;
2965             if(node.left.isLeaf) {
2966                 leftCenter = drawLeaf(node.distance);
2967             } else {
2968                 leftCenter = drawImpl(node.left);
2969             }
2970             
2971             if(node.right.isLeaf) {
2972                 rightCenter = drawLeaf(node.distance);
2973             } else {
2974                 rightCenter = drawImpl(node.right);
2975             }
2976             
2977             immutable leftTop = (node.left.isLeaf) ? (node.distance) :
2978                 node.left.distance;
2979             immutable rightTop = (node.right.isLeaf) ? (node.distance) :
2980                 node.right.distance;                
2981             
2982             immutable leftCenterPixels = toPixelsX(leftCenter);
2983             immutable rightCenterPixels = toPixelsX(rightCenter);
2984             immutable leftTopPixels = toPixelsY(leftTop);
2985             immutable rightTopPixels = toPixelsY(rightTop);
2986             immutable distPixels = toPixelsY(node.distance);
2987             
2988             form.drawClippedLine(pen, 
2989                 PlotPoint(leftCenterPixels, leftTopPixels),
2990                 PlotPoint(leftCenterPixels, distPixels), 
2991             );
2992             
2993             form.drawClippedLine(pen,
2994                 PlotPoint(leftCenterPixels, distPixels),
2995                 PlotPoint(rightCenterPixels, distPixels)
2996             );
2997             
2998             form.drawClippedLine(pen,
2999                 PlotPoint(rightCenterPixels, distPixels),
3000                 PlotPoint(rightCenterPixels, rightTopPixels)
3001             );
3002             
3003             return 0.5 * leftCenter + 0.5 * rightCenter;
3004         }
3005         
3006         drawImpl(clusters);
3007     }
3008     
3009     protected override void drawLegendSymbol(FigureBase fig, PlotRect where) {
3010         auto pen = fig.getPen(getColor(0, 0, 0), 1);
3011         scope(exit) doneWith(pen);
3012         drawLineLegend(pen, fig, where);
3013     }
3014             
3015 public:
3016     /** 
3017     Create a Dendrogram from hierarchically clustered data.
3018     */
3019     static Dendrogram opCall(Cluster* clusters) {
3020         return new Dendrogram(clusters);
3021     }
3022     
3023     /**
3024     Create a Dendrogram figure with rotated text leaf labels on the X-axis
3025     tick labels and "Distance" as the Y-axis label.
3026     */
3027     override Figure toLabeledFigure() @property {
3028         auto ret = this.toFigure;
3029 
3030         ret.xTickLabels(iota(nLeaves), _names);    
3031         ret.yLabel("Distance");
3032         ret.rotatedXTick(true);
3033         
3034         return ret;
3035     }
3036     
3037     /**
3038     The names of the leaf nodes, in order from left to right on the plot.
3039     */
3040     final const(string)[] leafNames() const pure nothrow {
3041         return _names;
3042     }        
3043 }     
3044 
3045 private:
3046 
3047 void addFudgeFactors(P)(P plot) {
3048     with(plot) {
3049         // Add fudge factors to bounds to make points not appear off the chart.
3050         immutable horizFudge = (upperLim - lowerLim) * 0.03;
3051         immutable verticalFudge = (rightLim - leftLim) * 0.03;
3052         leftLim -= verticalFudge;
3053         rightLim += verticalFudge;
3054         upperLim += horizFudge;
3055         lowerLim -= horizFudge;
3056     }
3057 }
3058 
3059 void drawLineLegend(Pen pen, FigureBase fig, PlotRect where) {
3060     auto mid = where.y + where.height / 2;
3061     fig.drawLine(pen,
3062         PlotPoint(where.x, mid),
3063         PlotPoint(where.x + where.width, mid));
3064 }
3065 
3066 void drawFillLegend(Color color, FigureBase fig, PlotRect where) {
3067     auto brush = fig.getBrush(color);
3068     scope(exit) doneWith(brush);
3069     fig.fillRectangle(brush, where.x, where.y, where.width, where.height);
3070 }
3071 
3072 void drawTextLegend(char symbol, Color color, FigureBase fig, PlotRect where) {
3073     // Draw this small even if the actual symbol is huge so it fits in the
3074     // allotted space.
3075     auto font = getFont(plot2kill.util.defaultFont,
3076         10 + Figure.fontSizeAdjust);
3077 
3078     // Center location.
3079     string writeThis = [cast(immutable) symbol];
3080     immutable meas = fig.measureText(writeThis, font);
3081     immutable stdMeas = fig.measureText("A", font);
3082 
3083     where.y += (where.height - meas.height) / 2 - (stdMeas.height - meas.height);
3084     where.height = meas.height;
3085 
3086     scope(exit) doneWith(font);
3087     fig.drawText(writeThis, font, color, where, TextAlignment.Center);
3088 }