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 
15 version (unittest)
16 {
17     import dunit.toolkit;
18 }
19 
20 alias TitleFunction = Title delegate(Title);
21 
22 // Currently only holds the title. In the future could also be used to store details on location etc.
23 struct Title
24 {
25     /// The actual title
26     string title;
27 }
28 
29 ///
30 TitleFunction title( string title )
31 {
32     return delegate(Title t) { t.title = title; return t; };
33 }
34 
35 private auto createEmptySurface( string fname, int width, int height )
36 {
37     cairo.Surface surface;
38 
39     static if (cconfig.CAIRO_HAS_PDF_SURFACE)
40         {
41         if (fname[$ - 3 .. $] == "pdf")
42             {
43             surface = new cpdf.PDFSurface(fname, width, height);
44         }
45     }
46     else
47         {
48         if (fname[$ - 3 .. $] == "pdf")
49             assert(0, "PDF support not enabled by cairoD");
50     }
51     static if (cconfig.CAIRO_HAS_SVG_SURFACE)
52         {
53         if (fname[$ - 3 .. $] == "svg")
54             {
55             surface = new csvg.SVGSurface(fname, width, height);
56         }
57     }
58     else
59     {
60         if (fname[$ - 3 .. $] == "svg")
61             assert(0, "SVG support not enabled by cairoD");
62     }
63     if (fname[$ - 3 .. $] == "png")
64     {
65         surface = new cairo.ImageSurface(cairo.Format.CAIRO_FORMAT_ARGB32, width, height);
66     }
67 
68     auto backcontext = cairo.Context(surface);
69     backcontext.setSourceRGB(1, 1, 1);
70     backcontext.rectangle(0, 0, width, height);
71     backcontext.fill();
72 
73     return surface;
74 }
75 
76 auto drawTitle( in Title title, ref cairo.Surface surface,
77     in Margins margins, int width, int height )
78 {
79     auto context = cairo.Context(surface);
80     context.setFontSize(16.0);
81     context.moveTo( width/2, margins.top/2 );
82     auto extents = context.textExtents(title.title);
83 
84     auto textSize = cairo.Point!double(0.5 * extents.width, 0.5 * extents.height);
85     context.relMoveTo(-textSize.x, textSize.y);
86 
87     context.showText(title.title);
88     return surface;
89 }
90 
91 auto drawGeom( Geom geom, ref cairo.Surface surface,
92     ColourMap colourMap, ScaleType scaleFunction, in Bounds bounds, 
93     in Margins margins, int width, int height )
94 {
95     cairo.Context context;
96     if (geom.mask) {
97         auto plotSurface = cairo.Surface.createForRectangle(surface,
98             cairo.Rectangle!double(margins.left, margins.top,
99             width - (margins.left+margins.right), 
100             height - (margins.top+margins.bottom)));
101         context = cairo.Context(plotSurface);
102     } else {
103         context = cairo.Context(surface);
104         context.translate(margins.left, margins.top);
105     }
106     import std.conv : to;
107     context = scaleFunction(context, bounds,
108         width.to!double - (margins.left+margins.right),
109         height.to!double - (margins.top+margins.bottom));
110     context = geom.draw(context, colourMap);
111     return surface;
112 }
113 
114 ///
115 struct Margins
116 {
117     size_t left = 50; ///
118     size_t right = 20; ///
119     size_t bottom = 50; ///
120     size_t top = 40; ///
121 }
122 
123 ///
124 struct GGPlotD
125 {
126     Geom[] geomRange;
127 
128     XAxis xaxis;
129     YAxis yaxis;
130 
131     Margins margins;
132 
133     Title title;
134 
135     ScaleType scaleFunction;
136 
137     ///
138     void save( string fname, int width = 470, int height = 470 )
139     {
140         bool pngWrite = false;
141         auto surface = createEmptySurface( fname, width, height );
142 
143         if (fname[$ - 3 .. $] == "png")
144         {
145             pngWrite = true;
146         }
147 
148         if (!initScale)
149             scaleFunction = scale(); // This needs to be removed later
150         import std.range : front;
151 
152         AdaptiveBounds bounds;
153         typeof(geomRange.front.colours) colourIDs;
154         auto xAxisTicks = geomRange.front.xTickLabels;
155         auto yAxisTicks = geomRange.front.yTickLabels;
156 
157         foreach (geom; geomRange)
158         {
159             bounds.adapt(geom.bounds);
160             colourIDs ~= geom.colours;
161             xAxisTicks ~= geom.xTickLabels;
162             yAxisTicks ~= geom.yTickLabels;
163         }
164 
165         auto colourMap = createColourMap(colourIDs);
166 
167         // Axis
168         import std.algorithm : sort, uniq, min, max;
169         import std.range : chain;
170         import std.array : array;
171         import ggplotd.axes;
172 
173         // If ticks are provided then we make sure the bounds include them
174         auto sortedTicks = xAxisTicks.sort().uniq.array;
175         if (!sortedTicks.empty)
176         {
177             bounds.min_x = min( bounds.min_x, sortedTicks[0][0] );
178             bounds.max_x = max( bounds.max_x, sortedTicks[$-1][0] );
179         }
180         if (initialized(xaxis))
181         {
182             bounds.min_x = xaxis.min;
183             bounds.max_x = xaxis.max;
184         }
185 
186         auto aesX = axisAes("x", bounds.min_x, bounds.max_x, bounds.min_y,
187             sortedTicks );
188 
189         sortedTicks = yAxisTicks.sort().uniq.array;
190         if (!sortedTicks.empty)
191         {
192             bounds.min_y = min( bounds.min_y, sortedTicks[0][0] );
193             bounds.max_y = max( bounds.max_y, sortedTicks[$-1][0] );
194         }
195         if (initialized(yaxis))
196         {
197             bounds.min_y = yaxis.min;
198             bounds.max_y = yaxis.max;
199         }
200 
201         auto aesY = axisAes("y", bounds.min_y, bounds.max_y, bounds.min_x,
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             surface = geom.drawGeom( surface,
210                 colourMap, scaleFunction, bounds, 
211                 margins, width, height );
212         }
213 
214         // Plot title
215         surface = title.drawTitle( surface, margins, width, height );
216  
217         if (pngWrite)
218             (cast(cairo.ImageSurface)(surface)).writeToPNG(fname);
219     }
220 
221     /// Using + to extend the plot for compatibility to ggplot2 in R
222     ref GGPlotD opBinary(string op, T)(T rhs) if (op == "+")
223     {
224         static if (is(ElementType!T==Geom))
225         {
226             import std.array : array;
227             geomRange ~= rhs.array;
228         }
229         static if (is(T==ScaleType))
230         {
231             initScale = true;
232             scaleFunction = rhs;
233         }
234         static if (is(T==XAxisFunction))
235         {
236             xaxis = rhs( xaxis );
237         }
238         static if (is(T==YAxisFunction))
239         {
240             yaxis = rhs( yaxis );
241         }
242         static if (is(T==TitleFunction))
243         {
244             title = rhs( title );
245         }
246         static if (is(T==Margins))
247         {
248             margins = rhs;
249         }
250         return this;
251     }
252 
253     ///
254     ref GGPlotD put(T)(T rhs)
255     {
256         return this.opBinary!("+", T)(rhs);
257     }
258 
259 private:
260     bool initScale = false;
261 }
262 
263 unittest
264 {
265     auto gg = GGPlotD()
266         .put( yaxisLabel( "My ylabel" ) )
267         .put( yaxisRange( 0, 2.0 ) );
268     assertEqual( gg.yaxis.max, 2.0 );
269     assertEqual( gg.yaxis.label, "My ylabel" );
270 
271     gg = GGPlotD(); 
272     gg.put( yaxisLabel( "My ylabel" ) )
273         .put( yaxisRange( 0, 2.0 ) );
274     assertEqual( gg.yaxis.max, 2.0 );
275     assertEqual( gg.yaxis.label, "My ylabel" );
276 }
277 
278 
279 ///
280 unittest
281 {
282     auto aes = Aes!(string[], "x", string[], "y", string[], "colour")(["a",
283         "b", "c", "b"], ["x", "y", "y", "x"], ["b", "b", "b", "b"]);
284     auto gg = GGPlotD();
285     gg + geomLine(aes) + scale();
286     gg.save( "test6.png");
287 }
288 
289 ///
290 unittest
291 {
292     import std.array : array;
293     import std.math : sqrt;
294     import std.algorithm : map;
295     import std.range : repeat, iota;
296     import std.random : uniform;
297     // Generate some noisy data with reducing width
298     auto f = (double x) { return x/(1+x); };
299     auto width = (double x) { return sqrt(0.1/(1+x)); };
300     auto xs = iota( 0, 10, 0.1 ).array;
301 
302     auto ysfit = xs.map!((x) => f(x));
303     auto ysnoise = xs.map!((x) => f(x) + uniform(-width(x),width(x))).array;
304 
305     auto aes = Aes!(typeof(xs), "x",
306         typeof(ysnoise), "y", string[], "colour" )( xs, ysnoise, ("a").repeat(xs.length).array );
307     auto gg = GGPlotD().put( geomPoint( aes ) );
308     gg.put( geomLine( Aes!(typeof(xs), "x",
309         typeof(ysfit), "y" )( xs, ysfit ) ) );
310 
311     //  
312     auto ys2fit = xs.map!((x) => 1-f(x));
313     auto ys2noise = xs.map!((x) => 1-f(x) + uniform(-width(x),width(x))).array;
314 
315     gg.put( geomLine( Aes!(typeof(xs), "x", typeof(ys2fit), "y" )( xs,
316         ys2fit) ) )
317         .put(
318             geomPoint( Aes!(typeof(xs), "x", typeof(ys2noise), "y", string[],
319         "colour" )( xs, ys2noise, ("b").repeat(xs.length).array) ) );
320 
321     gg.save( "noise.png" );
322 }
323 
324 ///
325 unittest
326 {
327     import std.array : array;
328     import std.algorithm : map;
329     import std.range : repeat, iota;
330     import std.random : uniform;
331     auto xs = iota(0,25,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
332     auto aes = Aes!(typeof(xs), "x")( xs );
333     auto gg = GGPlotD().put( geomHist( aes ) );
334 
335     auto ys = (0.0).repeat( xs.length ).array;
336     auto aesPs = aes.merge( Aes!(double[], "y", double[], "colour" )
337         ( ys, ys ) );
338     gg.put( geomPoint( aesPs ) );
339 
340     gg.save( "hist.png" );
341 }
342 
343 ///
344 unittest
345 {
346     import std.array : array;
347     import std.algorithm : map;
348     import std.range : repeat, iota, chain;
349     import std.random : uniform;
350     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
351     auto cols = "a".repeat(25).chain("b".repeat(25));
352     auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 
353         double[], "fill" )( 
354             xs, cols, 0.45.repeat(xs.length).array);
355     auto gg = GGPlotD().put( geomHist( aes ) );
356     gg.save( "filled_hist.svg" );
357 }
358 
359 ///
360 unittest
361 {
362     import std.array : array;
363     import std.algorithm : map;
364     import std.range : repeat, iota, chain;
365     import std.random : uniform;
366     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
367     auto cols = "a".repeat(25).chain("b".repeat(25)).array;
368     auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 
369         double[], "fill", typeof(cols), "label" )( 
370             xs, cols, 0.45.repeat(xs.length).array, cols);
371     auto gg = GGPlotD().put( geomBox( aes ) );
372     gg.save( "boxplot.svg" );
373 }
374 
375 ///
376 unittest
377 {
378     import std.array : array;
379     import std.math : sqrt;
380     import std.algorithm : map;
381     import std.range : iota;
382     // Generate some noisy data with reducing width
383     auto f = (double x) { return x/(1+x); };
384     auto width = (double x) { return sqrt(0.1/(1+x)); };
385     auto xs = iota( 0, 10, 0.1 ).array;
386 
387     auto ysfit = xs.map!((x) => f(x)).array;
388 
389     auto gg = GGPlotD().put( geomLine( Aes!(typeof(xs), "x",
390         typeof(ysfit), "y" )( xs, ysfit ) ) );
391 
392     // Setting range and label for xaxis
393     gg.put( xaxisRange( 0, 8 ) ).put( xaxisLabel( "My xlabel" ) );
394     assertEqual( gg.xaxis.min, 0 );
395     // Setting range and label for yaxis
396     gg.put( yaxisRange( 0, 2.0 ) ).put( yaxisLabel( "My ylabel" ) );
397     assertEqual( gg.yaxis.max, 2.0 );
398     assertEqual( gg.yaxis.label, "My ylabel" );
399 
400     // Change Margins
401     gg.put( Margins( 60, 60, 40, 30 ) );
402 
403     // Set a title
404     gg.put( title( "And now for something completely different" ) );
405     assertEqual( gg.title.title, "And now for something completely different" );
406 
407     // Saving on a 500x300 pixel surface
408     gg.save( "axes.svg", 500, 300 );
409 }
410 
411 ///
412 unittest
413 {
414     auto gg = GGPlotD().put( geomPolygon( 
415         Aes!(
416             double[], "x",
417             double[], "y",
418             double[], "colour" )(
419             [1,0,0], [ 1, 1, 0 ], [1,0.1,0] ) ) );
420     gg.save( "polygon.png" );
421 }