1 /**This file contains Figure, which is a holds and draws one or more Plots 
2  * onto a drawable surface.
3  *
4  * Copyright (C) 2010-2011 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 module plot2kill.figure;
33 
34 import plot2kill.plot, plot2kill.util, std.typetuple, std.random;
35 
36 version(dfl) {
37     public import plot2kill.dflwrapper;
38 } else {
39     public import plot2kill.gtkwrapper;
40 }
41 
42 package enum legendSymbolSize = 15;  // 30 by 30 pixels.
43 package enum legendSymbolTextSpace = 3;
44 
45 /**A container form for one or more Plot objects.
46  *
47  * Examples:
48  * ---
49  * auto nums = [1,1,1,1,2,2,3,3,4,5,6,7];
50  * auto hist = Histogram(nums, 10);
51  * auto fig = new Figure;
52  * fig.addPllot(hist);
53  * fig.title = "A plot";
54  * fig.xLabel = "X label";
55  * fig.yLabel = "Y label";
56  * fig.showAsMain();
57  * ---
58  */
59 class Figure : FigureBase {
60 private:
61     double upperLim = -double.infinity;
62     double lowerLim = double.infinity;
63     double leftLim = double.infinity;
64     double rightLim = -double.infinity;
65 
66     // These control whether to auto set the axes.
67     bool userSetXAxis = false;
68     bool userSetYAxis = false;
69 
70     bool _horizontalGrid;
71     bool _verticalGrid;
72 
73     bool _rotatedXTick;
74     bool _rotatedYTick;
75 
76     enum tickPixels = 10;
77     enum legendMarginHoriz = 20;
78     enum legendMarginVert = 10;
79     double xTickLabelWidth;
80     double yTickLabelWidth;
81     double tickLabelHeight;
82 
83     Pen axesPen;
84     ubyte _gridIntensity = 128;
85     Pen gridPen;
86 
87     Font _axesFont;
88     Font _legendFont;
89 
90     Color[] _xTickColors;
91     Color[] _yTickColors;
92 
93     LegendLocation _legendLoc = LegendLocation.bottom;
94 
95     void fixTickSizes() {
96         void fixTickLabelSize(ref double toFix, string[] axisText) {
97             toFix = 0;
98             foreach(lbl; axisText) {
99                 auto lblSize = measureText(lbl, _axesFont);
100                 if(lblSize.height > tickLabelHeight) {
101                     tickLabelHeight = lblSize.height;
102                 }
103 
104                 if(lblSize.width > toFix) {
105                     toFix = lblSize.width;
106                 }
107             }
108         }
109 
110         tickLabelHeight = 0;
111         fixTickLabelSize(xTickLabelWidth, xAxisText);
112         fixTickLabelSize(yTickLabelWidth, yAxisText);
113     }
114 
115     void fixMargins() {
116         fixTickSizes();
117         immutable legendMeasure = measureLegend();
118         immutable legendHeight = legendMeasure.height;
119         immutable legendWidth = legendMeasure.width;
120 
121         immutable xLabelSize = measureText(xLabel(), xLabelFont());
122         immutable bottomTickHeight = (rotatedXTick()) ?
123             xTickLabelWidth : tickLabelHeight;
124         immutable leftTickWidth = (rotatedYTick()) ?
125             tickLabelHeight : yTickLabelWidth;
126 
127         bottomMargin = bottomTickHeight + tickPixels + xLabelSize.height
128             + (legendLocation() == LegendLocation.bottom) * legendHeight + 20;
129 
130         topMargin = measureText(title(), titleFont(), plotWidth).height
131             + (legendLocation() == LegendLocation.top) * legendHeight + 20;
132 
133         leftMargin = measureText(yLabel(), yLabelFont()).height +
134              tickPixels + leftTickWidth
135              + (legendLocation() == LegendLocation.left) * legendWidth + 30;
136 
137         rightMargin = (legendLocation() == LegendLocation.right)
138             * legendWidth + 30;
139     }
140 
141     Tuple!(double, "height", double, "width", int, "nRows") measureLegend() {
142         immutable alwaysWrap = legendLocation() == LegendLocation.right ||
143             legendLocation() == LegendLocation.left;
144 
145         PlotSize ret = PlotSize(0, 0);
146 
147         double maxHeight = 0;
148         double maxWidth = 0;
149         int nRows = 1;
150         double rowPos = legendMarginHoriz;
151 
152         bool shouldReturnZero = true;
153         foreach(plot; plotData) {
154             if(plot.hasLegend && plot.legendText().length) {
155                 shouldReturnZero = false;
156             }
157         }
158 
159         if(shouldReturnZero) {
160             return typeof(return)(0, 0, 0);
161         }
162 
163         foreach(plot; plotData) if(plot.hasLegend) {
164             auto itemSize = plot.measureLegend(legendFont(), this);
165             maxHeight = max(maxHeight, itemSize.height);
166             maxWidth = max(maxWidth, itemSize.width);
167 
168             if(alwaysWrap ||
169             (itemSize.width + rowPos >= this.width - legendMarginHoriz
170             && rowPos > legendMarginHoriz)) {
171                 nRows++;
172                 rowPos = itemSize.width + legendMarginHoriz;
173             }
174 
175             rowPos += itemSize.width + legendMarginHoriz;
176         }
177 
178         return typeof(return)(
179             (maxHeight + legendMarginVert) * nRows,
180             maxWidth + legendMarginHoriz,
181             nRows
182         );
183     }
184 
185     FigureLine[] extraLines;
186 
187     final double plotWidth()  {
188         return this.width - leftMargin - rightMargin;
189     }
190 
191     final double plotHeight()  {
192         return this.height - topMargin - bottomMargin;
193     }
194 
195     mixin(toPixels);
196 
197     void drawTitle() {
198         if(nullOrInit(titleFont())) {
199             return;
200         }
201 
202         auto height = measureText(title(), titleFont()).height;
203         auto rect = PlotRect(leftMargin,
204             10, this.plotWidth, height);
205         auto format = TextAlignment.Center;
206         drawText(title(), titleFont(), getColor(0, 0, 0), rect, format);
207     }
208 
209     void drawXlabel() {
210         if(nullOrInit(xLabelFont())) {
211             return;
212         }
213 
214         immutable textSize = measureText(xLabel(), xLabelFont());
215         immutable yTop = this.height - textSize.height - 10
216             - (legendLocation() == LegendLocation.bottom) *
217                 measureLegend().height;
218         auto rect = PlotRect(leftMargin, yTop,
219             this.width - leftMargin - rightMargin, textSize.height);
220 
221         auto format = TextAlignment.Center;
222         drawText(xLabel(), xLabelFont(), getColor(0, 0, 0), rect, format);
223     }
224 
225     void drawYlabel() {
226         if(nullOrInit(yLabelFont()) || yLabel().length == 0) {
227             return;
228         }
229 
230         immutable textSize = measureText(yLabel(), yLabelFont());
231         immutable margin = (plotHeight - textSize.width) / 2 + topMargin;
232         immutable xCoord = 10 + measureLegend().width
233             * (legendLocation() == LegendLocation.left);
234 
235         auto rect = PlotRect(xCoord, margin, textSize.height, textSize.width);
236 
237         drawRotatedText(yLabel(),
238             yLabelFont(), getColor(0, 0, 0), rect, TextAlignment.Center);
239     }
240 
241     void drawExtraLines() {
242         foreach(line; extraLines) {
243             auto pen = getPen(line.lineColor, line.lineWidth);
244             scope(exit) doneWith(pen);
245 
246             auto start = PlotPoint(toPixelsX(line.x1), toPixelsY(line.y1));
247             auto end = PlotPoint(toPixelsX(line.x2), toPixelsY(line.y2));
248             drawClippedLine(pen, start, end);
249         }
250     }
251 
252     void drawAxes() {
253         immutable origin = PlotPoint(toPixelsX(leftLim), toPixelsY(lowerLim));
254         immutable topLeft = PlotPoint(origin.x, toPixelsY(upperLim));
255         immutable bottomRight = PlotPoint(toPixelsX(rightLim), origin.y);
256 
257         drawLine(axesPen, origin, topLeft);
258         drawLine(axesPen, origin, bottomRight);
259     }
260 
261     void drawTicks() {
262         auto black = getColor(0, 0, 0);
263 
264         foreach(i, tickPoint; xAxisLocations) {
265             if(!(tickPoint >= leftLim && tickPoint <= rightLim)) continue;
266             auto color = (_xTickColors.length) ? _xTickColors[i] : black;
267             drawXTick(tickPoint, xAxisText[i], color);
268         }
269 
270         foreach(i, tickPoint; yAxisLocations) {
271             if(!(tickPoint >= lowerLim && tickPoint <= upperLim)) continue;
272             auto color = (_yTickColors.length) ? _yTickColors[i] : black;
273             drawYTick(tickPoint, yAxisText[i], color);
274         }
275     }
276 
277     void drawLegend() {
278         immutable loc = legendLocation();
279         if(loc == LegendLocation.top || loc == LegendLocation.bottom) {
280             drawLegendImplTopBottom();
281         } else {
282             drawLegendImplLeftRight();
283         }
284     }
285 
286     void drawLegendImplTopBottom() {
287         immutable measurements = measureLegend();
288         if(measurements.height == 0) return;  // No legend.
289         immutable rowHeight = measurements.height / measurements.nRows;
290 
291         // This needs to be precomputed for centering purposes.
292         double[] rowStarts;
293 
294         double curX = legendMarginHoriz;
295 
296         immutable loc = legendLocation();
297         double curY;
298         if(loc == LegendLocation.bottom) {
299             curY = this.height - measurements.height - 10;
300         } else {
301             assert(loc == LegendLocation.top);
302             curY = measureText(title(), titleFont()).height + 10;
303         }
304 
305         size_t rowStartIndex = 0;
306 
307         foreach(plot; plotData) {
308             if(!plot.legendText.length) continue;
309             immutable itemSize = plot.measureLegend(legendFont(), this);
310 
311             if(itemSize.width + curX >= this.width - legendMarginHoriz
312             && curX > legendMarginHoriz) {
313                 // Find centering.
314                 auto rowSize = curX - legendMarginHoriz;
315                 rowStarts ~= max(0, (this.width - rowSize) / 2);
316                 curX = legendMarginHoriz;
317             }
318 
319             curX += itemSize.width + legendMarginHoriz;
320         }
321         // Append last row.
322         auto rowSize = curX - legendMarginHoriz;
323         rowStarts ~= max(0, (this.width - rowSize) / 2);
324 
325         curX = rowStarts[rowStartIndex];
326         double nextX;
327 
328         foreach(plot; plotData) {
329             if(!plot.legendText.length) continue;
330 
331             immutable itemSize = plot.measureLegend(legendFont(), this);
332             if(itemSize.width + curX >= this.width - legendMarginHoriz
333             && curX > legendMarginHoriz) {
334                 curY += rowHeight;
335                 rowStartIndex++;
336                 curX = rowStarts[rowStartIndex];
337                 nextX = curX;
338             }
339 
340             drawLegendElem(curX, curY, plot, rowHeight);
341             curX += itemSize.width + legendMarginHoriz;
342         }
343     }
344 
345     void drawLegendImplLeftRight() {
346         int nRows;
347         foreach(plot; plotData) {
348             if(plot.legendText().length) nRows++;
349         }
350 
351         immutable measurements = measureLegend();
352         immutable rowHeight = (measurements.height / measurements.nRows);
353 
354         double curY = this.height / 2 - nRows * rowHeight / 2;
355         immutable loc = legendLocation();
356         immutable x = (loc == LegendLocation.left) ? 10 :
357             (this.width - measurements.width - 10);
358 
359         foreach(plot; plotData) if(plot.legendText().length) {
360             drawLegendElem(x, curY, plot, rowHeight);
361             curY += rowHeight;
362         }
363     }
364 
365     void drawLegendElem(double curX, double curY, Plot plot, double rowHeight) {
366         immutable textSize = measureText(plot.legendText(), legendFont());
367         assert(textSize.height <= rowHeight);
368 
369         immutable stdLetterHeight = measureText("A", legendFont()).height;
370         immutable textX = curX + legendSymbolSize + legendSymbolTextSpace;
371         auto textRect = PlotRect(
372             textX,
373             curY + rowHeight / 2 - stdLetterHeight / 2,
374             textSize.width,
375             textSize.height
376         );
377         drawText(plot.legendText(), legendFont(), getColor(0, 0, 0), textRect,
378             TextAlignment.Left);
379 
380         auto ySlack = (stdLetterHeight - legendSymbolSize) / 2;
381         auto where = PlotRect(
382             curX, curY + rowHeight / 2 - legendSymbolSize / 2,
383             legendSymbolSize, legendSymbolSize
384         );
385         plot.drawLegendSymbol(this, where);
386     }
387 
388     // Controls the space between a tick line and the tick label.
389     enum lineLabelSpace = 2;
390 
391     void drawXTick(double where, string text, Color color) {
392         immutable wherePixels = toPixelsX(where);
393         drawLine(
394             axesPen,
395             PlotPoint(wherePixels, this.height - bottomMargin),
396             PlotPoint(wherePixels, this.height - bottomMargin + tickPixels)
397         );
398 
399         if(verticalGrid()) {
400             drawLine(gridPen,
401                 PlotPoint(wherePixels, topMargin),
402                 PlotPoint(wherePixels, this.height - bottomMargin));
403         }
404 
405         if(nullOrInit(_axesFont)) {
406             return;
407         }
408 
409         auto format = TextAlignment.Center;
410 
411         immutable textSize = measureText(text, _axesFont, format);
412         immutable tickTextStart =
413             this.height - bottomMargin  + tickPixels + lineLabelSpace;
414 
415         if(rotatedXTick()) {
416             auto rect = PlotRect(wherePixels - tickLabelHeight / 2,
417                 tickTextStart + tickPixels / 2,
418                 textSize.height,
419                 textSize.width
420             );
421 
422             drawRotatedText(text, _axesFont, color, rect, format);
423         } else {
424             auto rect = PlotRect(wherePixels - textSize.width / 2,
425                 tickTextStart,
426                 textSize.width,
427                 textSize.height
428             );
429 
430             drawText(text, _axesFont, color, rect, format);
431         }
432     }
433 
434     void drawYTick(double where, string text, Color color) {
435         immutable wherePixels = this.height - toPixelsY(where);
436         drawLine(
437             axesPen,
438             PlotPoint(leftMargin, this.height - wherePixels),
439             PlotPoint(leftMargin - tickPixels, this.height - wherePixels)
440         );
441 
442         if(nullOrInit(_axesFont)) {
443             return;
444         }
445 
446         if(horizontalGrid()) {
447             drawLine(
448                 gridPen,
449                 PlotPoint(leftMargin, this.height - wherePixels),
450                 PlotPoint(this.width - rightMargin, this.height - wherePixels));
451         }
452 
453         auto format = TextAlignment.Right;
454 
455         immutable textSize = measureText(text, _axesFont, format);
456         if(rotatedYTick()) {
457             auto rect = PlotRect(
458                 leftMargin - textSize.height - tickPixels - lineLabelSpace,
459                 this.height - wherePixels - textSize.width / 2,
460                 textSize.height,
461                 textSize.width
462             );
463 
464             drawRotatedText(text, _axesFont, color, rect, format);
465         } else {
466             auto rect = PlotRect(
467                 leftMargin - textSize.width - tickPixels - lineLabelSpace,
468                 this.height - wherePixels - textSize.height / 2,
469                 textSize.width,
470                 textSize.height
471             );
472 
473             drawText(text, _axesFont, color, rect, format);
474         }
475     }
476 
477     // Used in setupAxes() via delegate.
478     double marginSizeX() {
479         return leftMargin;
480     }
481 
482     // Used in setupAxes() via delegate.
483     double marginSizeY() {
484         return topMargin + bottomMargin;
485     }
486 
487     void setupAxes(
488         double lower,
489         double upper,
490         ref double[] axisLocations,
491         ref string[] axisText,
492         double axisSize,
493         ref double labelSize,
494         double delegate() marginSize
495     )
496     in {
497         assert(upper > lower, std.conv.text(lower, '\t', upper));
498     } body {
499 
500         immutable diff = upper - lower;
501 
502         double tickWidth = 10.0 ^^ floor(log10(diff));
503         if(diff / tickWidth < 2) {
504             tickWidth /= 10;
505         }
506 
507         if(diff / tickWidth > 9) {
508             tickWidth *= 2;
509         }
510 
511 
512         if(diff / tickWidth < 4) {
513             tickWidth /= 2;
514         }
515 
516         void updateAxes() {
517             double startPoint = ceil(lower / tickWidth) * tickWidth;
518 
519             // The tickWidth * 0.01 is a fudge factor to make the last tick
520             // get drawn in the presence of rounding error.
521             axisLocations = array(
522                 iota(startPoint, upper + tickWidth * 0.01, tickWidth)
523             );
524 
525             axisText = doublesToStrings(axisLocations);
526             fixMargins();
527         }
528 
529         do {
530             updateAxes();
531 
532             // Prevent labels from running together on small plots.
533             if((axisSize - marginSize()) / axisLocations.length < labelSize * 4
534                && diff / tickWidth > 2) {
535                 tickWidth *= 2;
536                 continue;
537             } else {
538                 break;
539             }
540         } while(true);
541 
542         // Force at least two ticks come hell or high water.
543         while(axisLocations.length < 2) {
544             tickWidth /= 2;
545             updateAxes();
546         }
547     }
548 
549     void setLim(
550         double lower,
551         double upper,
552         ref double oldLower,
553         ref double oldUpper,
554     ) {
555         enforce(upper > lower, "Can't have upper limit < lower limit.");
556         oldLower = lower;
557         oldUpper = upper;
558     }
559 
560     void nullFontsToDefaults() {
561         if(nullOrInit(titleFont())) {
562             _titleFont = getFont(plot2kill.util.defaultFont, 14 + fontSizeAdjust);
563         }
564         if(nullOrInit(xLabelFont())) {
565             _xLabelFont = getFont(plot2kill.util.defaultFont, 14 + fontSizeAdjust);
566         }
567 
568         if(nullOrInit(yLabelFont())) {
569             _yLabelFont = getFont
570                 (plot2kill.util.defaultFont, 14 + fontSizeAdjust);
571         }
572 
573         if(nullOrInit(axesFont())) {
574             _axesFont = getFont(plot2kill.util.defaultFont, 12 + fontSizeAdjust);
575         }
576 
577         if(nullOrInit(legendFont())) {
578             _legendFont = getFont(plot2kill.util.defaultFont, 12 + fontSizeAdjust);
579         }
580     }
581 
582     static bool isValidPlot(Plot plot) {
583         if(plot is null) {
584             return false;
585         }
586 
587         return plot.leftMost <= plot.rightMost &&
588             plot.bottomMost <= plot.topMost;
589     }
590 
591 package:
592 
593     this() {}
594 
595     this(Plot[] plots...) {
596         this();
597         addPlot!(Figure)(plots);
598     }
599 
600     // These goodies need to be known by various GUI-related code, but end
601     // users have no business fiddling with them.
602     Plot[] plotData;
603 
604     double[] xAxisLocations;
605     string[] xAxisText;
606 
607     double[] yAxisLocations;
608     string[] yAxisText;
609 
610     double topMargin = 10;
611     double bottomMargin = 10;
612     double leftMargin = 10;
613     double rightMargin = 30;
614 
615 public:
616 
617     override int defaultWindowWidth() {
618         return 800;
619     }
620 
621     override int defaultWindowHeight() {
622         return 600;
623     }
624 
625     override int minWindowWidth() {
626         return 400;
627     }
628 
629     override int minWindowHeight() {
630         return 300;
631     }
632 
633     // These drawing commands aren't documented for now b/c they're subject
634     // to change.
635 
636     // Returns whether any part of the rectangle is on screen.
637     bool clipRectangle(ref double x, ref double y, ref double width, ref double height) {
638         // Do clipping.
639         auto bottom = y + height;
640         auto right = x + width;
641         if(x < leftMargin) {
642             x = leftMargin;
643         }
644         if(right > this.width - rightMargin) {
645             right = this.width - rightMargin;
646         }
647 
648         if(y < topMargin) {
649             y = topMargin;
650         }
651 
652         if(bottom > this.height - bottomMargin) {
653             bottom = this.height - bottomMargin;
654         }
655 
656         width = right - x;
657         height = bottom - y;
658         return width > 0 && height > 0;
659     }
660 
661     // Convenience
662     static bool between(T, U, V)(T num, U lower, V upper) {
663         return lower <= num && num <= upper;
664     }
665 
666     bool clipLine(ref double x1, ref double y1, ref double x2, ref double y2) {      
667         immutable topPixel = topMargin;
668         immutable bottomPixel = this.height - bottomMargin - 1;
669         immutable leftPixel = leftMargin + 1;
670         immutable rightPixel = this.width - rightMargin;
671 
672         if(between(x1, leftPixel, rightPixel) &&
673            between(x2, leftPixel, rightPixel) &&
674            between(y1, topPixel, bottomPixel) &&
675            between(y2, topPixel, bottomPixel)) {
676 
677             return true;
678         }
679 
680         // Handle slope of zero or infinity as a special case.
681         if(x1 == x2) {
682             if(!between(x1, leftPixel, rightPixel)) {
683                 return false;
684             } else if(y1 < topPixel && y2 < topPixel) {
685                 return false;
686             } else if(y1 > bottomPixel && y2 > bottomPixel) {
687                 return false;
688             }
689 
690             y1 = max(y1, topPixel);
691             y1 = min(y1, bottomPixel);
692             y2 = max(y2, topPixel);
693             y2 = min(y2, bottomPixel);
694             return true;
695         } else if(y1 == y2) {
696             if(!between(y1, topPixel, bottomPixel)) {
697                 return false;
698             } else if(x1 < leftPixel && x2 < leftPixel) {
699                 return false;
700             } else if(x1 > rightPixel && x2 > rightPixel) {
701                 return false;
702             }
703 
704             x1 = max(x1, leftPixel);
705             x1 = min(x1, rightPixel);
706             x2 = max(x2, leftPixel);
707             x2 = min(x2, rightPixel);
708             return true;
709         }
710 
711         immutable slope = (y2 - y1) / (x2 - x1);
712         enum tol = 0;  // Compensate for rounding error.
713 
714         void fixX(ref double x, ref double y) {
715             if(x < leftPixel) {
716                 immutable diff = leftPixel - x;
717                 x = leftPixel;
718                 y = diff * slope + y;
719             } else if(x > rightPixel) {
720                 immutable diff = rightPixel - x;
721                 x = rightPixel;
722                 y = diff * slope + y;
723             }
724         }
725 
726         void fixY(ref double x, ref double y) {
727             if(y < topPixel) {
728                 immutable diff = topPixel - y;
729                 y = topPixel;
730                 x = diff / slope + x;
731             } else if(y > bottomPixel) {
732                 immutable diff = bottomPixel - y;
733                 y = bottomPixel;
734                 x = diff / slope + x;
735             }
736         }
737         fixX(x1, y1);
738         fixX(x2, y2);
739         fixY(x1, y1);
740         fixY(x2, y2);
741 
742         // This prevents weird rounding artifacts where a line appears as a
743         // single point on the edge of the figure.
744         if(y1 == y2 && x1 == x2 && (
745             (y1 == topPixel || y1 == bottomPixel) ||
746             (x1 == leftPixel || x1 == rightPixel))) {
747             return false;
748         }
749 
750         // The minuses and pluses are to deal w/ rounding error.
751         return between(x1, leftPixel - tol, rightPixel + tol) &&
752                between(x2, leftPixel - tol, rightPixel + tol) &&
753                between(y1, topPixel - tol, bottomPixel + tol) &&
754                between(y2, topPixel - tol, bottomPixel + tol);
755     }
756 
757     void drawClippedRectangle
758     (Pen pen, double x, double y, double width, double height) {
759         if(clipRectangle(x, y, width, height)) {
760             drawRectangle(pen, x, y, width, height);
761         }
762     }
763 
764     void drawClippedRectangle(Pen pen, PlotRect r) {
765         drawClippedRectangle(pen, r.x, r.y, r.width, r.height);
766     }
767 
768     void fillClippedRectangle
769     (Brush brush, double x, double y, double width, double height) {
770         if(clipRectangle(x, y, width, height)) {
771             fillRectangle(brush, x, y, width, height);
772         }
773     }
774 
775     void fillClippedRectangle(Brush brush, PlotRect rect) {
776         fillClippedRectangle(brush, rect.x, rect.y, rect.width, rect.height);
777     }
778 
779     void drawClippedLine(Pen pen, PlotPoint from, PlotPoint to) {
780         auto x1 = from.x;
781         auto y1 = from.y;
782         auto x2 = to.x;
783         auto y2 = to.y;
784         immutable shouldDraw = clipLine(x1, y1, x2, y2);
785 
786         if(!shouldDraw) {
787             return;
788         }
789 
790         drawLine(pen, PlotPoint(x1, y1), PlotPoint(x2, y2));
791     }
792 
793     bool insideAxes(PlotPoint point) {
794         if(between(point.x, leftMargin, this.width - rightMargin) &&
795            between(point.y, topMargin, this.height - bottomMargin)) {
796                return true;
797         } else {
798             return false;
799         }
800     }
801 
802     void drawClippedText(string text, Font font,
803         Color pointColor, PlotRect rect) {
804 
805         // To avoid cutting points off of scatter plots, this function only
806         // checks whether the center of each point is on the graph.  Therefore,
807         // it may allow points to extend slightly off the graph.  This is
808         // annoying, but there's no easy way to fix it w/o risking cutting off
809         // points.
810         immutable xMid = rect.x + rect.width / 2;
811         immutable yMid = rect.y + rect.height / 2;
812 
813         if(insideAxes(PlotPoint(xMid, yMid))) {
814             drawText(text, font, pointColor, rect);
815         }
816     }
817 
818     ///
819     final Font axesFont()() {
820         return _axesFont;
821     }
822 
823     ///
824     final This axesFont(this This)(Font newFont) {
825         _axesFont = newFont;
826         return cast(This) this;
827     }
828 
829     ///
830     final Font legendFont()() {
831         return _legendFont;
832     }
833 
834     ///
835     final This legendFont(this This)(Font newFont) {
836         _legendFont = newFont;
837         return cast(This) this;
838     }
839 
840     ///
841     static Figure opCall()() {
842         return new Figure;
843     }
844 
845     /**Convenience factory that adds all plots provided to the Figure.*/
846     static Figure opCall()(Plot[] plots...) {
847         return new Figure(plots);
848     }
849     
850     /// Ditto
851     static Figure opCall(P)(P[] plots)
852     if(is(P : Plot) && !is(P == Plot)) {
853         return new Figure(cast(Plot[]) plots);
854     }
855 
856     /**Manually set the X axis limits.
857      */
858     final This xLim(this This)(double newLower, double newUpper) {
859         setLim(newLower, newUpper, leftLim, rightLim);
860         return cast(This) this;
861     }
862 
863     /**Manually set the Y axis limits.
864      */
865     This yLim(this This)(double newLower, double newUpper) {
866         setLim(newLower, newUpper, lowerLim, upperLim);
867         return cast(This) this;
868     }
869 
870     /**
871     Set the zoom back to the default value, i.e. just large enough to fit
872     everything on the screen.
873     */
874     This defaultZoom(this This)() {
875         upperLim = -double.infinity;
876         lowerLim = double.infinity;
877         leftLim = double.infinity;
878         rightLim = -double.infinity;
879 
880         foreach(plot; plotData) {
881             upperLim = max(upperLim, plot.topMost);
882             rightLim = max(rightLim, plot.rightMost);
883             leftLim = min(leftLim, plot.leftMost);
884             lowerLim = min(lowerLim, plot.bottomMost);
885         }
886 
887         return cast(This) this;
888     }
889 
890     /**Set the X axis labels.  If text is null (default) the axis text is
891      * just the text of the axis locations.  R should be any range with
892      * length identical to text (unless text is null) and elements implicitly
893      * convertible to double.  If colors is empty, all labels will be made
894      * black.  Otherwise it must be the same length as locations.
895      */
896     This xTickLabels(R, this This)
897     (R locations, const string[] text = null, Color[] colors = null)
898     if(isInputRange!R && is(ElementType!R : double)) {
899         userSetXAxis = true;
900         xAxisLocations = toDoubleArray(locations);
901         enforce(colors.length == xAxisLocations.length || colors.length == 0);
902         this._xTickColors = colors.dup;
903 
904         if(text.length > 0) {
905             enforce(text.length == xAxisLocations.length,
906                 "Length mismatch between X axis locations and X axis text.");
907             xAxisText = text.dup;
908         } else {
909             xAxisText = doublesToStrings(xAxisLocations);
910         }
911 
912         return cast(This) this;
913     }
914 
915     /**
916     Resets the X tick labels to the default, effectively undoing a call to
917     xTickLabels.
918     */
919     This defaultXTick(this This)() {
920         userSetXAxis = false;
921         return cast(This) this;
922     }
923 
924     /**Set the Y axis labels.  If text is null (default) the axis text is
925      * just the text of the axis locations.  R should be any range with
926      * length identical to text (unless text is null) and elements implicitly
927      * convertible to double.  If colors is empty, all labels will be made
928      * black.  Otherwise it must be the same length as locations.
929      */
930     This yTickLabels(R, this This)
931     (R locations, const string[] text = null, Color[] colors = null)
932     if(isInputRange!R && is(ElementType!R : double)) {
933         userSetYAxis = true;
934         yAxisLocations = toDoubleArray(locations);
935         enforce(colors.length == xAxisLocations.length || colors.length == 0);
936         this._yTickColors = colors.dup;
937 
938         if(text.length > 0) {
939             enforce(text.length == yAxisLocations.length,
940                 "Length mismatch between Y axis locations and Y axis text.");
941             yAxisText = text.dup;
942         } else {
943             yAxisText = doublesToStrings(yAxisLocations);
944         }
945 
946         return cast(This) this;
947     }
948 
949     /**
950     Resets the X tick labels to the default, effectively undoing a call to
951     xTickLabels.
952     */
953     This defaultYTick(this This)() {
954         userSetYAxis = false;
955         return cast(This) this;
956     }
957 
958     /**Determines whether vertical gridlines are drawn.  Default is false.*/
959     bool verticalGrid()() {
960         return _verticalGrid;
961     }
962 
963     ///
964     This verticalGrid(this This)(bool val) {
965         this._verticalGrid = val;
966         return cast(This) this;
967     }
968 
969     /**Determines whether horizontal gridlines are drawn.  Default is false.*/
970     bool horizontalGrid()() {
971         return _horizontalGrid;
972     }
973 
974     ///
975     This horizontalGrid(this This)(bool val) {
976         this._horizontalGrid = val;
977         return cast(This) this;
978     }
979 
980     /// Grid intensity from zero (pure white) to 255 (pure black).
981     ubyte gridIntensity()() {
982         return _gridIntensity;
983     }
984 
985     /// Setter.
986     This gridIntensity(this This)(ubyte newIntensity) {
987         _gridIntensity = newIntensity;
988         return cast(This) this;
989     }
990 
991     ///
992     LegendLocation legendLocation()() {
993         return _legendLoc;
994     }
995 
996     ///
997     This legendLocation(this This)(LegendLocation newLoc) {
998         this._legendLoc = newLoc;
999         return cast(This) this;
1000     }
1001 
1002     /**
1003     Determines whether rotated text is used for the X tick labels.
1004     */
1005     final bool rotatedXTick()() {
1006         return _rotatedXTick;
1007     }
1008 
1009     /// Setter
1010     final This rotatedXTick(this This)(bool newVal) {
1011         _rotatedXTick = newVal;
1012         return cast(This) this;
1013     }
1014 
1015     /**
1016     Determines whether rotated text is used for the Y tick labels.
1017     */
1018     final bool rotatedYTick()() {
1019         return _rotatedYTick;
1020     }
1021 
1022     /// Setter
1023     final This rotatedYTick(this This)(bool newVal) {
1024         _rotatedYTick = newVal;
1025         return cast(This) this;
1026     }
1027 
1028     /**The leftmost point on the figure.*/
1029     double leftMost()  {
1030         return leftLim;
1031     }
1032 
1033     /**The rightmost point on the figure.*/
1034     double rightMost()  {
1035         return rightLim;
1036     }
1037 
1038     /**The topmost point on the figure.*/
1039     double topMost()  {
1040         return upperLim;
1041     }
1042 
1043     /**The bottommost point on the figure.*/
1044     double bottomMost()  {
1045         return lowerLim;
1046     }
1047 
1048     /**Add individual lines to the figure.  Coordinates are specified relative
1049      * to the plot area, not in pixels.  The lines are is clipped
1050      * to the visible part of the plot area.  This is useful for adding
1051      * annotation lines, as opposed to plot lines.
1052      */
1053     This addLines(this This)(FigureLine[] lines...) {
1054         extraLines ~= lines;
1055         return cast(This) this;
1056     }
1057 
1058     /**Add one or more plots to the figure.*/
1059     This addPlot(this This)(Plot[] plots...) {
1060         foreach(plot; plots) {
1061             if(!isValidPlot(plot)) {
1062                 continue;
1063             }
1064 
1065             upperLim = max(upperLim, plot.topMost);
1066             rightLim = max(rightLim, plot.rightMost);
1067             leftLim = min(leftLim, plot.leftMost);
1068             lowerLim = min(lowerLim, plot.bottomMost);
1069             plotData ~= plot;
1070         }
1071 
1072         return cast(This) this;
1073     }
1074 
1075     /// Ditto
1076     This addPlot(this This, P)(P[] plots) 
1077     if(is(P : Plot) && !is(P == Plot)) {
1078         return addPlot!(This)(cast(Plot[]) plots);
1079     }
1080 
1081     /**
1082     Remove one or more plots from the figure.  If the plots are not in the
1083     figure, they are silently ignored.
1084     */
1085     This removePlot(this This)(Plot[] plots...) {
1086         void removePlotImpl(Plot p) {
1087             auto plotIndex = countUntil!"a is b"(plotData, p);
1088             if(plotIndex == -1) return;
1089             plotData = plotData[0..plotIndex] ~ plotData[plotIndex + 1..$];
1090         }
1091 
1092         foreach(p; plots) {
1093             removePlotImpl(p);
1094         }
1095 
1096         upperLim = reduce!max(-double.infinity, map!"a.topMost"(plotData));
1097         lowerLim = reduce!min(double.infinity, map!"a.bottomMost"(plotData));
1098         rightLim = reduce!max(-double.infinity, map!"a.rightMost"(plotData));
1099         leftLim = reduce!min(double.infinity, map!"a.leftMost"(plotData));
1100 
1101         return cast(This) this;
1102     }
1103     
1104     /// Ditto
1105     This removePlot(this This, P)(P[] plots) 
1106     if(is(P : Plot) && !is(P == Plot)) {
1107         return removePlot!(This)(cast(Plot[]) plots);
1108     }
1109 
1110     /**Draw the plot but don't display it on screen.*/
1111     override void drawImpl() {
1112         // This block of code adds fudge factors if leftLim == rightLim or
1113         // lowerLim == upperLim (i.e. perfectly horizontal or vertical lines).
1114         // This avoids figures that are infinitely narrow in one direction.
1115         // It resets them on exit so that if more stuff is added to this figure,
1116         // there are no unanticipated side effects of this kludge.
1117         immutable oldLeft = leftLim;
1118         immutable oldRight = rightLim;
1119         immutable oldUpper = upperLim;
1120         immutable oldLower = lowerLim;
1121         
1122         scope(exit) {
1123             leftLim = oldLeft;
1124             rightLim = oldRight;
1125             upperLim = oldUpper;
1126             lowerLim = oldLower;
1127         }
1128         
1129         enum fudgeFactor = 1e-5;
1130         if(leftLim == rightLim) {
1131             leftLim -= fudgeFactor;
1132             rightLim += fudgeFactor;
1133         }
1134         
1135         if(upperLim == lowerLim) {
1136             upperLim += fudgeFactor;
1137             lowerLim -= fudgeFactor;
1138         }
1139         
1140         auto whiteBrush = getBrush(getColor(255, 255, 255));
1141         fillRectangle(whiteBrush, 0, 0, this.width, this.height);
1142         doneWith(whiteBrush);
1143         // If this is not a valid Figure, leave a big blank white rectangle.
1144         // It beats crashing.
1145         if(!(leftLim < rightLim && lowerLim < upperLim)) {
1146             return;
1147         }
1148         axesPen = getPen(getColor(0, 0, 0), 2);
1149         scope(exit) doneWith(axesPen);
1150 
1151         auto notGridIntens = cast(ubyte) (ubyte.max - gridIntensity());
1152         gridPen = getPen(
1153             getColor(notGridIntens, notGridIntens, notGridIntens), 1
1154         );
1155         scope(exit) doneWith(gridPen);
1156 
1157         nullFontsToDefaults();
1158 
1159         if(!userSetXAxis) {
1160             setupAxes(leftLim, rightLim, xAxisLocations, xAxisText,
1161                 this.width, xTickLabelWidth, &marginSizeX);
1162         }
1163 
1164         if(!userSetYAxis) {
1165             setupAxes(lowerLim, upperLim, yAxisLocations, yAxisText,
1166                 this.height, tickLabelHeight, &marginSizeY);
1167         }
1168 
1169         fixMargins();
1170         drawTicks();
1171 
1172         foreach(plot; plotData) {
1173             if(!isValidPlot(plot)) {
1174                 continue;
1175             }
1176 
1177             immutable x = toPixelsX(plot.leftMost);
1178             immutable y = toPixelsY(plot.topMost);
1179             immutable subHeight = toPixelsY(plot.bottomMost) - y;
1180             immutable subWidth = toPixelsX(plot.rightMost) - x;
1181             plot.drawPlot(this, x, y, subWidth, subHeight);
1182         }
1183 
1184         drawYlabel();
1185         drawExtraLines();
1186         drawAxes();
1187         drawTitle();
1188         drawXlabel();
1189         drawLegend();
1190     }
1191 
1192     version(none) {
1193     void showUsingImplicitMain() {
1194         ImplicitMain.initialize();
1195         ImplicitMain.addForm(this);
1196     }
1197     }
1198 }
1199 
1200 ///
1201 enum LegendLocation {
1202     ///
1203     top,
1204 
1205     ///
1206     bottom,
1207 
1208     ///
1209     left,
1210 
1211     ///
1212     right
1213 }
1214 
1215 /**For drawing extra lines on a Figure, with coordinates specified in plot
1216  * units and relative to the plot area, not in pixels.*/
1217 struct FigureLine {
1218 private:
1219     double x1;
1220     double y1;
1221     double x2;
1222     double y2;
1223     Color lineColor;
1224     uint lineWidth = 1;
1225 
1226 public:
1227     this(double x1, double y1, double x2,
1228          double y2, Color lineColor, uint lineWidth = 1) {
1229 
1230         enforce(isFinite(x1) && isFinite(x2) && isFinite(y1) && isFinite(y2),
1231                 "Line coordinates must be finite.");
1232         this.x1 = x1;
1233         this.y1 = y1;
1234         this.x2 = x2;
1235         this.y2 = y2;
1236         this.lineColor = lineColor;
1237         this.lineWidth = lineWidth;
1238     }
1239 }
1240 
1241 /**
1242 Most of these classes copy their input data into a double[] by default.  Use
1243 this to signal that copying is unnecessary.  The range primitives just forward
1244 to data.
1245 */
1246 struct NoCopy {
1247     ///
1248     double[] data;
1249 
1250     ///
1251     double front() @property { return data.front; }
1252 
1253     ///
1254     void popFront() { data.popFront(); }
1255 
1256     ///
1257     bool empty() @property { return data.empty; }
1258 
1259     ///
1260     typeof(this) save() @property { return this; }
1261 
1262     ///
1263     double opIndex(size_t index) { return data[index]; }
1264 
1265     ///
1266     typeof(this) opSlice(size_t lower, size_t upper) {
1267         return NoCopy(data[lower..upper]);
1268     }
1269 }
1270 
1271 private template isPlot(P) { enum isPlot = is(P : Plot); }