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.aes;
9 import ggplotd.axes;
10 import ggplotd.colour;
11 import ggplotd.geom;
12 import ggplotd.bounds;
13 import ggplotd.scale;
14 import ggplotd.theme;
15 
16 version (unittest)
17 {
18     import dunit.toolkit;
19 }
20 
21 alias TitleFunction = Title delegate(Title);
22 
23 // Currently only holds the title. In the future could also be used to store details on location etc.
24 struct Title
25 {
26     /// The actual title
27     string title;
28 }
29 
30 ///
31 TitleFunction title( string title )
32 {
33     return delegate(Title t) { t.title = title; return t; };
34 }
35 
36 private auto createEmptySurface( string fname, int width, int height,
37     RGBA colour )
38 {
39     cairo.Surface surface;
40 
41     static if (cconfig.CAIRO_HAS_PDF_SURFACE)
42         {
43         if (fname[$ - 3 .. $] == "pdf")
44             {
45             surface = new cpdf.PDFSurface(fname, width, height);
46         }
47     }
48     else
49         {
50         if (fname[$ - 3 .. $] == "pdf")
51             assert(0, "PDF support not enabled by cairoD");
52     }
53     static if (cconfig.CAIRO_HAS_SVG_SURFACE)
54         {
55         if (fname[$ - 3 .. $] == "svg")
56             {
57             surface = new csvg.SVGSurface(fname, width, height);
58         }
59     }
60     else
61     {
62         if (fname[$ - 3 .. $] == "svg")
63             assert(0, "SVG support not enabled by cairoD");
64     }
65     if (fname[$ - 3 .. $] == "png")
66     {
67         surface = new cairo.ImageSurface(cairo.Format.CAIRO_FORMAT_ARGB32, width, height);
68     }
69 
70     auto backcontext = cairo.Context(surface);
71     backcontext.setSourceRGBA(colour);
72     backcontext.paint;
73 
74     return surface;
75 }
76 
77 ///
78 auto drawTitle( in Title title, ref cairo.Surface surface,
79     in Margins margins, int width, int height )
80 {
81     auto context = cairo.Context(surface);
82     context.setFontSize(16.0);
83     context.moveTo( width/2, margins.top/2 );
84     auto extents = context.textExtents(title.title);
85 
86     auto textSize = cairo.Point!double(0.5 * extents.width, 0.5 * extents.height);
87     context.relMoveTo(-textSize.x, textSize.y);
88 
89     context.showText(title.title);
90     return surface;
91 }
92 
93 auto drawGeom( in Geom geom, ref cairo.Surface surface,
94     in ColourMap colourMap, in ScaleType scaleFunction, in Bounds bounds, 
95     in Margins margins, int width, int height )
96 {
97     cairo.Context context;
98     if (geom.mask) {
99         auto plotSurface = cairo.Surface.createForRectangle(surface,
100             cairo.Rectangle!double(margins.left, margins.top,
101             width - (margins.left+margins.right), 
102             height - (margins.top+margins.bottom)));
103         context = cairo.Context(plotSurface);
104     } else {
105         context = cairo.Context(surface);
106         context.translate(margins.left, margins.top);
107     }
108     import std.conv : to;
109     context = scaleFunction(context, bounds,
110         width.to!double - (margins.left+margins.right),
111         height.to!double - (margins.top+margins.bottom));
112     context = geom.draw(context, colourMap);
113     return surface;
114 }
115 
116 ///
117 struct Margins
118 {
119     size_t left = 50; ///
120     size_t right = 20; ///
121     size_t bottom = 50; ///
122     size_t top = 40; ///
123 }
124 
125 ///
126 struct GGPlotD
127 {
128     Geom[] geomRange;
129 
130     XAxis xaxis;
131     YAxis yaxis;
132 
133     Margins margins;
134 
135     Title title;
136     Theme theme;
137 
138     ScaleType scaleFunction;
139 
140     ///
141     auto drawToSurface( ref cairo.Surface surface, int width, int height ) const
142     {
143         import std.range : empty, front;
144 
145         AdaptiveBounds bounds;
146         ColourID[] colourIDs;
147         Tuple!(double, string)[] xAxisTicks;
148         Tuple!(double, string)[] yAxisTicks;
149 
150         foreach (geom; geomRange)
151         {
152             bounds.adapt(geom.bounds);
153             colourIDs ~= geom.colours;
154             xAxisTicks ~= geom.xTickLabels;
155             yAxisTicks ~= geom.yTickLabels;
156         }
157 
158         auto colourMap = createColourMap(colourIDs);
159 
160         // Axis
161         import std.algorithm : sort, uniq, min, max;
162         import std.range : chain;
163         import std.array : array;
164         import ggplotd.axes;
165 
166         // If ticks are provided then we make sure the bounds include them
167         auto sortedTicks = xAxisTicks.sort().uniq.array;
168         if (!sortedTicks.empty)
169         {
170             bounds.min_x = min( bounds.min_x, sortedTicks[0][0] );
171             bounds.max_x = max( bounds.max_x, sortedTicks[$-1][0] );
172         }
173         if (initialized(xaxis))
174         {
175             bounds.min_x = xaxis.min;
176             bounds.max_x = xaxis.max;
177         }
178 
179         import std.math : isNaN;
180         auto offset = bounds.min_y;
181         if (!isNaN(xaxis.offset))
182             offset = xaxis.offset;
183         auto aesX = axisAes("x", bounds.min_x, bounds.max_x, offset,
184             sortedTicks );
185 
186         sortedTicks = yAxisTicks.sort().uniq.array;
187         if (!sortedTicks.empty)
188         {
189             bounds.min_y = min( bounds.min_y, sortedTicks[0][0] );
190             bounds.max_y = max( bounds.max_y, sortedTicks[$-1][0] );
191         }
192         if (initialized(yaxis))
193         {
194             bounds.min_y = yaxis.min;
195             bounds.max_y = yaxis.max;
196         }
197 
198         offset = bounds.min_x;
199         if (!isNaN(yaxis.offset))
200             offset = yaxis.offset;
201         auto aesY = axisAes("y", bounds.min_y, bounds.max_y, offset,
202             sortedTicks );
203 
204         auto gR = chain(geomAxis(aesX, 10.0*bounds.height / height, xaxis.label), geomAxis(aesY, 10.0*bounds.width / width, yaxis.label));
205 
206         // Plot axis and geomRange
207         foreach (geom; chain(geomRange, gR) )
208         {
209             if (initScale)
210                 surface = geom.drawGeom( surface,
211                     colourMap, scaleFunction, bounds, 
212                     margins, width, height );
213             else 
214                 surface = geom.drawGeom( surface,
215                     colourMap, scale(), bounds, 
216                     margins, width, height );
217          }
218 
219         // Plot title
220         surface = title.drawTitle( surface, margins, width, height );
221         return surface;
222     }
223  
224 
225     ///
226     void save( string fname, int width = 470, int height = 470 ) const
227     {
228         bool pngWrite = false;
229         auto surface = createEmptySurface( fname, width, height,
230             theme.backgroundColour );
231 
232         surface = drawToSurface( surface, width, height );
233 
234         if (fname[$ - 3 .. $] == "png")
235         {
236             pngWrite = true;
237         }
238 
239         if (pngWrite)
240             (cast(cairo.ImageSurface)(surface)).writeToPNG(fname);
241     }
242 
243     /// Using + to extend the plot for compatibility to ggplot2 in R
244     ref GGPlotD opBinary(string op, T)(T rhs) if (op == "+")
245     {
246         static if (is(ElementType!T==Geom))
247         {
248             import std.array : array;
249             geomRange ~= rhs.array;
250         }
251         static if (is(T==ScaleType))
252         {
253             initScale = true;
254             scaleFunction = rhs;
255         }
256         static if (is(T==XAxisFunction))
257         {
258             xaxis = rhs( xaxis );
259         }
260         static if (is(T==YAxisFunction))
261         {
262             yaxis = rhs( yaxis );
263         }
264         static if (is(T==TitleFunction))
265         {
266             title = rhs( title );
267         }
268         static if (is(T==ThemeFunction))
269         {
270             theme = rhs( theme );
271         }
272         static if (is(T==Margins))
273         {
274             margins = rhs;
275         }
276         return this;
277     }
278 
279     ///
280     ref GGPlotD put(T)(T rhs)
281     {
282         return this.opBinary!("+", T)(rhs);
283     }
284 
285 private:
286     bool initScale = false;
287 }
288 
289 unittest
290 {
291     auto gg = GGPlotD()
292         .put( yaxisLabel( "My ylabel" ) )
293         .put( yaxisRange( 0, 2.0 ) );
294     assertEqual( gg.yaxis.max, 2.0 );
295     assertEqual( gg.yaxis.label, "My ylabel" );
296 
297     gg = GGPlotD(); 
298     gg.put( yaxisLabel( "My ylabel" ) )
299         .put( yaxisRange( 0, 2.0 ) );
300     assertEqual( gg.yaxis.max, 2.0 );
301     assertEqual( gg.yaxis.label, "My ylabel" );
302 }
303 
304 
305 ///
306 unittest
307 {
308     auto aes = Aes!(string[], "x", string[], "y", string[], "colour")(["a",
309         "b", "c", "b"], ["x", "y", "y", "x"], ["b", "b", "b", "b"]);
310     auto gg = GGPlotD();
311     gg + geomLine(aes) + scale();
312     gg.save( "test6.png");
313 }
314 
315 ///
316 unittest
317 {
318     /// http://blackedder.github.io/ggplotd/images/noise.png
319     import std.array : array;
320     import std.math : sqrt;
321     import std.algorithm : map;
322     import std.range : repeat, iota;
323     import std.random : uniform;
324     // Generate some noisy data with reducing width
325     auto f = (double x) { return x/(1+x); };
326     auto width = (double x) { return sqrt(0.1/(1+x)); };
327     auto xs = iota( 0, 10, 0.1 ).array;
328 
329     auto ysfit = xs.map!((x) => f(x));
330     auto ysnoise = xs.map!((x) => f(x) + uniform(-width(x),width(x))).array;
331 
332     auto aes = Aes!(typeof(xs), "x",
333         typeof(ysnoise), "y", string[], "colour" )( xs, ysnoise, ("a").repeat(xs.length).array );
334     auto gg = GGPlotD().put( geomPoint( aes ) );
335     gg.put( geomLine( Aes!(typeof(xs), "x",
336         typeof(ysfit), "y" )( xs, ysfit ) ) );
337 
338     //  
339     auto ys2fit = xs.map!((x) => 1-f(x));
340     auto ys2noise = xs.map!((x) => 1-f(x) + uniform(-width(x),width(x))).array;
341 
342     gg.put( geomLine( Aes!(typeof(xs), "x", typeof(ys2fit), "y" )( xs,
343         ys2fit) ) )
344         .put(
345             geomPoint( Aes!(typeof(xs), "x", typeof(ys2noise), "y", string[],
346         "colour" )( xs, ys2noise, ("b").repeat(xs.length).array) ) );
347 
348     gg.save( "noise.png" );
349 }
350 
351 ///
352 unittest
353 {
354     /// http://blackedder.github.io/ggplotd/images/hist.png
355     import std.array : array;
356     import std.algorithm : map;
357     import std.range : repeat, iota;
358     import std.random : uniform;
359     auto xs = iota(0,25,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
360     auto aes = Aes!(typeof(xs), "x")( xs );
361     auto gg = GGPlotD().put( geomHist( aes ) );
362 
363     auto ys = (0.0).repeat( xs.length ).array;
364     auto aesPs = aes.mergeRange( Aes!(double[], "y", double[], "colour" )
365         ( ys, ys ) );
366     gg.put( geomPoint( aesPs ) );
367 
368     gg.save( "hist.png" );
369 }
370 
371 ///
372 unittest
373 {
374     /// http://blackedder.github.io/ggplotd/images/filled_hist.svg
375     import std.array : array;
376     import std.algorithm : map;
377     import std.range : repeat, iota, chain;
378     import std.random : uniform;
379     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
380     auto cols = "a".repeat(25).chain("b".repeat(25));
381     auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 
382         double[], "fill" )( 
383             xs, cols, 0.45.repeat(xs.length).array);
384     auto gg = GGPlotD().put( geomHist( aes ) );
385     gg.save( "filled_hist.svg" );
386 }
387 
388 /// Boxplot example
389 unittest
390 {
391     /// http://blackedder.github.io/ggplotd/images/boxplot.svg
392     import std.array : array;
393     import std.algorithm : map;
394     import std.range : repeat, iota, chain;
395     import std.random : uniform;
396     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
397     auto cols = "a".repeat(25).chain("b".repeat(25)).array;
398     auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 
399         double[], "fill", typeof(cols), "label" )( 
400             xs, cols, 0.45.repeat(xs.length).array, cols);
401     auto gg = GGPlotD().put( geomBox( aes ) );
402     gg.save( "boxplot.svg" );
403 }
404 
405 ///
406 unittest
407 {
408     /// http://blackedder.github.io/ggplotd/images/hist3D.svg
409     import std.array : array;
410     import std.algorithm : map;
411     import std.range : repeat, iota;
412     import std.random : uniform;
413 
414     auto xs = iota(0,100,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
415     auto ys = iota(0,100,1).map!((y) => uniform(0.0,5)+uniform(0.0,5)).array;
416     auto aes = Aes!(typeof(xs), "x", typeof(ys), "y")( xs, ys);
417     auto gg = GGPlotD().put( geomHist3D( aes ) );
418 
419     gg.save( "hist3D.svg" );
420 }
421 
422 
423 
424 /// Changing axes details
425 unittest
426 {
427     /// http://blackedder.github.io/ggplotd/images/axes.svg
428     import std.array : array;
429     import std.math : sqrt;
430     import std.algorithm : map;
431     import std.range : iota;
432     // Generate some noisy data with reducing width
433     auto f = (double x) { return x/(1+x); };
434     auto width = (double x) { return sqrt(0.1/(1+x)); };
435     auto xs = iota( 0, 10, 0.1 ).array;
436 
437     auto ysfit = xs.map!((x) => f(x)).array;
438 
439     auto gg = GGPlotD().put( geomLine( Aes!(typeof(xs), "x",
440         typeof(ysfit), "y" )( xs, ysfit ) ) );
441 
442     // Setting range and label for xaxis
443     gg.put( xaxisRange( 0, 8 ) ).put( xaxisLabel( "My xlabel" ) );
444     assertEqual( gg.xaxis.min, 0 );
445     // Setting range and label for yaxis
446     gg.put( yaxisRange( 0, 2.0 ) ).put( yaxisLabel( "My ylabel" ) );
447     assertEqual( gg.yaxis.max, 2.0 );
448     assertEqual( gg.yaxis.label, "My ylabel" );
449 
450     // change offset
451     gg.put( xaxisOffset( 0.25 ) ).put( yaxisOffset( 0.5 ) );
452 
453     // Change Margins
454     gg.put( Margins( 60, 60, 40, 30 ) );
455 
456     // Set a title
457     gg.put( title( "And now for something completely different" ) );
458     assertEqual( gg.title.title, "And now for something completely different" );
459 
460     // Saving on a 500x300 pixel surface
461     gg.save( "axes.svg", 500, 300 );
462 }
463 
464 /// Polygon
465 unittest
466 {
467     /// http://blackedder.github.io/ggplotd/images/polygon.png
468     auto gg = GGPlotD().put( geomPolygon( 
469         Aes!(
470             double[], "x",
471             double[], "y",
472             double[], "colour" )(
473             [1,0,0], [ 1, 1, 0 ], [1,0.1,0] ) ) );
474     gg.save( "polygon.png" );
475 }
476 
477 /// Setting background colour
478 unittest
479 {
480     /// http://blackedder.github.io/ggplotd/images/background.svg
481     import ggplotd.theme;
482     auto gg = GGPlotD().put( background( RGBA(0.7,0.7,0.7,1) ) );
483     gg.put( geomPoint( 
484         Aes!(
485             double[], "x",
486             double[], "y",
487             double[], "colour" )(
488             [1,0,0], [ 1, 1, 0 ], [1,0.1,0] ) ) );
489     gg.save( "background.svg" );
490 }
491 
492 /// Other data type
493 unittest
494 {
495     /// http://blackedder.github.io/ggplotd/images/data.png
496     import std.array : array;
497     import std.math : sqrt;
498     import std.algorithm : map;
499     import std.range : repeat, iota;
500     import std.random : uniform;
501     struct Point { double x; double y; }
502     // Generate some noisy data with reducing width
503     auto f = (double x) { return x/(1+x); };
504     auto width = (double x) { return sqrt(0.1/(1+x)); };
505     auto xs = iota( 0, 10, 0.1 ).array;
506 
507     auto points = xs.map!((x) => Point(x,
508         f(x) + uniform(-width(x),width(x))));
509 
510     auto gg = GGPlotD().put( geomPoint( points ) );
511 
512     gg.save( "data.png" );
513 }
514 
515 
516 ///
517 struct Facets
518 {
519     ///
520     void put(GGPlotD facet)
521     {
522         ggs.put( facet );
523     }
524 
525     ///
526     auto drawToSurface( ref cairo.Surface surface, int dimX, int dimY, 
527             int width, int height ) const
528     {
529         import std.conv : to;
530         import std.math : floor;
531         import std.range : save;
532         import cairo.cairo : Rectangle;
533         int w = floor( width.to!double/dimX ).to!int;
534         int h = floor( height.to!double/dimY ).to!int;
535 
536         auto gs = ggs.data.save;
537         foreach( i; 0..dimX )
538         {
539             foreach( j; 0..dimY )
540             {
541                 if (!gs.empty) 
542                 {
543                     auto rect = Rectangle!double( w*i, h*j, w, h );
544                     auto subS = cairo.Surface.createForRectangle( surface, rect );
545                     gs.front.drawToSurface( subS, w, h ),
546                     gs.popFront;
547                 }
548             }
549         }
550 
551         return surface;
552     }
553 
554     ///
555     auto drawToSurface( ref cairo.Surface surface,
556             int width, int height ) const
557     {
558         import std.conv : to;
559         // Calculate dimX/dimY from width/height
560         auto grid = gridLayout( ggs.data.length, width.to!double/height );
561         return drawToSurface( surface, grid[0], grid[1], width, height );
562     }
563  
564  
565     ///
566     void save( string fname, int dimX, int dimY, int width = 470, int height = 470 ) const
567     {
568         import cairo.cairo : RGBA;
569 
570         bool pngWrite = false;
571         auto surface = createEmptySurface( fname, width, height,
572             RGBA(1,1,1,1) );
573 
574         surface = drawToSurface( surface, dimX, dimY, width, height );
575 
576         if (fname[$ - 3 .. $] == "png")
577         {
578             pngWrite = true;
579         }
580 
581         if (pngWrite)
582             (cast(cairo.ImageSurface)(surface)).writeToPNG(fname);
583     }
584 
585     ///
586     void save( string fname, int width = 470, int height = 470 ) const
587     {
588         import std.conv : to;
589         // Calculate dimX/dimY from width/height
590         auto grid = gridLayout( ggs.data.length, width.to!double/height );
591         save( fname, grid[0], grid[1], width, height );
592     }
593 
594     import std.range : Appender;
595 
596     Appender!(GGPlotD[]) ggs;
597  }
598 
599 auto gridLayout( size_t length, double ratio )
600 {
601     import std.conv : to;
602     import std.math : ceil, sqrt;
603     auto h = ceil( sqrt(length/ratio) );
604     auto w = ceil(length/h);
605     return Tuple!(int, int)( w.to!int, h.to!int );
606 }
607 
608 unittest
609 {
610     assertEqual(gridLayout(4, 1), Tuple!(int, int)(2, 2));
611     assertEqual(gridLayout(2, 1), Tuple!(int, int)(1, 2));
612     assertEqual(gridLayout(3, 1), Tuple!(int, int)(2, 2));
613     assertEqual(gridLayout(2, 2), Tuple!(int, int)(2, 1));
614 }