1 module ggplotd.geom;
2 
3 import std.range : front, popFront, empty;
4 
5 import cairo = cairo.cairo;
6 
7 import ggplotd.bounds;
8 import ggplotd.aes;
9 
10 version (unittest)
11 {
12     import dunit.toolkit;
13 }
14 
15 version (assert)
16 {
17     import std.stdio : writeln;
18 }
19 
20 /// Hold the data needed to draw to a plot context
21 struct Geom
22 {
23     import std.typecons : Nullable;
24 
25     /// Construct from a tuple
26     this(T)( in T tup ) //if (is(T==Tuple))
27     {
28         import ggplotd.aes : hasAesField;
29         static if (hasAesField!(T, "x"))
30             xStore.put(tup.x);
31         static if (hasAesField!(T, "y"))
32             yStore.put(tup.y);
33         static if (hasAesField!(T, "colour"))
34             colourStore.put(tup.colour);
35         static if (hasAesField!(T, "sizeStore"))
36             sizeStore.put(tup.sizeStore);
37         mask = tup.mask;
38     }
39 
40     import ggplotd.guide : GuideToColourFunction, GuideToDoubleFunction;
41     /// Delegate that takes a context and draws to it
42     alias drawFunction = cairo.Context delegate(cairo.Context context, 
43         in GuideToDoubleFunction xFunc, in GuideToDoubleFunction yFunc,
44         in GuideToColourFunction cFunc, in GuideToDoubleFunction sFunc);
45 
46     /// Function to draw to a cairo context
47     Nullable!drawFunction draw; 
48 
49     import ggplotd.guide : GuideStore;
50     GuideStore!"colour" colourStore;
51     GuideStore!"x" xStore;
52     GuideStore!"y" yStore;
53     GuideStore!"size" sizeStore;
54 
55     /// Whether to mask/prevent drawing outside plotting area
56     bool mask = true; 
57 }
58 
59 import ggplotd.colourspace : RGBA;
60 private auto fillAndStroke( cairo.Context context, in RGBA colour, 
61     in double fill, in double alpha )
62 {
63     import ggplotd.colourspace : toCairoRGBA;
64     context.save;
65 
66     context.identityMatrix();
67     if (fill>0)
68         {
69         context.setSourceRGBA(
70         RGBA(colour.r, colour.g, colour.b, fill).toCairoRGBA
71         );
72         context.fillPreserve();
73     }
74     context.setSourceRGBA(
75         RGBA(colour.r, colour.g, colour.b, alpha).toCairoRGBA
76     );
77     context.stroke();
78     context.restore;
79     return context;
80 }
81 
82 /++
83 General function for drawing geomShapes
84 +/
85 private template geomShape( string shape, AES )
86 {
87     import std.algorithm : map;
88     import ggplotd.range : mergeRange;
89     alias CoordType = typeof(DefaultValues
90         .mergeRange(AES.init));
91 
92     struct VolderMort 
93     {
94         this(AES aes)
95         {
96             import ggplotd.range : mergeRange;
97             _aes = DefaultValues
98                 .mergeRange(aes);
99         }
100 
101         @property auto front()
102         {
103             import ggplotd.guide : GuideToDoubleFunction, GuideToColourFunction;
104             immutable tup = _aes.front;
105             immutable f = delegate(cairo.Context context, 
106                  in GuideToDoubleFunction xFunc, in GuideToDoubleFunction yFunc,
107                  in GuideToColourFunction cFunc, in GuideToDoubleFunction sFunc ) {
108                 import std.math : isFinite;
109                 auto x = xFunc(tup.x);
110                 auto y = yFunc(tup.y);
111                 auto col = cFunc(tup.colour);
112                 if (!isFinite(x) || !isFinite(y))
113                     return context;
114                 context.save();
115                 context.translate(x, y);
116                 import ggplotd.aes : hasAesField;
117                 static if (hasAesField!(typeof(tup), "sizeStore")) {
118                     auto width = tup.width*sFunc(tup.sizeStore);
119                     auto height = tup.height*sFunc(tup.sizeStore);
120                 } else  {
121                     auto width = tup.width;
122                     auto height = tup.height;
123                 }
124 
125                 static if (is(typeof(tup.width)==immutable(Pixel)))
126                     auto devP = context.deviceToUserDistance(cairo.Point!double( width, height )); //tup.width.to!double, tup.width.to!double ));
127                 context.rotate(tup.angle);
128                 static if (shape=="ellipse")
129                 {
130                     import std.math : PI;
131                     static if (is(typeof(tup.width)==immutable(Pixel)))
132                     {
133                         context.scale( devP.x/2.0, devP.y/2.0 );
134                     } else {
135                         context.scale( width/2.0, height/2.0 );
136                     }
137                     context.arc(0,0, 1.0, 0,2*PI);
138                 } else {
139                     static if (is(typeof(tup.width)==immutable(Pixel)))
140                     {
141                         context.scale( devP.x, devP.y );
142                     } else {
143                         context.scale( width, height );
144                     }
145                     static if (shape=="triangle")
146                     {
147                         context.moveTo( -0.5, -0.5 );
148                         context.lineTo( 0.5, -0.5 );
149                         context.lineTo( 0, 0.5 );
150                     } else static if (shape=="diamond") {
151                         context.moveTo( 0, -0.5 );
152                         context.lineTo( 0.5, 0 );
153                         context.lineTo( 0, 0.5 );
154                         context.lineTo( -0.5, 0 );
155                     } else {
156                         context.moveTo( -0.5, -0.5 );
157                         context.lineTo( -0.5,  0.5 );
158                         context.lineTo(  0.5,  0.5 );
159                         context.lineTo(  0.5, -0.5 );
160                     }
161                     context.closePath;
162                 }
163 
164                 context.restore();
165                 context.fillAndStroke( col, tup.fill, tup.alpha );
166                 return context;
167             };
168 
169             auto geom = Geom( tup );
170             geom.draw = f;
171 
172             static if (!is(typeof(tup.width)==immutable(Pixel))) 
173             {
174                 geom.xStore.put(tup.x, 0.5*tup.width);
175                 geom.xStore.put(tup.x, -0.5*tup.width);
176             }
177             static if (!is(typeof(tup.height)==immutable(Pixel))) 
178             {
179                 geom.yStore.put(tup.y, 0.5*tup.height);
180                 geom.yStore.put(tup.y, -0.5*tup.height);
181             }
182 
183             return geom;
184         }
185 
186         void popFront()
187         {
188             _aes.popFront();
189         }
190 
191         @property bool empty()
192         {
193             return _aes.empty;
194         }
195 
196     private:
197         CoordType _aes;
198     }
199 
200     auto geomShape(AES aes)
201     {
202         return VolderMort(aes);
203     }
204 }
205 
206 unittest
207 {
208     import std.range : walkLength, zip;
209     import std.algorithm : map;
210 
211     import ggplotd.aes : aes;
212     auto aesRange = zip([1.0, 2.0], [3.0, 4.0], [1.0,1], [2.0,2])
213         .map!((a) => aes!("x", "y", "width", "height")( a[0], a[1], a[2], a[3]));
214     auto geoms = geomShape!("rectangle")(aesRange);
215 
216     assertEqual(geoms.walkLength, 2);
217     assertEqual(geoms.front.xStore.min, 0.5);
218     assertEqual(geoms.front.xStore.max, 1.5);
219     geoms.popFront;
220     assertEqual(geoms.front.xStore.max, 2.5);
221 }
222 
223 /**
224 Draw any type of geom
225 
226 The type field is required, which should be a string. Any of the geom* functions in ggplotd.geom 
227 can be passed using a lower case string minus the geom prefix, i.e. hist2d calls geomHist2D etc.
228 
229   Examples:
230   --------------
231     import ggplotd.geom : geomType;
232     geomType(Aes!(double[], "x", double[], "y", string[], "type")
233         ( [0.0,1,2], [5.0,6,7], ["line", "point", "line"] ));
234   --------------
235 
236 */
237 template geomType(AES)
238 {
239     string injectToGeom()
240     {
241         import std.format : format;
242         import std.traits;
243         import std.string : toLower;
244         string str = "auto toGeom(A)( A aes, string type ) {\nimport std.traits; import std.array : array;\n";
245         foreach( name; __traits(allMembers, ggplotd.geom) )
246         {
247             static if (name.length > 6 && name[0..4] == "geom" 
248                     && name != "geomType"
249                     )
250             {
251                 str ~= format( "static if(__traits(compiles,(A a) => %s(a))) {\nif (type == q{%s})\n\treturn %s!A(aes).array;\n}\n", name, name[4..$].toLower, name );
252             }
253         }
254 
255         str ~= "assert(0, q{Unknown type passed to geomType});\n}\n";
256         return str;
257     }
258 
259     /**
260 Draw any type of geom
261 
262 The type field is required, which should be a string. Any of the geom* functions in ggplotd.geom 
263 can be passed using a lower case string minus the geom prefix, i.e. hist2d calls geomHist2D etc.
264 */
265     auto geomType( AES aes )
266     {
267         import std.algorithm : map, joiner;
268 
269         import ggplotd.aes : group;
270         mixin(injectToGeom());
271 
272         return aes
273             .group!"type"
274             .map!((g) => toGeom(g, g[0].type)).joiner;
275     }
276 }
277 
278 ///
279 unittest
280 {
281     import std.range : walkLength;
282     assertEqual(
283             geomType(Aes!(double[], "x", double[], "y", string[], "type")
284                 ( [0.0,1,2], [5.0,6,7], ["line", "point", "line"] )).walkLength, 2
285             );
286 }
287 
288 /**
289 Draw rectangle centered at given x,y location
290 
291 Aside from x and y also width and height are required.
292 If the type of width is of type Pixel (see aes.d) then dimensions are assumed to be in Pixel (not user coordinates).
293 */
294 auto geomRectangle(AES)(AES aes)
295 {
296     return geomShape!("rectangle", AES)(aes);
297 }
298 
299 /**
300 Draw ellipse centered at given x,y location
301 
302 Aside from x and y also width and height are required.
303 If the type of width is of type Pixel (see aes.d) then dimensions are assumed to be in Pixel (not user coordinates).
304 */
305 auto geomEllipse(AES)(AES aes)
306 {
307     return geomShape!("ellipse", AES)(aes);
308 }
309 
310 /**
311 Draw triangle centered at given x,y location
312 
313 Aside from x and y also width and height are required.
314 If the type of width is of type Pixel (see aes.d) then dimensions are assumed to be in Pixel (not user coordinates).
315 */
316 auto geomTriangle(AES)(AES aes)
317 {
318     return geomShape!("triangle", AES)(aes);
319 }
320 
321 /**
322 Draw diamond centered at given x,y location
323 
324 Aside from x and y also width and height are required.
325 If the type of width is of type Pixel (see aes.d) then dimensions are assumed to be in Pixel (not user coordinates).
326 */
327 auto geomDiamond(AES)(AES aes)
328 {
329     return geomShape!("diamond", AES)(aes);
330 }
331 
332 /// Create points from the data
333 auto geomPoint(AES)(AES aesRange)
334 {
335     import std.algorithm : map;
336     import ggplotd.aes : aes, Pixel;
337     import ggplotd.range : mergeRange;
338     return DefaultValues
339         .mergeRange(aesRange)
340         .map!((a) => a.merge(aes!("sizeStore", "width", "height", "fill")
341             (a.size, Pixel(8), Pixel(8), a.alpha)))
342         .geomEllipse;
343 }
344 
345 ///
346 unittest
347 {
348     auto aes = Aes!(double[], "x", double[], "y")([1.0], [2.0]);
349     auto gl = geomPoint(aes);
350     gl.popFront;
351     assert(gl.empty);
352 }
353 
354 /// Create lines from data 
355 template geomLine(AES)
356 {
357     import std.algorithm : map;
358     import std.range : array, zip;
359 
360     import ggplotd.range : mergeRange;
361  
362     struct VolderMort 
363     {
364         this(AES aes)
365         {
366             groupedAes = DefaultValues.mergeRange(aes).group;
367         }
368 
369         @property auto front()
370         {
371             import ggplotd.aes : aes;
372             import ggplotd.guide : GuideToColourFunction, GuideToDoubleFunction;
373             auto coordsZip = groupedAes.front
374                 .map!((a) => aes!("x","y")(a.x, a.y));
375 
376             immutable flags = groupedAes.front.front;
377             immutable f = delegate(cairo.Context context, 
378                  in GuideToDoubleFunction xFunc, in GuideToDoubleFunction yFunc,
379                  in GuideToColourFunction cFunc, in GuideToDoubleFunction sFunc ) {
380 
381                 import std.math : isFinite;
382                 auto coords = coordsZip.save;
383                 auto fr = coords.front;
384                 context.moveTo(xFunc(fr.x), yFunc(fr.y));
385                 coords.popFront;
386                 foreach (tup; coords)
387                 {
388                     auto x = xFunc(tup.x);
389                     auto y = yFunc(tup.y);
390                     // TODO should we actually move to next coordinate here?
391                     if (isFinite(x) && isFinite(y))
392                     {
393                         context.lineTo(x, y);
394                         context.lineWidth = 2.0*flags.size;
395                     } else {
396                         context.newSubPath();
397                     }
398                 }
399 
400                 auto col = cFunc(flags.colour);
401                 context.fillAndStroke( col, flags.fill, flags.alpha );
402                 return context;
403             };
404 
405 
406             auto geom = Geom(groupedAes.front.front);
407             foreach (tup; coordsZip)
408             {
409                 geom.xStore.put(tup.x);
410                 geom.yStore.put(tup.y);
411             }
412             geom.draw = f;
413             return geom;
414         }
415 
416         void popFront()
417         {
418             groupedAes.popFront;
419         }
420 
421         @property bool empty()
422         {
423             return groupedAes.empty;
424         }
425 
426     private:
427         typeof(group(DefaultValues.mergeRange(AES.init))) groupedAes;
428     }
429 
430     auto geomLine(AES aes)
431     {
432         return VolderMort(aes);
433     }
434 }
435 
436 ///
437 unittest
438 {
439     auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([1.0,
440         2.0, 1.1, 3.0], [3.0, 1.5, 1.1, 1.8], ["a", "b", "a", "b"]);
441 
442     auto gl = geomLine(aes);
443 
444     import std.range : empty;
445 
446     assertEqual(gl.front.xStore.min(), 1.0);
447     assertEqual(gl.front.xStore.max(), 1.1);
448     gl.popFront;
449     assertEqual(gl.front.xStore.max(), 3.0);
450     gl.popFront;
451     assert(gl.empty);
452 }
453 
454 unittest
455 {
456     auto aes = Aes!(string[], "x", string[], "y", string[], "colour")(["a",
457         "b", "c", "b"], ["a", "b", "b", "a"], ["b", "b", "b", "b"]);
458 
459     auto gl = geomLine(aes);
460     assertEqual(gl.front.xStore.store.length, 3);
461     assertEqual(gl.front.yStore.store.length, 2);
462 }
463 
464 unittest
465 {
466     auto aes = Aes!(string[], "x", string[], "y", string[], "colour")(["a",
467         "b", "c", "b"], ["a", "b", "b", "a"], ["b", "b", "b", "b"]);
468 
469     auto gl = geomLine(aes);
470     auto aes2 = Aes!(string[], "x", string[], "y", double[], "colour")(["a",
471         "b", "c", "b"], ["a", "b", "b", "a"], [0, 1, 0, 0.1]);
472 
473     auto gl2 = geomLine(aes2);
474 
475     import std.range : chain, walkLength;
476 
477     assertEqual(gl.chain(gl2).walkLength, 4);
478 }
479 
480 /// Draw histograms based on the x coordinates of the data
481 auto geomHist(AES)(AES aes, size_t noBins = 0)
482 {
483     import ggplotd.stat : statHist;
484     return geomRectangle( statHist( aes, noBins ) );
485 }
486 
487 /** 
488 Draw histograms based on the x and y coordinates of the data
489   
490   Examples:
491   --------------
492     /// http://blackedder.github.io/ggplotd/images/hist2D.svg
493      import std.array : array;
494     import std.algorithm : map;
495     import std.conv : to;
496     import std.range : repeat, iota;
497     import std.random : uniform;
498 
499     import ggplotd.aes : Aes;
500     import ggplotd.colour : colourGradient;
501     import ggplotd.colourspace : XYZ;
502     import ggplotd.geom : geomHist2D;
503     import ggplotd.ggplotd : GGPlotD;
504 
505     auto xs = iota(0,500,1).map!((x) => uniform(0.0,5)+uniform(0.0,5))
506         .array;
507     auto ys = iota(0,500,1).map!((y) => uniform(0.0,5)+uniform(0.0,5))
508         .array;
509     auto aes = Aes!(typeof(xs), "x", typeof(ys), "y")( xs, ys);
510     auto gg = GGPlotD().put( geomHist2D( aes ) );
511     // Use a different colour scheme
512     gg.put( colourGradient!XYZ( "white-cornflowerBlue-crimson" ) );
513 
514     gg.save( "hist2D.svg" );
515   --------------
516 */
517 auto geomHist2D(AES)(AES aes, size_t noBinsX = 0, size_t noBinsY = 0)
518 {
519     import std.algorithm : map, joiner;
520     import ggplotd.stat : statHist2D;
521 
522     return statHist2D( aes, noBinsX, noBinsY )
523             .map!( (poly) => geomPolygon( poly ) ).joiner;
524 }
525 
526 
527 /**
528     Deprecated: superseded by geomHist2D
529 */
530 deprecated alias geomHist3D = geomHist2D;
531 
532 /// Draw axis, first and last location are start/finish
533 /// others are ticks (perpendicular)
534 auto geomAxis(AES)(AES aes, double tickLength, string label)
535 {
536     import std.algorithm : find;
537     import std.array : array;
538     import std.range : chain, empty, repeat;
539     import std.math : sqrt, pow;
540 
541     import ggplotd.range : mergeRange;
542 
543     double[] xs;
544     double[] ys;
545 
546     double[] lxs;
547     double[] lys;
548     double[] langles;
549     string[] lbls;
550 
551     auto merged = DefaultValues.mergeRange(aes);
552 
553     immutable toDir = 
554         merged.find!("a.x != b.x || a.y != b.y")(merged.front).front; 
555     auto direction = [toDir.x - merged.front.x, toDir.y - merged.front.y];
556     immutable dirLength = sqrt(pow(direction[0], 2) + pow(direction[1], 2));
557     direction[0] *= tickLength / dirLength;
558     direction[1] *= tickLength / dirLength;
559  
560     while (!merged.empty)
561     {
562         auto tick = merged.front;
563         xs ~= tick.x;
564         ys ~= tick.y;
565 
566         merged.popFront;
567 
568         // Draw ticks perpendicular to main axis;
569         if (xs.length > 1 && !merged.empty)
570         {
571             xs ~= [tick.x + direction[1], tick.x];
572             ys ~= [tick.y + direction[0], tick.y];
573 
574             lxs ~= tick.x - 1.3*direction[1];
575             lys ~= tick.y - 1.3*direction[0];
576             lbls ~= tick.label;
577             langles ~= tick.angle;
578         }
579     }
580 
581     // Main label
582     auto xm = xs[0] + 0.5*(xs[$-1]-xs[0]) - 4.0*direction[1];
583     auto ym = ys[0] + 0.5*(ys[$-1]-ys[0]) - 4.0*direction[0];
584     auto aesM = Aes!(double[], "x", double[], "y", string[], "label", 
585         double[], "angle", bool[], "mask")( [xm], [ym], [label], 
586             langles, [false]);
587 
588     return geomLine(Aes!(typeof(xs), "x", typeof(ys), "y", bool[], "mask")(
589         xs, ys, false.repeat(xs.length).array)).chain(
590         geomLabel(Aes!(double[], "x", double[], "y", string[], "label",
591         double[], "angle", bool[], "mask", double[], "size")(lxs, lys, lbls, langles, 
592             false.repeat(lxs.length).array, aes.front.size.repeat(lxs.length).array)))
593             .chain( geomLabel(aesM) );
594 }
595 
596 /**
597     Draw Label at given x and y position
598 
599     You can specify justification, by passing a justify field in the passed data (aes).
600        $(UL
601         $(LI "center" (default))
602         $(LI "left")
603         $(LI "right")
604         $(LI "bottom")
605         $(LI "top"))
606 */
607 template geomLabel(AES)
608 {
609     import std.algorithm : map;
610     import std.typecons : Tuple;
611     import ggplotd.range : mergeRange;
612     alias CoordType = typeof(DefaultValues
613         .merge(Tuple!(string, "justify").init)
614         .mergeRange(AES.init));
615 
616     struct VolderMort
617     {
618         this(AES aes)
619         {
620             import std.algorithm : map;
621             import ggplotd.range : mergeRange;
622 
623             _aes = DefaultValues
624                 .merge(Tuple!(string, "justify")("center"))
625                 .mergeRange(aes);
626         }
627 
628         @property auto front()
629         {
630             import ggplotd.guide : GuideToDoubleFunction, GuideToColourFunction;
631             immutable tup = _aes.front;
632             immutable f = delegate(cairo.Context context, 
633                  in GuideToDoubleFunction xFunc, in GuideToDoubleFunction yFunc,
634                  in GuideToColourFunction cFunc, in GuideToDoubleFunction sFunc ) {
635                 auto x = xFunc(tup.x);
636                 auto y = yFunc(tup.y);
637                 auto col = cFunc(tup.colour);
638                 import std.math : ceil, isFinite;
639                 if (!isFinite(x) || !isFinite(y))
640                     return context;
641                 context.setFontSize(ceil(14.0*tup.size));
642                 context.moveTo(x, y);
643                 context.save();
644                 context.identityMatrix;
645                 context.rotate(tup.angle);
646                 auto extents = context.textExtents(tup.label);
647                 auto textSize = cairo.Point!double(extents.width, extents.height);
648                 // Justify
649                 if (tup.justify == "left")
650                     context.relMoveTo(0, 0.5*textSize.y);
651                 else if (tup.justify == "right")
652                     context.relMoveTo(-textSize.x, 0.5*textSize.y);
653                 else if (tup.justify == "bottom")
654                     context.relMoveTo(-0.5*textSize.x, 0);
655                 else if (tup.justify == "top")
656                     context.relMoveTo(-0.5*textSize.x, textSize.y);
657                 else
658                     context.relMoveTo(-0.5*textSize.x, 0.5*textSize.y);
659 
660                 import ggplotd.colourspace : RGBA, toCairoRGBA;
661 
662                 context.setSourceRGBA(
663                     RGBA(col.r, col.g, col.b, tup.alpha)
664                         .toCairoRGBA
665                 );
666  
667                 context.showText(tup.label);
668                 context.restore();
669                 return context;
670             };
671 
672             auto geom = Geom( tup );
673             geom.draw = f;
674  
675             return geom;
676         }
677 
678         void popFront()
679         {
680             _aes.popFront();
681         }
682 
683         @property bool empty()
684         {
685             return _aes.empty;
686         }
687 
688     private:
689         CoordType _aes;
690     }
691 
692     auto geomLabel(AES aes)
693     {
694         return VolderMort(aes);
695     }
696 }
697 
698 unittest
699 {
700     auto aes = Aes!(string[], "x", string[], "y", string[], "label")(["a", "b",
701         "c", "b"], ["a", "b", "b", "a"], ["b", "b", "b", "b"]);
702 
703     auto gl = geomLabel(aes);
704     import std.range : walkLength;
705 
706     assertEqual(gl.walkLength, 4);
707 }
708 
709 // geomBox
710 /// Return the limits indicated with different alphas
711 private auto limits( RANGE )( RANGE range, double[] alphas )
712 {
713     import std.algorithm : sort, map, min, max;
714     import std.math : floor;
715     import std.conv : to;
716     auto sorted = range.sort();
717     return alphas.map!( (a) { 
718         auto id = min( sorted.length.to!int-2,
719             max(0,floor( a*(sorted.length+1) ).to!int-1 ) );
720         assert( id >= 0 );
721         if (a<=0.5)
722             return sorted[id];
723         else
724             return sorted[id+1];
725     });
726 }
727 
728 unittest
729 {
730     import std.range : array, front;
731     assertEqual( [1,2,3,4,5].limits( [0.01, 0.5, 0.99] ).array, 
732             [1,3,5] );
733 
734     assertEqual( [1,2,3,4].limits( [0.41] ).front, 2 );
735     assertEqual( [1,2,3,4].limits( [0.39] ).front, 1 );
736     assertEqual( [1,2,3,4].limits( [0.61] ).front, 4 );
737     assertEqual( [1,2,3,4].limits( [0.59] ).front, 3 );
738 }
739 
740 /// Draw a boxplot. The "x" data is used. If labels are given then the data is grouped by the label
741 auto geomBox(AES)(AES aesRange)
742 {
743     import std.algorithm : filter, map;
744     import std.array : array;
745     import std.range : Appender, walkLength, ElementType;
746     import std.typecons : Tuple;
747     import ggplotd.aes : aes, hasAesField;
748     import ggplotd.range : mergeRange;
749 
750     Appender!(Geom[]) result;
751 
752     // If has y, use that
753     static if (hasAesField!(ElementType!AES, "y"))
754     {
755         auto myAes = aesRange.map!((a) => a.merge(aes!("label")(a.y)));
756     } else {
757         static if (!hasAesField!(ElementType!AES, "label"))
758         {
759             import std.range : repeat, walkLength;
760             auto myAes = aesRange.map!((a) => a.merge(aes!("label")(0.0)));
761         } else {
762             auto myAes = aesRange;
763         }
764     }
765     
766     // TODO if x (y in the original aesRange) is numerical then this should relly scale 
767     // by the range
768     double delta = 0.2;
769 
770     foreach( grouped; myAes.group().filter!((a) => a.walkLength > 3) )
771     {
772         auto lims = grouped.map!("a.x.to!double")
773             .array.limits( [0.1,0.25,0.5,0.75,0.9] ).array;
774         auto x = grouped.front.label;
775         result.put(
776             [grouped.front.merge(aes!("x", "y", "width", "height")
777                 (x, (lims[2]+lims[1])/2.0, 2*delta, lims[2]-lims[1])),
778              grouped.front.merge(aes!("x", "y", "width", "height")
779                 (x, (lims[3]+lims[2])/2.0, 2*delta, lims[3]-lims[2]))
780             ].geomRectangle
781         );
782 
783         result.put(
784             [grouped.front.merge(aes!("x", "y")(x,lims[0])),
785                 grouped.front.merge(aes!("x", "y")(x,lims[1]))].geomLine);
786         result.put(
787             [grouped.front.merge(aes!("x", "y")(x,lims[3])),
788                 grouped.front.merge(aes!("x", "y")(x,lims[4]))].geomLine);
789 
790         // Increase plot bounds
791         result.data.front.xStore.put(x, 2*delta);
792         result.data.front.xStore.put(x, -2*delta);
793     }
794 
795     return result.data;
796 }
797 
798 ///
799 unittest 
800 {
801     import std.array : array;
802     import std.algorithm : map;
803     import std.range : repeat, iota, chain, zip;
804     import std.random : uniform;
805     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
806     auto cols = "a".repeat(25).chain("b".repeat(25)).array;
807     auto aesRange = zip(xs, cols)
808         .map!((a) => aes!("x", "colour", "fill", "label")(a[0], a[1], 0.45, a[1]));
809     auto gb = geomBox( aesRange );
810     assertEqual( gb.front.xStore.min(), -0.4 );
811 }
812 
813 unittest 
814 {
815     import std.array : array;
816     import std.algorithm : map;
817     import std.range : repeat, iota, chain, zip;
818     import std.random : uniform;
819     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
820     auto cols = "a".repeat(25).chain("b".repeat(25)).array;
821     auto ys = 2.repeat(25).chain(3.repeat(25)).array;
822     auto aesRange = zip(xs, cols, ys)
823         .map!((a) => aes!("x", "colour", "fill", "y")(a[0], a[1], .45, a[2]));
824     auto gb = geomBox( aesRange );
825     assertEqual( gb.front.xStore.min, 1.6 );
826 }
827 
828 unittest 
829 {
830     // Test when passing one data point
831     import std.array : array;
832     import std.algorithm : map;
833     import std.range : repeat, iota, chain;
834     import std.random : uniform;
835     auto xs = iota(0,1,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
836     auto cols = "a".repeat(1).array;
837     auto ys = 2.repeat(1).array;
838     auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 
839         double[], "fill", typeof(ys), "y" )( 
840             xs, cols, 0.45.repeat(xs.length).array, ys);
841     auto gb = geomBox( aes );
842     assertEqual( gb.length, 0 );
843 }
844 
845 unittest 
846 {
847     import std.array : array;
848     import std.algorithm : map;
849     import std.range : repeat, iota, chain, zip;
850     import std.random : uniform;
851     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
852     auto cols = "a".repeat(25).chain("b".repeat(25)).array;
853     auto aesRange = zip(xs, cols)
854         .map!((a) => aes!("x", "colour", "fill")(a[0], a[1], .45));
855     auto gb = geomBox( aesRange );
856     assertEqual( gb.front.xStore.min, -0.4 );
857 }
858 
859 /// Draw a polygon 
860 auto geomPolygon(AES)(AES aes)
861 {
862     // TODO would be nice to allow grouping of triangles
863     import std.array : array;
864     import std.algorithm : map, swap;
865     import std.conv : to;
866     import ggplotd.geometry : gradientVector, Vertex3D;
867     import ggplotd.range : mergeRange;
868 
869     auto merged = DefaultValues.mergeRange(aes);
870 
871     immutable flags = merged.front;
872 
873     auto geom = Geom( flags );
874 
875     foreach(tup; merged)
876     {
877         geom.xStore.put(tup.x);
878         geom.yStore.put(tup.y);
879         geom.colourStore.put(tup.colour);
880     }
881 
882     import ggplotd.guide : GuideToDoubleFunction, GuideToColourFunction;
883     // Define drawFunction
884     immutable f = delegate(cairo.Context context, 
885          in GuideToDoubleFunction xFunc, in GuideToDoubleFunction yFunc,
886          in GuideToColourFunction cFunc, in GuideToDoubleFunction sFunc ) 
887     {
888         // Turn into vertices.
889         auto vertices = merged.map!((t) => Vertex3D( xFunc(t.x), yFunc(t.y), 
890             cFunc.toDouble(t.colour)));
891 
892             // Find lowest, highest
893         auto triangle = vertices.array;
894         if (triangle[1].z < triangle[0].z)
895             swap( triangle[1], triangle[0] );
896         if (triangle[2].z < triangle[0].z)
897             swap( triangle[2], triangle[0] );
898         if (triangle[1].z > triangle[2].z)
899             swap( triangle[1], triangle[2] );
900 
901         if (triangle.length > 3) 
902         { 
903             foreach( v; triangle[3..$] )
904             {
905                 if (v.z < triangle[0].z)
906                     swap( triangle[0], v );
907                 else if ( v.z > triangle[2].z )
908                     swap( triangle[2], v );
909             }
910         }
911         auto gV = gradientVector( triangle[0..3] );
912 
913         auto gradient = new cairo.LinearGradient( gV[0].x, gV[0].y, 
914             gV[1].x, gV[1].y );
915 
916         context.lineWidth = 0.0;
917 
918         /*
919             We add a number of stops to the gradient. Optimally we should only add the top
920             and bottom, but this is not possible for two reasons. First of all we support
921             other colour spaces than rgba, while cairo only support rgba. We _simulate_ 
922             the other colourspace in RGBA by taking small steps in the rgba colourspace.
923             Secondly to support multiple colour stops in our own colourgradient we need to 
924             add all those.
925 
926             The ideal way to solve the second problem would be by using the colourGradient
927             stops here, but that wouldn't solve the first issue, so we go for the stupider
928             solution here.
929 
930             Ideally we would see how cairo does their colourgradient and implement the same
931             for other colourspaces.
932 i       */
933         auto no_stops = 10.0; import std.range : iota;
934         import std.array : array;
935         auto stepsize = (gV[1].z - gV[0].z)/no_stops;
936         auto steps = [gV[0].z, gV[1].z];
937         if (stepsize > 0)
938             steps = iota(gV[0].z, gV[1].z, stepsize).array ~ gV[1].z;
939 
940         foreach(i, z; steps) {
941             auto col = cFunc(z);
942             import ggplotd.colourspace : RGBA, toCairoRGBA;
943             gradient.addColorStopRGBA(i/(steps.length-1.0),
944                 RGBA(col.r, col.g, col.b, flags.alpha).toCairoRGBA
945             );
946         }
947 
948         context.moveTo( vertices.front.x, vertices.front.y );
949         vertices.popFront;
950         foreach( v; vertices )
951             context.lineTo( v.x, v.y );
952         context.closePath;
953         context.setSource( gradient );
954         context.fillPreserve;
955         context.identityMatrix();
956         context.stroke;
957         return context;
958     };
959 
960     geom.draw = f;
961     return [geom];
962 }
963 
964 
965 /**
966   Draw kernel density based on the x coordinates of the data
967 
968   Examples:
969   --------------
970     /// http://blackedder.github.io/ggplotd/images/filled_density.svg
971     import std.array : array;
972     import std.algorithm : map;
973     import std.range : repeat, iota, chain;
974     import std.random : uniform;
975 
976     import ggplotd.aes : Aes;
977     import ggplotd.geom : geomDensity;
978     import ggplotd.ggplotd : GGPlotD;
979     import ggplotd.legend : discreteLegend;
980     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
981     auto cols = "a".repeat(25).chain("b".repeat(25));
982     auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 
983         double[], "fill" )( 
984             xs, cols, 0.45.repeat(xs.length).array);
985     auto gg = GGPlotD().put( geomDensity( aes ) );
986     gg.put(discreteLegend);
987     gg.save( "filled_density.svg" );
988   --------------
989 */
990 auto geomDensity(AES)(AES aes)
991 {
992     import ggplotd.stat : statDensity;
993     return geomLine( statDensity( aes ) );
994 }
995 
996 /**
997   Draw kernel density based on the x and y coordinates of the data
998 
999   Examples:
1000   --------------
1001     /// http://blackedder.github.io/ggplotd/images/density2D.png
1002     import std.array : array;
1003     import std.algorithm : map;
1004     import std.conv : to;
1005     import std.range : repeat, iota;
1006     import std.random : uniform;
1007 
1008     import ggplotd.aes : Aes;
1009     import ggplotd.colour : colourGradient;
1010     import ggplotd.colourspace : XYZ;
1011     import ggplotd.geom : geomDensity2D;
1012     import ggplotd.ggplotd : GGPlotD;
1013     import ggplotd.legend : continuousLegend;
1014 
1015     auto xs = iota(0,500,1).map!((x) => uniform(0.0,5)+uniform(0.0,5))
1016         .array;
1017     auto ys = iota(0,500,1).map!((y) => uniform(0.5,1.5)+uniform(0.5,1.5))
1018         .array;
1019     auto aes = Aes!(typeof(xs), "x", typeof(ys), "y")( xs, ys);
1020     auto gg = GGPlotD().put( geomDensity2D( aes ) );
1021     // Use a different colour scheme
1022     gg.put( colourGradient!XYZ( "white-cornflowerBlue-crimson" ) );
1023     gg.put(continuousLegend);
1024 
1025     gg.save( "density2D.png" );
1026   --------------
1027 */
1028 auto geomDensity2D(AES)(AES aes) 
1029 {
1030     import std.algorithm : map, joiner;
1031     import ggplotd.stat : statDensity2D;
1032 
1033     return statDensity2D( aes )
1034             .map!( (poly) => geomPolygon( poly ) ).joiner;
1035 }