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 }