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(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 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         AdaptiveBounds bounds;
250         bounds.adapt(xStore.min(), yStore.min());
251         bounds.adapt(xStore.max(), yStore.max());
252 
253         import std.algorithm : map;
254         import std.array : array;
255         import std.typecons : tuple;
256         if (xStore.hasDiscrete)
257             xAxisTicks = xStore
258                 .storeHash
259                 .byKeyValue()
260                 .map!((kv) => tuple(kv.value, kv.key))
261                 .array;
262         if (yStore.hasDiscrete)
263             yAxisTicks = yStore
264                 .storeHash
265                 .byKeyValue()
266                 .map!((kv) => tuple(kv.value, kv.key))
267                 .array;
268 
269         // Axis
270         import std.algorithm : sort, uniq, min, max;
271         import std.range : chain;
272         import std.array : array;
273 
274         import ggplotd.axes : initialized, axisAes;
275 
276         // TODO move this out of here and add some tests
277         // If ticks are provided then we make sure the bounds include them
278         auto xSortedTicks = xAxisTicks.sort().uniq.array;
279         if (!xSortedTicks.empty)
280         {
281             bounds.min_x = min( bounds.min_x, xSortedTicks[0][0] );
282             bounds.max_x = max( bounds.max_x, xSortedTicks[$-1][0] );
283         }
284         if (initialized(xaxis))
285         {
286             bounds.min_x = xaxis.min;
287             bounds.max_x = xaxis.max;
288         }
289 
290         // This needs to happen before the offset of x axis is set
291         auto ySortedTicks = yAxisTicks.sort().uniq.array;
292         if (!ySortedTicks.empty)
293         {
294             bounds.min_y = min( bounds.min_y, ySortedTicks[0][0] );
295             bounds.max_y = max( bounds.max_y, ySortedTicks[$-1][0] );
296         }
297         if (initialized(yaxis))
298         {
299             bounds.min_y = yaxis.min;
300             bounds.max_y = yaxis.max;
301         }
302 
303         import std.math : isNaN;
304         auto offset = bounds.min_y;
305         if (!isNaN(xaxis.offset))
306             offset = xaxis.offset;
307         if (!xaxis.show) // Trixk to draw the axis off screen if it is hidden
308             offset = yaxis.min - bounds.height;
309 
310         // TODO: Should really take separate scaling for number of ticks (defaultScaling(width)) 
311         // and for font: defaultScaling(widht, height)
312         auto aesX = axisAes("x", bounds.min_x, bounds.max_x, offset, defaultScaling(width, height),
313             xSortedTicks );
314 
315         offset = bounds.min_x;
316         if (!isNaN(yaxis.offset))
317             offset = yaxis.offset;
318         if (!yaxis.show) // Trixk to draw the axis off screen if it is hidden
319             offset = xaxis.min - bounds.width;
320         auto aesY = axisAes("y", bounds.min_y, bounds.max_y, offset, defaultScaling(height, width),
321             ySortedTicks );
322 
323         import ggplotd.geom : geomAxis;
324         import ggplotd.axes : tickLength;
325 
326         auto currentMargins = margins(width, height);
327 
328         auto gR = chain(
329                 geomAxis(aesX, 
330                     bounds.height.tickLength(height - currentMargins.bottom - currentMargins.top, 
331                         defaultScaling(width), defaultScaling(height)), xaxis.label), 
332                 geomAxis(aesY, 
333                     bounds.width.tickLength(width - currentMargins.left - currentMargins.right, 
334                         defaultScaling(width), defaultScaling(height)), yaxis.label), 
335             );
336         auto plotMargins = Margins(currentMargins);
337         if (!legends.empty)
338             plotMargins.right += legends[0].width;
339 
340         // Plot axis and geomRange
341         import ggplotd.guide : guideFunction;
342         auto xFunc = guideFunction(xStore);
343         auto yFunc = guideFunction(yStore);
344         auto cFunc = guideFunction(colourStore, this.colourGradient());
345         auto sFunc = guideFunction(sizeStore);
346 
347         foreach (geom; chain(geomRange.data, gR) )
348         {
349             surface = geom.drawGeom( surface,
350                 xFunc, yFunc, cFunc, sFunc,
351                 scale(), bounds, 
352                 plotMargins, width, height );
353         }
354 
355         // Plot title
356         surface = title.drawTitle( surface, currentMargins, width );
357 
358         import std.range : iota, zip, dropOne;
359         foreach(ly; zip(legends, iota(0.0, height, height/(legends.length+1.0)).dropOne)) 
360         {
361             auto legend = ly[0];
362             auto y = ly[1] - legend.height*.5;
363             if (legend.type == "continuous") {
364                 import ggplotd.legend : drawContinuousLegend; 
365                 auto legendSurface = cairo.Surface.createForRectangle(surface,
366                     cairo.Rectangle!double(width - currentMargins.right - legend.width, 
367                     y, legend.width, legend.height ));//margins.right, margins.right));
368                 legendSurface = drawContinuousLegend( legendSurface, 
369                 legend.width, legend.height, 
370                     colourStore, this.colourGradient );
371             } else if (legend.type == "discrete") {
372                 import ggplotd.legend : drawDiscreteLegend; 
373                 auto legendSurface = cairo.Surface.createForRectangle(surface,
374                     cairo.Rectangle!double(width - currentMargins.right - legend.width, 
375                     y, legend.width, legend.height ));//margins.right, margins.right));
376                 legendSurface = drawDiscreteLegend( legendSurface, 
377                 legend.width, legend.height, 
378                     colourStore, this.colourGradient );
379             }
380         }
381 
382         return surface;
383     }
384  
385     version(ggplotdGTK) 
386     {
387         import gtkdSurface = cairo.Surface; // cairo surface module in GtkD package.
388 
389         /**
390         Draw the plot to a GtkD cairo surface.
391 
392         Params:
393             surface = Surface object of type cairo.Surface from GtkD library, on top of which this plot is drawn.
394             width = Width of the given surface.
395             height = Height of the given surface.
396 
397         Returns:
398             Resulting surface of the same type as input surface, with this plot drawn on top of it.
399         */
400         auto drawToSurface( ref gtkdSurface.Surface surface, int width, int height ) const
401         {
402             import gtkc = gtkc.cairotypes;
403             import cairod = cairo.c.cairo;
404 
405             alias gtkd_surface_t = gtkc.cairo_surface_t;
406             alias cairod_surface_t = cairod.cairo_surface_t;
407 
408             cairo.Surface cairodSurface = new cairo.Surface(cast(cairod_surface_t*)surface.getSurfaceStruct());
409             drawToSurface(cairodSurface, width, height);
410 
411             return surface;
412         }
413     }
414 
415     /// save the plot to a file
416     void save( string fname, int width = 470, int height = 470 ) const
417     {
418         bool pngWrite = false;
419         auto surface = createEmptySurface( fname, width, height,
420             theme.backgroundColour );
421 
422         surface = drawToSurface( surface, width, height );
423 
424         if (fname[$ - 3 .. $] == "png")
425         {
426             pngWrite = true;
427         }
428 
429         if (pngWrite)
430             (cast(cairo.ImageSurface)(surface)).writeToPNG(fname);
431     }
432 
433     /// Using + to extend the plot for compatibility to ggplot2 in R
434     ref GGPlotD opBinary(string op, T)(T rhs) if (op == "+")
435     {
436         import ggplotd.axes : XAxisFunction, YAxisFunction;
437         import ggplotd.colour : ColourGradientFunction;
438         static if (is(ElementType!T==Geom))
439         {
440             geomRange.put( rhs );
441         }
442         static if (is(T==ScaleType))
443         {
444             scaleFunction = rhs;
445         }
446         static if (is(T==XAxisFunction))
447         {
448             xaxis = rhs( xaxis );
449         }
450         static if (is(T==YAxisFunction))
451         {
452             yaxis = rhs( yaxis );
453         }
454         static if (is(T==TitleFunction))
455         {
456             title = rhs( title );
457         }
458         static if (is(T==ThemeFunction))
459         {
460             theme = rhs( theme );
461         }
462         static if (is(T==Margins))
463         {
464             _margins = rhs;
465         }
466         static if (is(T==Legend))
467         {
468             legends ~= rhs;
469         }
470         static if (is(T==ColourGradientFunction)) {
471             colourGradientFunction = rhs;
472         }
473         return this;
474     }
475 /// put/add to the plot
476     ref GGPlotD put(T)(T rhs)
477     {
478         return this.opBinary!("+", T)(rhs);
479     }
480 
481     /// Active scale
482     ScaleType scale() const
483     {
484         import ggplotd.scale : defaultScale = scale;
485         // Return active function or the default
486         if (!scaleFunction.isNull)
487             return scaleFunction;
488         else 
489             return defaultScale();
490     }
491 
492     /// Active colourGradient
493     ColourGradientFunction colourGradient() const
494     {
495         import ggplotd.colour : defaultColourGradient = colourGradient;
496         import ggplotd.colourspace : HCY;
497         if (!colourGradientFunction.isNull)
498             return colourGradientFunction;
499         else
500             return defaultColourGradient!HCY("");
501     }
502 
503     /// Active margins
504     Margins margins(int width, int height) const
505     {
506         if (!_margins.isNull)
507             return _margins;
508         else
509             return defaultMargins(width, height);
510     }
511 
512 private:
513     import std.range : Appender;
514     import ggplotd.theme : Theme, ThemeFunction;
515     import ggplotd.legend : Legend;
516     Appender!(Geom[]) geomRange;
517 
518     import ggplotd.axes : XAxis, YAxis;
519     XAxis xaxis;
520     YAxis yaxis;
521 
522 
523     Title title;
524     Theme theme;
525 
526     import std.typecons : Nullable;
527     Nullable!(Margins) _margins;
528     Nullable!(ScaleType) scaleFunction;
529     Nullable!(ColourGradientFunction) colourGradientFunction;
530 
531     Legend[] legends;
532 }
533 
534 unittest
535 {
536     import std.range : zip;
537     import std.algorithm : map;
538     import ggplotd.geom;
539     import ggplotd.aes;
540 
541     const win_width = 1024;
542     const win_height = 1024;
543 
544     const radius = 400.;
545 
546 
547     auto gg = GGPlotD();
548     gg = zip([ 0, radius*0.45 ], [ 0, radius*0.45])
549         .map!((a) => aes!("x","y")(a[0], a[1]))
550         .geomLine.putIn(gg);
551     gg = zip([ 300, radius*0.45 ], [ 210, radius*0.45])
552         .map!((a) => aes!("x","y")(a[0], a[1]))
553         .geomLine.putIn(gg);
554 
555     import ggplotd.theme : Theme, ThemeFunction;
556     Theme theme;
557 
558     auto surface = createEmptySurface( "test.png", win_width, win_height,
559         theme.backgroundColour );
560 
561     auto dim = gg.geomRange.data.length;
562     surface = gg.drawToSurface( surface, win_width, win_height );
563     assertEqual( dim, gg.geomRange.data.length );
564     surface = gg.drawToSurface( surface, win_width, win_height );
565     assertEqual( dim, gg.geomRange.data.length );
566     surface = gg.drawToSurface( surface, win_width, win_height );
567     assertEqual( dim, gg.geomRange.data.length );
568 }
569 
570 version(ggplotdGTK) 
571 {
572     unittest 
573     {
574         import std.range : zip;
575         import std.algorithm : map;
576         // Draw same plot on cairod.ImageSurface, and on gtkd.cairo.ImageSurface,
577         // and prove resulting images are the same.
578 
579         import ggplotd.geom;
580         import ggplotd.aes;
581 
582         import gtkSurface = cairo.Surface;
583         import gtkImageSurface = cairo.ImageSurface;
584         import gtkCairoTypes = gtkc.cairotypes;
585 
586         const win_width = 1024;
587         const win_height = 1024;
588 
589         const radius = 400.;
590 
591         auto gg = GGPlotD();
592         gg = zip([ 0, radius*0.45 ], [ 0, radius*0.45])
593             .map!((a) => aes!("x","y")(a[0], a[1]))
594             .geomLine.putIn(gg);
595         gg = zip([ 300, radius*0.45 ], [ 210, radius*0.45])
596             .map!((a) => aes!("x","y")(a[0], a[1]))
597             .geomLine.putIn(gg);
598 
599         cairo.Surface cairodSurface = 
600             new cairo.ImageSurface(cairo.Format.CAIRO_FORMAT_RGB24, win_width, win_height);
601         gtkSurface.Surface gtkdSurface = 
602             gtkImageSurface.ImageSurface.create(gtkCairoTypes.cairo_format_t.RGB24, 
603                 win_width, win_height);
604 
605         auto cairodImageSurface = cast(cairo.ImageSurface)cairodSurface;
606         auto gtkdImageSurface = cast(gtkImageSurface.ImageSurface)gtkdSurface;
607 
608         gg.drawToSurface(cairodSurface, win_width, win_height);
609         gg.drawToSurface(gtkdSurface, win_width, win_height);
610 
611         auto byteSize = win_width*win_height*4;
612 
613         assertEqual(cairodImageSurface.getData()[0..byteSize], 
614             gtkdImageSurface.getData()[0..byteSize]);
615     }
616 }
617 
618 unittest
619 {
620     import ggplotd.axes : yaxisLabel, yaxisRange;
621     auto gg = GGPlotD()
622         .put( yaxisLabel( "My ylabel" ) )
623         .put( yaxisRange( 0, 2.0 ) );
624     assertEqual( gg.yaxis.max, 2.0 );
625     assertEqual( gg.yaxis.label, "My ylabel" );
626 
627     gg = GGPlotD(); 
628     gg.put( yaxisLabel( "My ylabel" ) )
629         .put( yaxisRange( 0, 2.0 ) );
630     assertEqual( gg.yaxis.max, 2.0 );
631     assertEqual( gg.yaxis.label, "My ylabel" );
632 }
633 
634 
635 ///
636 unittest
637 {
638     import std.range : zip;
639     import std.algorithm : map;
640 
641     import ggplotd.aes : aes;
642     import ggplotd.geom : geomLine;
643     import ggplotd.scale : scale;
644     auto gg = zip(["a", "b", "c", "b"], ["x", "y", "y", "x"], ["b", "b", "b", "b"])
645         .map!((a) => aes!("x", "y", "colour")(a[0], a[1], a[2]))
646         .geomLine
647         .putIn(GGPlotD());
648     gg + scale();
649     gg.save( "test6.png");
650 }
651 
652 ///
653 unittest
654 {
655     // http://blackedder.github.io/ggplotd/images/noise.png
656     import std.array : array;
657     import std.math : sqrt;
658     import std.algorithm : map;
659     import std.range : zip, iota;
660     import std.random : uniform;
661 
662     import ggplotd.aes : aes;
663     import ggplotd.geom : geomLine, geomPoint;
664     // Generate some noisy data with reducing width
665     auto f = (double x) { return x/(1+x); };
666     auto width = (double x) { return sqrt(0.1/(1+x)); };
667     auto xs = iota( 0, 10, 0.1 ).array;
668 
669     auto ysfit = xs.map!((x) => f(x));
670     auto ysnoise = xs.map!((x) => f(x) + uniform(-width(x),width(x))).array;
671 
672     auto gg = xs.zip(ysnoise)
673         .map!((a) => aes!("x", "y", "colour")(a[0], a[1], "a"))
674         .geomPoint
675         .putIn(GGPlotD());
676 
677     gg = xs.zip(ysfit).map!((a) => aes!("x", "y")(a[0], a[1])).geomLine.putIn(gg);
678 
679     //  
680     auto ys2fit = xs.map!((x) => 1-f(x));
681     auto ys2noise = xs.map!((x) => 1-f(x) + uniform(-width(x),width(x))).array;
682 
683     gg = xs.zip(ys2fit).map!((a) => aes!("x", "y")(a[0], a[1]))
684         .geomLine
685         .putIn(gg);
686     gg = xs.zip(ys2noise)
687         .map!((a) => aes!("x", "y", "colour")(a[0], a[1], "b"))
688         .geomPoint
689         .putIn(gg);
690 
691     gg.save( "noise.png" );
692 }
693 
694 ///
695 unittest
696 {
697     // http://blackedder.github.io/ggplotd/images/hist.png
698     import std.array : array;
699     import std.algorithm : map;
700     import std.range : iota, zip;
701     import std.random : uniform;
702 
703     import ggplotd.aes : aes;
704     import ggplotd.geom : geomHist, geomPoint;
705     import ggplotd.range : mergeRange;
706 
707     auto xs = iota(0,25,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
708     auto gg = xs 
709         .map!((a) => aes!("x")(a))
710         .geomHist
711         .putIn(GGPlotD());
712 
713     gg = xs.map!((a) => aes!("x", "y")(a, 0.0))
714         .geomPoint
715         .putIn(gg);
716 
717     gg.save( "hist.png" );
718 }
719 
720 /// Setting background colour
721 unittest
722 {
723     /// http://blackedder.github.io/ggplotd/images/background.svg
724     import std.range : zip;
725     import std.algorithm : map;
726     import ggplotd.aes : aes;
727     import ggplotd.theme : background;
728     import ggplotd.geom : geomPoint;
729 
730     // http://blackedder.github.io/ggplotd/images/polygon.png
731     auto gg = zip([1, 0, 0.0], [1, 1, 0.0], [1, 0.1, 0])
732         .map!((a) => aes!("x", "y", "colour")(a[0], a[1], a[2]))
733         .geomPoint
734         .putIn(GGPlotD());
735     gg.put(background(RGBA(0.7, 0.7, 0.7, 1)));
736     gg.save( "background.svg" );
737 }
738 
739 /// Other data type
740 unittest
741 {
742     /// http://blackedder.github.io/ggplotd/images/data.png
743     import std.array : array;
744     import std.math : sqrt;
745     import std.algorithm : map;
746     import std.range : iota;
747     import std.random : uniform;
748 
749     import ggplotd.geom : geomPoint;
750 
751     struct Point { double x; double y; }
752     // Generate some noisy data with reducing width
753     auto f = (double x) { return x/(1+x); };
754     auto width = (double x) { return sqrt(0.1/(1+x)); };
755     immutable xs = iota( 0, 10, 0.1 ).array;
756 
757     auto points = xs.map!((x) => Point(x,
758         f(x) + uniform(-width(x),width(x))));
759 
760     auto gg = GGPlotD().put( geomPoint( points ) );
761 
762     gg.save( "data.png" );
763 }
764 
765 import std.range : ElementType;
766 
767 /**
768 Put an element into a plot/facets struct
769 
770 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.
771 
772 Examples:
773 --------------------
774 auto gg = data.aes.geomPoint.putIn(GGPlotD());
775 // instead of
776 auto gg = GGPlotD().put(geomPoint(aes(data)));
777 --------------------
778 */
779 ref auto putIn(T, U)(T t, U u) 
780 {
781     return u.put(t);
782 }
783 
784 /**
785 Plot multiple (sub) plots
786 */
787 struct Facets
788 {
789     ///
790     ref Facets put(GGPlotD facet)
791     {
792         ggs.put( facet );
793         return this;
794     }
795 
796     ///
797     auto drawToSurface( ref cairo.Surface surface, int dimX, int dimY, 
798             int width, int height ) const
799     {
800         import std.conv : to;
801         import std.math : floor;
802         import std.range : save, empty, front, popFront;
803         import cairo.cairo : Rectangle;
804         int w = floor( width.to!double/dimX ).to!int;
805         int h = floor( height.to!double/dimY ).to!int;
806 
807         auto gs = ggs.data.save;
808         foreach( i; 0..dimX )
809         {
810             foreach( j; 0..dimY )
811             {
812                 if (!gs.empty) 
813                 {
814                     auto rect = Rectangle!double( w*i, h*j, w, h );
815                     auto subS = cairo.Surface.createForRectangle( surface, rect );
816                     gs.front.drawToSurface( subS, w, h ),
817                     gs.popFront;
818                 }
819             }
820         }
821 
822         return surface;
823     }
824 
825     ///
826     auto drawToSurface( ref cairo.Surface surface,
827             int width, int height ) const
828     {
829         import std.conv : to;
830         // Calculate dimX/dimY from width/height
831         auto grid = gridLayout( ggs.data.length, width.to!double/height );
832         return drawToSurface( surface, grid[0], grid[1], width, height );
833     }
834  
835  
836     ///
837     void save( string fname, int dimX, int dimY, int width = 470, int height = 470 ) const
838     {
839         bool pngWrite = false;
840         auto surface = createEmptySurface( fname, width, height,
841             RGBA(1,1,1,1) );
842 
843         surface = drawToSurface( surface, dimX, dimY, width, height );
844 
845         if (fname[$ - 3 .. $] == "png")
846         {
847             pngWrite = true;
848         }
849 
850         if (pngWrite)
851             (cast(cairo.ImageSurface)(surface)).writeToPNG(fname);
852     }
853 
854     ///
855     void save( string fname, int width = 470, int height = 470 ) const
856     {
857         import std.conv : to;
858         // Calculate dimX/dimY from width/height
859         auto grid = gridLayout( ggs.data.length, width.to!double/height );
860         save( fname, grid[0], grid[1], width, height );
861     }
862 
863     import std.range : Appender;
864 
865     Appender!(GGPlotD[]) ggs;
866  }
867 
868 auto gridLayout( size_t length, double ratio )
869 {
870     import std.conv : to;
871     import std.math : ceil, sqrt;
872     import std.typecons : Tuple;
873     auto h = ceil( sqrt(length/ratio) );
874     auto w = ceil(length/h);
875     return Tuple!(int, int)( w.to!int, h.to!int );
876 }
877 
878 unittest
879 {
880     import std.typecons : Tuple;
881     assertEqual(gridLayout(4, 1), Tuple!(int, int)(2, 2));
882     assertEqual(gridLayout(2, 1), Tuple!(int, int)(1, 2));
883     assertEqual(gridLayout(3, 1), Tuple!(int, int)(2, 2));
884     assertEqual(gridLayout(2, 2), Tuple!(int, int)(2, 1));
885 }
886 
887 ///
888 unittest
889 {
890     // Drawing different shapes
891     import ggplotd.aes : aes, Pixel;
892     import ggplotd.axes : xaxisRange, yaxisRange;
893     import ggplotd.geom : geomDiamond, geomRectangle;
894 
895     auto gg = GGPlotD();
896 
897     auto aes1 = [aes!("x", "y", "width", "height")(1.0, -1.0, 3.0, 5.0)];
898     gg.put( geomDiamond( aes1 ) );
899     gg.put( geomRectangle( aes1 ) );
900     gg.put( xaxisRange( -5, 11.0 ) );
901     gg.put( yaxisRange( -9, 9.0 ) );
902 
903 
904     auto aes2 = [aes!("x", "y", "width", "height")(8.0, 5.0, Pixel(10), Pixel(20))];
905     gg.put( geomDiamond( aes2 ) );
906     gg.put( geomRectangle( aes2 ) );
907 
908     auto aes3 = [aes!("x", "y", "width", "height")(6.0, -5.0, Pixel(25), Pixel(25))];
909     gg.put( geomDiamond( aes3 ) );
910     gg.put( geomRectangle( aes3 ) );
911  
912     gg.save( "shapes1.png", 300, 300 );
913 }
914 
915 ///
916 unittest
917 {
918     // Drawing different shapes
919     import ggplotd.aes : aes, Pixel;
920     import ggplotd.axes : xaxisRange, yaxisRange;
921 
922     import ggplotd.geom : geomEllipse, geomTriangle;
923 
924     auto gg = GGPlotD();
925 
926     auto aes1 = [aes!("x", "y", "width", "height")( 1.0, -1.0, 3.0, 5.0 )];
927     gg.put( geomEllipse( aes1 ) );
928     gg.put( geomTriangle( aes1 ) );
929     gg.put( xaxisRange( -5, 11.0 ) );
930     gg.put( yaxisRange( -9, 9.0 ) );
931 
932 
933     auto aes2 = [aes!("x", "y", "width", "height")(8.0, 5.0, Pixel(10), Pixel(20))];
934     gg.put( geomEllipse( aes2 ) );
935     gg.put( geomTriangle( aes2 ) );
936 
937     auto aes3 = [aes!("x", "y", "width", "height")( 6.0, -5.0, Pixel(25), Pixel(25))];
938     gg.put( geomEllipse( aes3 ) );
939     gg.put( geomTriangle( aes3 ) );
940  
941     gg.save( "shapes2.png", 300, 300 );
942 }