1 module ggplotd.ggplotd;
2 
3 import cconfig = cairo.c.config;
4 import cpdf = cairo.pdf;
5 import csvg = cairo.svg;
6 import cairo = cairo;
7 
8 import ggplotd.colour;
9 import ggplotd.geom : Geom;
10 import ggplotd.bounds : Bounds;
11 import ggplotd.colourspace : RGBA;
12 
13 version (unittest)
14 {
15     import dunit.toolkit;
16 }
17 
18 /// delegate that takes a Title struct and returns a changed Title struct
19 alias TitleFunction = Title delegate(Title);
20 
21 /// Currently only holds the title. In the future could also be used to store details on location etc.
22 struct Title
23 {
24     /// The actual title
25     string[] title;
26 }
27 
28 /**
29 Draw the title
30 
31 Examples:
32 --------------------
33 GGPlotD().put( title( "My title" ) );
34 --------------------
35 */
36 TitleFunction title( string title )
37 {
38     return delegate(Title t) { t.title = [title]; return t; };
39 }
40 
41 /**
42 Draw the multiline title
43 
44 Examples:
45 --------------------
46 GGPlotD().put( title( ["My title line1", "line2", "line3"] ) );
47 --------------------
48 */
49 TitleFunction title( string[] title )
50 {
51     return delegate(Title t) { t.title = title; return t; };
52 }
53 
54 private auto createEmptySurface( string fname, int width, int height,
55     RGBA colour )
56 {
57     cairo.Surface surface;
58 
59     static if (cconfig.CAIRO_HAS_PDF_SURFACE)
60         {
61         if (fname[$ - 3 .. $] == "pdf")
62             {
63             surface = new cpdf.PDFSurface(fname, width, height);
64         }
65     }
66     else
67         {
68         if (fname[$ - 3 .. $] == "pdf")
69             assert(0, "PDF support not enabled by cairoD");
70     }
71     static if (cconfig.CAIRO_HAS_SVG_SURFACE)
72         {
73         if (fname[$ - 3 .. $] == "svg")
74             {
75             surface = new csvg.SVGSurface(fname, width, height);
76         }
77     }
78     else
79     {
80         if (fname[$ - 3 .. $] == "svg")
81             assert(0, "SVG support not enabled by cairoD");
82     }
83     if (fname[$ - 3 .. $] == "png")
84     {
85         surface = new cairo.ImageSurface(cairo.Format.CAIRO_FORMAT_ARGB32, width, height);
86     }
87 
88     import ggplotd.colourspace : toCairoRGBA;
89     auto backcontext = cairo.Context(surface);
90     backcontext.setSourceRGBA(colour.toCairoRGBA);
91     backcontext.paint;
92 
93     return surface;
94 }
95 
96 ///
97 private auto drawTitle( in Title title, ref cairo.Surface surface,
98     in Margins margins, int width )
99 {
100     auto context = cairo.Context(surface);
101     context.setFontSize(16.0);
102     context.moveTo( width/2, margins.top/2 );
103 
104     auto f = context.fontExtents();
105     foreach(t; title.title)
106     {
107         auto e = context.textExtents(t);
108         context.relMoveTo( -e.width/2, 0 );
109         context.showText(t);
110         context.relMoveTo( -e.width/2, f.height );
111     }
112 
113     return surface;
114 }
115 
116 import ggplotd.scale : ScaleType;
117 import ggplotd.guide : GuideToDoubleFunction, GuideToColourFunction;
118 private auto drawGeom( in Geom geom, ref cairo.Surface surface,
119      in GuideToDoubleFunction xFunc, in GuideToDoubleFunction yFunc,
120      in GuideToColourFunction cFunc, in GuideToDoubleFunction sFunc,
121      in ScaleType scaleFunction,
122      in Bounds bounds,
123      in Margins margins, int width, int height )
124 {
125     if (geom.draw.isNull)
126         return surface;
127     cairo.Context context;
128     if (geom.mask) {
129         auto plotSurface = cairo.Surface.createForRectangle(surface,
130             cairo.Rectangle!double(margins.left, margins.top,
131             width - (margins.left+margins.right),
132             height - (margins.top+margins.bottom)));
133         context = cairo.Context(plotSurface);
134     } else {
135         context = cairo.Context(surface);
136         context.translate(margins.left, margins.top);
137     }
138     import std.conv : to;
139     context = scaleFunction(context, bounds,
140         width.to!double - (margins.left+margins.right),
141         height.to!double - (margins.top+margins.bottom));
142     context = geom.draw.get()(context, xFunc, yFunc, cFunc, sFunc);
143     return surface;
144 }
145 
146 /// Specify margins in number of pixels
147 struct Margins
148 {
149     /// Create new Margins object based on old one
150     this(in Margins copy) {
151         this(copy.left, copy.right, copy.bottom, copy.top);
152     }
153 
154     /// Create new Margins object based on specified sizes
155     this(in size_t l, in size_t r, in size_t b, in size_t t) {
156         left = l;
157         right = r;
158         bottom = b;
159         top = t;
160     }
161 
162     /// left margin
163     size_t left = 50;
164     /// right margin
165     size_t right = 20;
166     /// bottom margin
167     size_t bottom = 50;
168     /// top margin
169     size_t top = 40;
170 }
171 
172 Margins defaultMargins(int size1, int size2)
173 {
174     import std.conv : to;
175     Margins margins;
176     auto scale = defaultScaling(size1, size2);
177     margins.left = (margins.left*scale).to!size_t;
178     margins.right = (margins.right*scale).to!size_t;
179     margins.top = (margins.top*scale).to!size_t;
180     margins.bottom = (margins.bottom*scale).to!size_t;
181     return margins;
182 }
183 
184 private auto defaultScaling( int size )
185 {
186     if (size > 500)
187         return 1;
188     if (size < 100)
189         return 0.6;
190     return 0.6+(1.0-0.6)*(size-100)/(500-100);
191 }
192 
193 private auto defaultScaling( int size1, int size2 )
194 {
195     return (defaultScaling(size1) + defaultScaling(size2))/2.0;
196 }
197 
198 unittest
199 {
200     assertEqual(defaultScaling(50), 0.6);
201     assertEqual(defaultScaling(600), 1.0);
202     assertEqual(defaultScaling(100), 0.6);
203     assertEqual(defaultScaling(500), 1.0);
204     assertEqual(defaultScaling(300), 0.8);
205 }
206 
207 /// GGPlotD contains the needed information to create a plot
208 struct GGPlotD
209 {
210     import ggplotd.bounds : height, width;
211     import ggplotd.colour : ColourGradientFunction;
212     import ggplotd.scale : ScaleType;
213 
214     /**
215     Draw the plot to a cairoD cairo surface.
216 
217     Params:
218         surface = Surface object of type cairo.Surface from cairoD library, on top of which this plot is drawn.
219         width = Width of the given surface.
220         height = Height of the given surface.
221 
222     Returns:
223         Resulting surface of the same type as input surface, with this plot drawn on top of it.
224     */
225     ref cairo.Surface drawToSurface(ref return cairo.Surface surface, int width, int height ) const
226     {
227         import std.range : empty, front;
228         import std.typecons : Tuple;
229 
230         import ggplotd.bounds : AdaptiveBounds;
231         import ggplotd.guide : GuideStore;
232 
233         Tuple!(double, string)[] xAxisTicks;
234         Tuple!(double, string)[] yAxisTicks;
235 
236         GuideStore!"x" xStore;
237         GuideStore!"y" yStore;
238         GuideStore!"colour" colourStore;
239         GuideStore!"size" sizeStore;
240 
241         foreach (geom; geomRange.data)
242         {
243             xStore.put(geom.xStore);
244             yStore.put(geom.yStore);
245             colourStore.put(geom.colourStore);
246             sizeStore.put(geom.sizeStore);
247         }
248 
249         // Set scaling
250         import ggplotd.guide : guideFunction;
251         import ggplotd.scale : applyScaleFunction, applyScale;
252         auto xFunc = guideFunction(xStore);
253         auto yFunc = guideFunction(yStore);
254         auto cFunc = guideFunction(colourStore, this.colourGradient());
255         auto sFunc = guideFunction(sizeStore);
256         foreach (scale; scaleFunctions)
257             scale.applyScaleFunction(xFunc, yFunc, cFunc, sFunc);
258 
259         AdaptiveBounds bounds;
260         bounds = bounds.applyScale(xFunc, xStore, yFunc, yStore);
261 
262         import std.algorithm : map;
263         import std.array : array;
264         import std.typecons : tuple;
265         if (xStore.hasDiscrete)
266             xAxisTicks = xStore
267                 .storeHash
268                 .byKeyValue()
269                 .map!((kv) => tuple(xFunc(kv.value), kv.key))
270                 .array;
271         if (yStore.hasDiscrete)
272             yAxisTicks = yStore
273                 .storeHash
274                 .byKeyValue()
275                 .map!((kv) => tuple(yFunc(kv.value), kv.key))
276                 .array;
277 
278         // Axis
279         import std.algorithm : sort, uniq, min, max;
280         import std.range : chain;
281         import std.array : array;
282 
283         import ggplotd.axes : initialized, axisAes;
284 
285         // TODO move this out of here and add some tests
286         // If ticks are provided then we make sure the bounds include them
287         auto xSortedTicks = xAxisTicks.sort().uniq.array;
288         if (!xSortedTicks.empty)
289         {
290             bounds.min_x = min( bounds.min_x, xSortedTicks[0][0] );
291             bounds.max_x = max( bounds.max_x, xSortedTicks[$-1][0] );
292         }
293         if (initialized(xaxis))
294         {
295             bounds.min_x = xaxis.min;
296             bounds.max_x = xaxis.max;
297         }
298 
299         // This needs to happen before the offset of x axis is set
300         auto ySortedTicks = yAxisTicks.sort().uniq.array;
301         if (!ySortedTicks.empty)
302         {
303             bounds.min_y = min( bounds.min_y, ySortedTicks[0][0] );
304             bounds.max_y = max( bounds.max_y, ySortedTicks[$-1][0] );
305         }
306         if (initialized(yaxis))
307         {
308             bounds.min_y = yaxis.min;
309             bounds.max_y = yaxis.max;
310         }
311 
312         import std.math : isNaN;
313         auto offset = bounds.min_y;
314         if (!isNaN(xaxis.offset))
315             offset = xaxis.offset;
316         if (!xaxis.show) // Trixk to draw the axis off screen if it is hidden
317             offset = yaxis.min - bounds.height;
318 
319         // TODO: Should really take separate scaling for number of ticks (defaultScaling(width))
320         // and for font: defaultScaling(widht, height)
321         auto aesX = axisAes("x", bounds.min_x, bounds.max_x, offset, defaultScaling(width, height),
322             xSortedTicks );
323 
324         offset = bounds.min_x;
325         if (!isNaN(yaxis.offset))
326             offset = yaxis.offset;
327         if (!yaxis.show) // Trixk to draw the axis off screen if it is hidden
328             offset = xaxis.min - bounds.width;
329         auto aesY = axisAes("y", bounds.min_y, bounds.max_y, offset, defaultScaling(height, width),
330             ySortedTicks );
331 
332         import ggplotd.geom : geomAxis;
333         import ggplotd.axes : tickLength;
334 
335         auto currentMargins = margins(width, height);
336 
337         auto gR = chain(
338                 geomAxis(aesX,
339                     bounds.height.tickLength(height - currentMargins.bottom - currentMargins.top,
340                         defaultScaling(width), defaultScaling(height)),
341 						xaxis.label, xaxis.textAngle),
342                 geomAxis(aesY,
343                     bounds.width.tickLength(width - currentMargins.left - currentMargins.right,
344                         defaultScaling(width), defaultScaling(height)),
345 						yaxis.label, yaxis.textAngle),
346             );
347         auto plotMargins = Margins(currentMargins);
348         if (!legends.empty)
349             plotMargins.right += legends[0].width;
350 
351         foreach (geom; chain(geomRange.data, gR) )
352         {
353             surface = geom.drawGeom( surface,
354                 xFunc, yFunc, cFunc, sFunc,
355                 scale(), bounds,
356                 plotMargins, width, height );
357         }
358 
359         // Plot title
360         surface = title.drawTitle( surface, currentMargins, width );
361 
362         import std.range : iota, zip, dropOne;
363         foreach(ly; zip(legends, iota(0.0, height, height/(legends.length+1.0)).dropOne))
364         {
365             auto legend = ly[0];
366             auto y = ly[1] - legend.height*.5;
367             if (legend.type == "continuous") {
368                 import ggplotd.legend : drawContinuousLegend;
369                 auto legendSurface = cairo.Surface.createForRectangle(surface,
370                     cairo.Rectangle!double(width - currentMargins.right - legend.width,
371                     y, legend.width, legend.height ));//margins.right, margins.right));
372                 legendSurface = drawContinuousLegend( legendSurface,
373                 legend.width, legend.height,
374                     colourStore, this.colourGradient );
375             } else if (legend.type == "discrete") {
376                 import ggplotd.legend : drawDiscreteLegend;
377                 auto legendSurface = cairo.Surface.createForRectangle(surface,
378                     cairo.Rectangle!double(width - currentMargins.right - legend.width,
379                     y, legend.width, legend.height ));//margins.right, margins.right));
380                 legendSurface = drawDiscreteLegend( legendSurface,
381                 legend.width, legend.height,
382                     colourStore, this.colourGradient );
383             }
384         }
385 
386         return surface;
387     }
388 
389     version(ggplotdGTK)
390     {
391         import gtkdSurface = cairo.Surface; // cairo surface module in GtkD package.
392 
393         /**
394         Draw the plot to a GtkD cairo surface.
395 
396         Params:
397             surface = Surface object of type cairo.Surface from GtkD library, on top of which this plot is drawn.
398             width = Width of the given surface.
399             height = Height of the given surface.
400 
401         Returns:
402             Resulting surface of the same type as input surface, with this plot drawn on top of it.
403         */
404         auto drawToSurface( ref gtkdSurface.Surface surface, int width, int height ) const
405         {
406             import gtkc = gtkc.cairotypes;
407             import cairod = cairo.c.cairo;
408 
409             alias gtkd_surface_t = gtkc.cairo_surface_t;
410             alias cairod_surface_t = cairod.cairo_surface_t;
411 
412             cairo.Surface cairodSurface = new cairo.Surface(cast(cairod_surface_t*)surface.getSurfaceStruct());
413             drawToSurface(cairodSurface, width, height);
414 
415             return surface;
416         }
417     }
418 
419     /// save the plot to a file
420     void save( string fname, int width = 470, int height = 470 ) const
421     {
422         bool pngWrite = false;
423         auto surface = createEmptySurface( fname, width, height,
424             theme.backgroundColour );
425 
426         surface = drawToSurface( surface, width, height );
427 
428         if (fname[$ - 3 .. $] == "png")
429         {
430             pngWrite = true;
431         }
432 
433         if (pngWrite)
434             (cast(cairo.ImageSurface)(surface)).writeToPNG(fname);
435     }
436 
437     /// Using + to extend the plot for compatibility to ggplot2 in R
438     ref GGPlotD opBinary(string op, T)(T rhs) if (op == "+")
439     {
440         import ggplotd.axes : XAxisFunction, YAxisFunction;
441         import ggplotd.colour : ColourGradientFunction;
442         static if (is(ElementType!T==Geom))
443         {
444             geomRange.put( rhs );
445         }
446         static if (is(T==ScaleType))
447         {
448             scaleFunction = rhs;
449         }
450         static if (is(T==XAxisFunction))
451         {
452             xaxis = rhs( xaxis );
453         }
454         static if (is(T==YAxisFunction))
455         {
456             yaxis = rhs( yaxis );
457         }
458         static if (is(T==TitleFunction))
459         {
460             title = rhs( title );
461         }
462         static if (is(T==ThemeFunction))
463         {
464             theme = rhs( theme );
465         }
466         static if (is(T==Margins))
467         {
468             _margins = rhs;
469         }
470         static if (is(T==Legend))
471         {
472             legends ~= rhs;
473         }
474         static if (is(T==ColourGradientFunction)) {
475             colourGradientFunction = rhs;
476         }
477         static if (is(T==ScaleFunction)) {
478             scaleFunctions ~= rhs;
479         }
480         return this;
481     }
482 /// put/add to the plot
483     ref GGPlotD put(T)(T rhs)
484     {
485         return this.opBinary!("+", T)(rhs);
486     }
487 
488     /// Active scale
489     ScaleType scale() const
490     {
491         import ggplotd.scale : defaultScale = scale;
492         // Return active function or the default
493         if (!scaleFunction.isNull)
494             return scaleFunction.get();
495         else
496             return defaultScale();
497     }
498 
499     /// Active colourGradient
500     ColourGradientFunction colourGradient() const
501     {
502         import ggplotd.colour : defaultColourGradient = colourGradient;
503         import ggplotd.colourspace : HCY;
504         if (!colourGradientFunction.isNull)
505             return colourGradientFunction.get();
506         else
507             return defaultColourGradient!HCY("");
508     }
509 
510     /// Active margins
511     Margins margins(int width, int height) const
512     {
513         if (!_margins.isNull)
514             return _margins.get();
515         else
516             return defaultMargins(width, height);
517     }
518 
519 private:
520     import std.range : Appender;
521     import ggplotd.theme : Theme, ThemeFunction;
522     import ggplotd.legend : Legend;
523     Appender!(Geom[]) geomRange;
524 
525     import ggplotd.scale : ScaleFunction;
526     ScaleFunction[] scaleFunctions;
527 
528     import ggplotd.axes : XAxis, YAxis;
529     XAxis xaxis;
530     YAxis yaxis;
531 
532 
533     Title title;
534     Theme theme;
535 
536     import std.typecons : Nullable;
537     Nullable!(Margins) _margins;
538     Nullable!(ScaleType) scaleFunction;
539     Nullable!(ColourGradientFunction) colourGradientFunction;
540 
541     Legend[] legends;
542 }
543 
544 unittest
545 {
546     import std.range : zip;
547     import std.algorithm : map;
548     import ggplotd.geom;
549     import ggplotd.aes;
550 
551     const win_width = 1024;
552     const win_height = 1024;
553 
554     const radius = 400.;
555 
556 
557     auto gg = GGPlotD();
558     gg = zip([ 0, radius*0.45 ], [ 0, radius*0.45])
559         .map!((a) => aes!("x","y")(a[0], a[1]))
560         .geomLine.putIn(gg);
561     gg = zip([ 300, radius*0.45 ], [ 210, radius*0.45])
562         .map!((a) => aes!("x","y")(a[0], a[1]))
563         .geomLine.putIn(gg);
564 
565     import ggplotd.theme : Theme, ThemeFunction;
566     Theme theme;
567 
568     auto surface = createEmptySurface( "test.png", win_width, win_height,
569         theme.backgroundColour );
570 
571     auto dim = gg.geomRange.data.length;
572     surface = gg.drawToSurface( surface, win_width, win_height );
573     assertEqual( dim, gg.geomRange.data.length );
574     surface = gg.drawToSurface( surface, win_width, win_height );
575     assertEqual( dim, gg.geomRange.data.length );
576     surface = gg.drawToSurface( surface, win_width, win_height );
577     assertEqual( dim, gg.geomRange.data.length );
578 }
579 
580 version(ggplotdGTK)
581 {
582     unittest
583     {
584         import std.range : zip;
585         import std.algorithm : map;
586         // Draw same plot on cairod.ImageSurface, and on gtkd.cairo.ImageSurface,
587         // and prove resulting images are the same.
588 
589         import ggplotd.geom;
590         import ggplotd.aes;
591 
592         import gtkSurface = cairo.Surface;
593         import gtkImageSurface = cairo.ImageSurface;
594         import gtkCairoTypes = gtkc.cairotypes;
595 
596         const win_width = 1024;
597         const win_height = 1024;
598 
599         const radius = 400.;
600 
601         auto gg = GGPlotD();
602         gg = zip([ 0, radius*0.45 ], [ 0, radius*0.45])
603             .map!((a) => aes!("x","y")(a[0], a[1]))
604             .geomLine.putIn(gg);
605         gg = zip([ 300, radius*0.45 ], [ 210, radius*0.45])
606             .map!((a) => aes!("x","y")(a[0], a[1]))
607             .geomLine.putIn(gg);
608 
609         cairo.Surface cairodSurface =
610             new cairo.ImageSurface(cairo.Format.CAIRO_FORMAT_RGB24, win_width, win_height);
611         gtkSurface.Surface gtkdSurface =
612             gtkImageSurface.ImageSurface.create(gtkCairoTypes.cairo_format_t.RGB24,
613                 win_width, win_height);
614 
615         auto cairodImageSurface = cast(cairo.ImageSurface)cairodSurface;
616         auto gtkdImageSurface = cast(gtkImageSurface.ImageSurface)gtkdSurface;
617 
618         gg.drawToSurface(cairodSurface, win_width, win_height);
619         gg.drawToSurface(gtkdSurface, win_width, win_height);
620 
621         auto byteSize = win_width*win_height*4;
622 
623         assertEqual(cairodImageSurface.getData()[0..byteSize],
624             gtkdImageSurface.getData()[0..byteSize]);
625     }
626 }
627 
628 unittest
629 {
630     import ggplotd.axes : yaxisLabel, yaxisRange;
631     auto gg = GGPlotD()
632         .put( yaxisLabel( "My ylabel" ) )
633         .put( yaxisRange( 0, 2.0 ) );
634     assertEqual( gg.yaxis.max, 2.0 );
635     assertEqual( gg.yaxis.label, "My ylabel" );
636 
637     gg = GGPlotD();
638     gg.put( yaxisLabel( "My ylabel" ) )
639         .put( yaxisRange( 0, 2.0 ) );
640     assertEqual( gg.yaxis.max, 2.0 );
641     assertEqual( gg.yaxis.label, "My ylabel" );
642 }
643 
644 
645 ///
646 unittest
647 {
648     import std.range : zip;
649     import std.algorithm : map;
650 
651     import ggplotd.aes : aes;
652     import ggplotd.geom : geomLine;
653     import ggplotd.scale : scale;
654     auto gg = zip(["a", "b", "c", "b"], ["x", "y", "y", "x"], ["b", "b", "b", "b"])
655         .map!((a) => aes!("x", "y", "colour")(a[0], a[1], a[2]))
656         .geomLine
657         .putIn(GGPlotD());
658     gg + scale();
659     gg.save( "test6.png");
660 }
661 
662 ///
663 unittest
664 {
665     // http://blackedder.github.io/ggplotd/images/noise.png
666     import std.array : array;
667     import std.math : sqrt;
668     import std.algorithm : map;
669     import std.range : zip, iota;
670     import std.random : uniform;
671 
672     import ggplotd.aes : aes;
673     import ggplotd.geom : geomLine, geomPoint;
674     // Generate some noisy data with reducing width
675     auto f = (double x) { return x/(1+x); };
676     auto width = (double x) { return sqrt(0.1/(1+x)); };
677     auto xs = iota( 0, 10, 0.1 ).array;
678 
679     auto ysfit = xs.map!((x) => f(x));
680     auto ysnoise = xs.map!((x) => f(x) + uniform(-width(x),width(x))).array;
681 
682     auto gg = xs.zip(ysnoise)
683         .map!((a) => aes!("x", "y", "colour")(a[0], a[1], "a"))
684         .geomPoint
685         .putIn(GGPlotD());
686 
687     gg = xs.zip(ysfit).map!((a) => aes!("x", "y")(a[0], a[1])).geomLine.putIn(gg);
688 
689     //
690     auto ys2fit = xs.map!((x) => 1-f(x));
691     auto ys2noise = xs.map!((x) => 1-f(x) + uniform(-width(x),width(x))).array;
692 
693     gg = xs.zip(ys2fit).map!((a) => aes!("x", "y")(a[0], a[1]))
694         .geomLine
695         .putIn(gg);
696     gg = xs.zip(ys2noise)
697         .map!((a) => aes!("x", "y", "colour")(a[0], a[1], "b"))
698         .geomPoint
699         .putIn(gg);
700 
701     gg.save( "noise.png" );
702 }
703 
704 ///
705 unittest
706 {
707     // http://blackedder.github.io/ggplotd/images/hist.png
708     import std.array : array;
709     import std.algorithm : map;
710     import std.range : iota, zip;
711     import std.random : uniform;
712 
713     import ggplotd.aes : aes;
714     import ggplotd.geom : geomHist, geomPoint;
715     import ggplotd.range : mergeRange;
716 
717     auto xs = iota(0,25,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
718     auto gg = xs
719         .map!((a) => aes!("x")(a))
720         .geomHist
721         .putIn(GGPlotD());
722 
723     gg = xs.map!((a) => aes!("x", "y")(a, 0.0))
724         .geomPoint
725         .putIn(gg);
726 
727     gg.save( "hist.png" );
728 }
729 
730 /// Setting background colour
731 unittest
732 {
733     /// http://blackedder.github.io/ggplotd/images/background.svg
734     import std.range : zip;
735     import std.algorithm : map;
736     import ggplotd.aes : aes;
737     import ggplotd.theme : background;
738     import ggplotd.geom : geomPoint;
739 
740     // http://blackedder.github.io/ggplotd/images/polygon.png
741     auto gg = zip([1, 0, 0.0], [1, 1, 0.0], [1, 0.1, 0])
742         .map!((a) => aes!("x", "y", "colour")(a[0], a[1], a[2]))
743         .geomPoint
744         .putIn(GGPlotD());
745     gg.put(background(RGBA(0.7, 0.7, 0.7, 1)));
746     gg.save( "background.svg" );
747 }
748 
749 /// Other data type
750 unittest
751 {
752     /// http://blackedder.github.io/ggplotd/images/data.png
753     import std.array : array;
754     import std.math : sqrt;
755     import std.algorithm : map;
756     import std.range : iota;
757     import std.random : uniform;
758 
759     import ggplotd.geom : geomPoint;
760 
761     struct Point { double x; double y; }
762     // Generate some noisy data with reducing width
763     auto f = (double x) { return x/(1+x); };
764     auto width = (double x) { return sqrt(0.1/(1+x)); };
765     immutable xs = iota( 0, 10, 0.1 ).array;
766 
767     auto points = xs.map!((x) => Point(x,
768         f(x) + uniform(-width(x),width(x))));
769 
770     auto gg = GGPlotD().put( geomPoint( points ) );
771 
772     gg.save( "data.png" );
773 }
774 
775 import std.range : ElementType;
776 
777 /**
778 Put an element into a plot/facets struct
779 
780 This basically reverses a call to put and allows one to write more idiomatic D code where code flows from left to right instead of right to left.
781 
782 Examples:
783 --------------------
784 auto gg = data.aes.geomPoint.putIn(GGPlotD());
785 // instead of
786 auto gg = GGPlotD().put(geomPoint(aes(data)));
787 --------------------
788 */
789 ref auto putIn(T, U)(T t, U u)
790 {
791     return u.put(t);
792 }
793 
794 /**
795 Plot multiple (sub) plots
796 */
797 struct Facets
798 {
799     ///
800     ref Facets put(GGPlotD facet) return
801     {
802         ggs.put( facet );
803         return this;
804     }
805 
806     ///
807     auto drawToSurface( ref cairo.Surface surface, int dimX, int dimY,
808             int width, int height ) const
809     {
810         import std.conv : to;
811         import std.math : floor;
812         import std.range : save, empty, front, popFront;
813         import cairo.cairo : Rectangle;
814         int w = floor( width.to!double/dimX ).to!int;
815         int h = floor( height.to!double/dimY ).to!int;
816 
817         auto gs = ggs.data.save;
818         foreach( i; 0..dimX )
819         {
820             foreach( j; 0..dimY )
821             {
822                 if (!gs.empty)
823                 {
824                     auto rect = Rectangle!double( w*i, h*j, w, h );
825                     auto subS = cairo.Surface.createForRectangle( surface, rect );
826                     gs.front.drawToSurface( subS, w, h ),
827                     gs.popFront;
828                 }
829             }
830         }
831 
832         return surface;
833     }
834 
835     ///
836     auto drawToSurface( ref cairo.Surface surface,
837             int width, int height ) const
838     {
839         import std.conv : to;
840         // Calculate dimX/dimY from width/height
841         auto grid = gridLayout( ggs.data.length, width.to!double/height );
842         return drawToSurface( surface, grid[0], grid[1], width, height );
843     }
844 
845 
846     ///
847     void save( string fname, int dimX, int dimY, int width = 470, int height = 470 ) const
848     {
849         bool pngWrite = false;
850         auto surface = createEmptySurface( fname, width, height,
851             RGBA(1,1,1,1) );
852 
853         surface = drawToSurface( surface, dimX, dimY, width, height );
854 
855         if (fname[$ - 3 .. $] == "png")
856         {
857             pngWrite = true;
858         }
859 
860         if (pngWrite)
861             (cast(cairo.ImageSurface)(surface)).writeToPNG(fname);
862     }
863 
864     ///
865     void save( string fname, int width = 470, int height = 470 ) const
866     {
867         import std.conv : to;
868         // Calculate dimX/dimY from width/height
869         auto grid = gridLayout( ggs.data.length, width.to!double/height );
870         save( fname, grid[0], grid[1], width, height );
871     }
872 
873     import std.range : Appender;
874 
875     Appender!(GGPlotD[]) ggs;
876  }
877 
878 auto gridLayout( size_t length, double ratio )
879 {
880     import std.conv : to;
881     import std.math : ceil, sqrt;
882     import std.typecons : Tuple;
883     auto h = ceil( sqrt(length/ratio) );
884     auto w = ceil(length/h);
885     return Tuple!(int, int)( w.to!int, h.to!int );
886 }
887 
888 unittest
889 {
890     import std.typecons : Tuple;
891     assertEqual(gridLayout(4, 1), Tuple!(int, int)(2, 2));
892     assertEqual(gridLayout(2, 1), Tuple!(int, int)(1, 2));
893     assertEqual(gridLayout(3, 1), Tuple!(int, int)(2, 2));
894     assertEqual(gridLayout(2, 2), Tuple!(int, int)(2, 1));
895 }
896 
897 ///
898 unittest
899 {
900     // Drawing different shapes
901     import ggplotd.aes : aes, Pixel;
902     import ggplotd.axes : xaxisRange, yaxisRange;
903     import ggplotd.geom : geomDiamond, geomRectangle;
904 
905     auto gg = GGPlotD();
906 
907     auto aes1 = [aes!("x", "y", "width", "height")(1.0, -1.0, 3.0, 5.0)];
908     gg.put( geomDiamond( aes1 ) );
909     gg.put( geomRectangle( aes1 ) );
910     gg.put( xaxisRange( -5, 11.0 ) );
911     gg.put( yaxisRange( -9, 9.0 ) );
912 
913 
914     auto aes2 = [aes!("x", "y", "width", "height")(8.0, 5.0, Pixel(10), Pixel(20))];
915     gg.put( geomDiamond( aes2 ) );
916     gg.put( geomRectangle( aes2 ) );
917 
918     auto aes3 = [aes!("x", "y", "width", "height")(6.0, -5.0, Pixel(25), Pixel(25))];
919     gg.put( geomDiamond( aes3 ) );
920     gg.put( geomRectangle( aes3 ) );
921 
922     gg.save( "shapes1.png", 300, 300 );
923 }
924 
925 ///
926 unittest
927 {
928     // Drawing different shapes
929     import ggplotd.aes : aes, Pixel;
930     import ggplotd.axes : xaxisRange, yaxisRange;
931 
932     import ggplotd.geom : geomEllipse, geomTriangle;
933 
934     auto gg = GGPlotD();
935 
936     auto aes1 = [aes!("x", "y", "width", "height")( 1.0, -1.0, 3.0, 5.0 )];
937     gg.put( geomEllipse( aes1 ) );
938     gg.put( geomTriangle( aes1 ) );
939     gg.put( xaxisRange( -5, 11.0 ) );
940     gg.put( yaxisRange( -9, 9.0 ) );
941 
942 
943     auto aes2 = [aes!("x", "y", "width", "height")(8.0, 5.0, Pixel(10), Pixel(20))];
944     gg.put( geomEllipse( aes2 ) );
945     gg.put( geomTriangle( aes2 ) );
946 
947     auto aes3 = [aes!("x", "y", "width", "height")( 6.0, -5.0, Pixel(25), Pixel(25))];
948     gg.put( geomEllipse( aes3 ) );
949     gg.put( geomTriangle( aes3 ) );
950 
951     gg.save( "shapes2.png", 300, 300 );
952 }
953 
954 unittest
955 {
956     import std.typecons;
957 
958     void testTwoClassPlot(Tuple!(int, string)[] data, string fileName, string titleName) {
959         import ggplotd.aes : aes;
960         import ggplotd.geom;
961         import ggplotd.ggplotd : putIn, GGPlotD, title, Margins;
962         import ggplotd.legend: discreteLegend;
963         import std.algorithm: map;
964         auto gg = data
965             .map!(a => aes!("x", "colour", "fill")(a[0], a[1], 0.45))
966             .geomHist
967             .putIn(GGPlotD().put(title(titleName)));
968         gg.put(discreteLegend);
969         gg.save(fileName~".svg");
970     }
971 
972     import std.array;
973     import std.range : chain, zip, repeat;
974     import std.random : uniform;
975     import std.range : generate, take;
976     auto class1 = generate!(() => uniform(0, 20)).take(1000).array;
977     auto class2 = generate!(() => uniform(0, 20)).take(1000).array;
978     auto class12 = class1.chain(class2);
979     auto class12Labels = "A".repeat(class1.length).chain("B".repeat(class2.length));
980     auto plotData = class12.zip(class12Labels).array;
981     testTwoClassPlot(plotData, "issue_63", "Fake data");
982 }