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( Geom geom, ref cairo.Surface surface,
94     ColourMap colourMap, 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 )
142     {
143         if (!initScale)
144             scaleFunction = scale(); // This needs to be removed later
145         import std.range : empty, front;
146 
147         AdaptiveBounds bounds;
148         ColourID[] colourIDs;
149         Tuple!(double, string)[] xAxisTicks;
150         Tuple!(double, string)[] yAxisTicks;
151 
152         foreach (geom; geomRange)
153         {
154             bounds.adapt(geom.bounds);
155             colourIDs ~= geom.colours;
156             xAxisTicks ~= geom.xTickLabels;
157             yAxisTicks ~= geom.yTickLabels;
158         }
159 
160         auto colourMap = createColourMap(colourIDs);
161 
162         // Axis
163         import std.algorithm : sort, uniq, min, max;
164         import std.range : chain;
165         import std.array : array;
166         import ggplotd.axes;
167 
168         // If ticks are provided then we make sure the bounds include them
169         auto sortedTicks = xAxisTicks.sort().uniq.array;
170         if (!sortedTicks.empty)
171         {
172             bounds.min_x = min( bounds.min_x, sortedTicks[0][0] );
173             bounds.max_x = max( bounds.max_x, sortedTicks[$-1][0] );
174         }
175         if (initialized(xaxis))
176         {
177             bounds.min_x = xaxis.min;
178             bounds.max_x = xaxis.max;
179         }
180 
181         import std.math : isNaN;
182         auto offset = bounds.min_y;
183         if (!isNaN(xaxis.offset))
184             offset = xaxis.offset;
185         auto aesX = axisAes("x", bounds.min_x, bounds.max_x, offset,
186             sortedTicks );
187 
188         sortedTicks = yAxisTicks.sort().uniq.array;
189         if (!sortedTicks.empty)
190         {
191             bounds.min_y = min( bounds.min_y, sortedTicks[0][0] );
192             bounds.max_y = max( bounds.max_y, sortedTicks[$-1][0] );
193         }
194         if (initialized(yaxis))
195         {
196             bounds.min_y = yaxis.min;
197             bounds.max_y = yaxis.max;
198         }
199 
200         offset = bounds.min_x;
201         if (!isNaN(yaxis.offset))
202             offset = yaxis.offset;
203         auto aesY = axisAes("y", bounds.min_y, bounds.max_y, offset,
204             sortedTicks );
205 
206         auto gR = chain(geomAxis(aesX, 10.0*bounds.height / height, xaxis.label), geomAxis(aesY, 10.0*bounds.width / width, yaxis.label));
207 
208         // Plot axis and geomRange
209         foreach (geom; chain(geomRange, gR) )
210         {
211             surface = geom.drawGeom( surface,
212                 colourMap, scaleFunction, bounds, 
213                 margins, width, height );
214         }
215 
216         // Plot title
217         surface = title.drawTitle( surface, margins, width, height );
218         return surface;
219     }
220  
221 
222     ///
223     void save( string fname, int width = 470, int height = 470 )
224     {
225         bool pngWrite = false;
226         auto surface = createEmptySurface( fname, width, height,
227             theme.backgroundColour );
228 
229         surface = drawToSurface( surface, width, height );
230 
231         if (fname[$ - 3 .. $] == "png")
232         {
233             pngWrite = true;
234         }
235 
236         if (pngWrite)
237             (cast(cairo.ImageSurface)(surface)).writeToPNG(fname);
238     }
239 
240     /// Using + to extend the plot for compatibility to ggplot2 in R
241     ref GGPlotD opBinary(string op, T)(T rhs) if (op == "+")
242     {
243         static if (is(ElementType!T==Geom))
244         {
245             import std.array : array;
246             geomRange ~= rhs.array;
247         }
248         static if (is(T==ScaleType))
249         {
250             initScale = true;
251             scaleFunction = rhs;
252         }
253         static if (is(T==XAxisFunction))
254         {
255             xaxis = rhs( xaxis );
256         }
257         static if (is(T==YAxisFunction))
258         {
259             yaxis = rhs( yaxis );
260         }
261         static if (is(T==TitleFunction))
262         {
263             title = rhs( title );
264         }
265         static if (is(T==ThemeFunction))
266         {
267             theme = rhs( theme );
268         }
269         static if (is(T==Margins))
270         {
271             margins = rhs;
272         }
273         return this;
274     }
275 
276     ///
277     ref GGPlotD put(T)(T rhs)
278     {
279         return this.opBinary!("+", T)(rhs);
280     }
281 
282 private:
283     bool initScale = false;
284 }
285 
286 unittest
287 {
288     auto gg = GGPlotD()
289         .put( yaxisLabel( "My ylabel" ) )
290         .put( yaxisRange( 0, 2.0 ) );
291     assertEqual( gg.yaxis.max, 2.0 );
292     assertEqual( gg.yaxis.label, "My ylabel" );
293 
294     gg = GGPlotD(); 
295     gg.put( yaxisLabel( "My ylabel" ) )
296         .put( yaxisRange( 0, 2.0 ) );
297     assertEqual( gg.yaxis.max, 2.0 );
298     assertEqual( gg.yaxis.label, "My ylabel" );
299 }
300 
301 
302 ///
303 unittest
304 {
305     auto aes = Aes!(string[], "x", string[], "y", string[], "colour")(["a",
306         "b", "c", "b"], ["x", "y", "y", "x"], ["b", "b", "b", "b"]);
307     auto gg = GGPlotD();
308     gg + geomLine(aes) + scale();
309     gg.save( "test6.png");
310 }
311 
312 ///
313 unittest
314 {
315     /// http://blackedder.github.io/ggplotd/images/noise.png
316     import std.array : array;
317     import std.math : sqrt;
318     import std.algorithm : map;
319     import std.range : repeat, iota;
320     import std.random : uniform;
321     // Generate some noisy data with reducing width
322     auto f = (double x) { return x/(1+x); };
323     auto width = (double x) { return sqrt(0.1/(1+x)); };
324     auto xs = iota( 0, 10, 0.1 ).array;
325 
326     auto ysfit = xs.map!((x) => f(x));
327     auto ysnoise = xs.map!((x) => f(x) + uniform(-width(x),width(x))).array;
328 
329     auto aes = Aes!(typeof(xs), "x",
330         typeof(ysnoise), "y", string[], "colour" )( xs, ysnoise, ("a").repeat(xs.length).array );
331     auto gg = GGPlotD().put( geomPoint( aes ) );
332     gg.put( geomLine( Aes!(typeof(xs), "x",
333         typeof(ysfit), "y" )( xs, ysfit ) ) );
334 
335     //  
336     auto ys2fit = xs.map!((x) => 1-f(x));
337     auto ys2noise = xs.map!((x) => 1-f(x) + uniform(-width(x),width(x))).array;
338 
339     gg.put( geomLine( Aes!(typeof(xs), "x", typeof(ys2fit), "y" )( xs,
340         ys2fit) ) )
341         .put(
342             geomPoint( Aes!(typeof(xs), "x", typeof(ys2noise), "y", string[],
343         "colour" )( xs, ys2noise, ("b").repeat(xs.length).array) ) );
344 
345     gg.save( "noise.png" );
346 }
347 
348 ///
349 unittest
350 {
351     /// http://blackedder.github.io/ggplotd/images/hist.png
352     import std.array : array;
353     import std.algorithm : map;
354     import std.range : repeat, iota;
355     import std.random : uniform;
356     auto xs = iota(0,25,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
357     auto aes = Aes!(typeof(xs), "x")( xs );
358     auto gg = GGPlotD().put( geomHist( aes ) );
359 
360     auto ys = (0.0).repeat( xs.length ).array;
361     auto aesPs = aes.mergeRange( Aes!(double[], "y", double[], "colour" )
362         ( ys, ys ) );
363     gg.put( geomPoint( aesPs ) );
364 
365     gg.save( "hist.png" );
366 }
367 
368 ///
369 unittest
370 {
371     /// http://blackedder.github.io/ggplotd/images/filled_hist.svg
372     import std.array : array;
373     import std.algorithm : map;
374     import std.range : repeat, iota, chain;
375     import std.random : uniform;
376     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
377     auto cols = "a".repeat(25).chain("b".repeat(25));
378     auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 
379         double[], "fill" )( 
380             xs, cols, 0.45.repeat(xs.length).array);
381     auto gg = GGPlotD().put( geomHist( aes ) );
382     gg.save( "filled_hist.svg" );
383 }
384 
385 /// Boxplot example
386 unittest
387 {
388     /// http://blackedder.github.io/ggplotd/images/boxplot.svg
389     import std.array : array;
390     import std.algorithm : map;
391     import std.range : repeat, iota, chain;
392     import std.random : uniform;
393     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
394     auto cols = "a".repeat(25).chain("b".repeat(25)).array;
395     auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 
396         double[], "fill", typeof(cols), "label" )( 
397             xs, cols, 0.45.repeat(xs.length).array, cols);
398     auto gg = GGPlotD().put( geomBox( aes ) );
399     gg.save( "boxplot.svg" );
400 }
401 
402 ///
403 unittest
404 {
405     /// http://blackedder.github.io/ggplotd/images/hist3D.svg
406     import std.array : array;
407     import std.algorithm : map;
408     import std.range : repeat, iota;
409     import std.random : uniform;
410 
411     auto xs = iota(0,100,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
412     auto ys = iota(0,100,1).map!((y) => uniform(0.0,5)+uniform(0.0,5)).array;
413     auto aes = Aes!(typeof(xs), "x", typeof(ys), "y")( xs, ys);
414     auto gg = GGPlotD().put( geomHist3D( aes ) );
415 
416     gg.save( "hist3D.svg" );
417 }
418 
419 
420 
421 /// Changing axes details
422 unittest
423 {
424     /// http://blackedder.github.io/ggplotd/images/axes.svg
425     import std.array : array;
426     import std.math : sqrt;
427     import std.algorithm : map;
428     import std.range : iota;
429     // Generate some noisy data with reducing width
430     auto f = (double x) { return x/(1+x); };
431     auto width = (double x) { return sqrt(0.1/(1+x)); };
432     auto xs = iota( 0, 10, 0.1 ).array;
433 
434     auto ysfit = xs.map!((x) => f(x)).array;
435 
436     auto gg = GGPlotD().put( geomLine( Aes!(typeof(xs), "x",
437         typeof(ysfit), "y" )( xs, ysfit ) ) );
438 
439     // Setting range and label for xaxis
440     gg.put( xaxisRange( 0, 8 ) ).put( xaxisLabel( "My xlabel" ) );
441     assertEqual( gg.xaxis.min, 0 );
442     // Setting range and label for yaxis
443     gg.put( yaxisRange( 0, 2.0 ) ).put( yaxisLabel( "My ylabel" ) );
444     assertEqual( gg.yaxis.max, 2.0 );
445     assertEqual( gg.yaxis.label, "My ylabel" );
446 
447     // change offset
448     gg.put( xaxisOffset( 0.25 ) ).put( yaxisOffset( 0.5 ) );
449 
450     // Change Margins
451     gg.put( Margins( 60, 60, 40, 30 ) );
452 
453     // Set a title
454     gg.put( title( "And now for something completely different" ) );
455     assertEqual( gg.title.title, "And now for something completely different" );
456 
457     // Saving on a 500x300 pixel surface
458     gg.save( "axes.svg", 500, 300 );
459 }
460 
461 /// Polygon
462 unittest
463 {
464     /// http://blackedder.github.io/ggplotd/images/polygon.png
465     auto gg = GGPlotD().put( geomPolygon( 
466         Aes!(
467             double[], "x",
468             double[], "y",
469             double[], "colour" )(
470             [1,0,0], [ 1, 1, 0 ], [1,0.1,0] ) ) );
471     gg.save( "polygon.png" );
472 }
473 
474 /// Setting background colour
475 unittest
476 {
477     /// http://blackedder.github.io/ggplotd/images/background.svg
478     import ggplotd.theme;
479     auto gg = GGPlotD().put( background( RGBA(0.7,0.7,0.7,1) ) );
480     gg.put( geomPoint( 
481         Aes!(
482             double[], "x",
483             double[], "y",
484             double[], "colour" )(
485             [1,0,0], [ 1, 1, 0 ], [1,0.1,0] ) ) );
486     gg.save( "background.svg" );
487 }
488 
489 /// Other data type
490 unittest
491 {
492     /// http://blackedder.github.io/ggplotd/images/data.png
493     import std.array : array;
494     import std.math : sqrt;
495     import std.algorithm : map;
496     import std.range : repeat, iota;
497     import std.random : uniform;
498     struct Point { double x; double y; }
499     // Generate some noisy data with reducing width
500     auto f = (double x) { return x/(1+x); };
501     auto width = (double x) { return sqrt(0.1/(1+x)); };
502     auto xs = iota( 0, 10, 0.1 ).array;
503 
504     auto points = xs.map!((x) => Point(x,
505         f(x) + uniform(-width(x),width(x))));
506 
507     auto gg = GGPlotD().put( geomPoint( points ) );
508 
509     gg.save( "data.png" );
510 }
511 
512