The OpenD Programming Language

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