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 import ggplotd.colour : ColourID, ColourMap;
10 
11 version (unittest)
12 {
13     import dunit.toolkit;
14 }
15 
16 ///
17 struct Geom
18 {
19     this(T)( in T tup ) //if (is(T==Tuple))
20     {
21         mask = tup.mask;
22     }
23 
24     alias drawFunction = cairo.Context delegate(cairo.Context context, 
25         ColourMap colourMap);
26     drawFunction draw; ///
27     ColourID[] colours; ///
28     AdaptiveBounds bounds; ///
29 
30     bool mask = true; /// Whether to mask/prevent drawing outside plotting area
31 
32     import std.typecons : Tuple;
33 
34     Tuple!(double, string)[] xTickLabels; ///
35     Tuple!(double, string)[] yTickLabels; ///
36 }
37 
38 ///
39 auto geomPoint(AES)(AES aes)
40 {
41     import std.algorithm : map;
42     auto xsMap = aes.map!("a.x");
43     auto ysMap = aes.map!("a.y");
44     alias CoordX = typeof(NumericLabel!(typeof(xsMap))(xsMap));
45     alias CoordY = typeof(NumericLabel!(typeof(ysMap))(ysMap));
46     alias CoordType = typeof(DefaultValues
47         .mergeRange(aes)
48         .mergeRange( Aes!(CoordX, "x", CoordY, "y")
49             (CoordX(xsMap), CoordY(ysMap))));
50 
51     struct GeomRange(T)
52     {
53         this(T aes)
54         {
55             _aes = DefaultValues
56                 .mergeRange(aes)
57                 .mergeRange( Aes!(CoordX, "x", CoordY, "y")(
58                     CoordX(xsMap), CoordY(ysMap)));
59         }
60 
61         @property auto front()
62         {
63             immutable tup = _aes.front;
64             auto f = delegate(cairo.Context context, ColourMap colourMap ) 
65             {
66                 auto devP = context.userToDevice(cairo.Point!double(tup.x[0], tup.y[0]));
67                 context.save();
68                 context.identityMatrix;
69                 context.rectangle(devP.x - 4 * tup.size, 
70                         devP.y - 4 * tup.size, 8*tup.size, 8*tup.size);
71                 context.restore();
72 
73                 auto col = colourMap(ColourID(tup.colour));
74                 import ggplotd.colourspace : RGBA, toCairoRGBA;
75 
76                 context.identityMatrix();
77 
78                 context.setSourceRGBA(
79                     RGBA(col.r, col.g, col.b, tup.alpha).toCairoRGBA);
80                 context.fill();
81 
82                 return context;
83             };
84 
85             AdaptiveBounds bounds;
86             bounds.adapt(Point(tup.x[0], tup.y[0]));
87             auto geom = Geom( tup );
88             geom.draw = f;
89             geom.colours ~= ColourID(tup.colour);
90             geom.bounds = bounds;
91             return geom;
92         }
93 
94         void popFront()
95         {
96             _aes.popFront();
97         }
98 
99         @property bool empty()
100         {
101             return _aes.empty;
102         }
103 
104     private:
105         CoordType _aes;
106     }
107 
108     return GeomRange!AES(aes);
109 }
110 
111 ///
112 unittest
113 {
114     auto aes = Aes!(double[], "x", double[], "y")([1.0], [2.0]);
115     auto gl = geomPoint(aes);
116     assertEqual(gl.front.colours[0][1], "black");
117     gl.popFront;
118     assert(gl.empty);
119 }
120 
121 ///
122 auto geomLine(AES)(AES aes)
123 {
124     import std.algorithm : map;
125     import std.range : array, zip;
126 
127     struct GeomRange(T)
128     {
129         this(T aes)
130         {
131             groupedAes = aes.group;
132         }
133 
134         @property auto front()
135         {
136             auto xs = NumericLabel!(typeof(groupedAes.front.front.x)[])(
137                 groupedAes.front.map!((t) => t.x).array);
138             auto ys = NumericLabel!(typeof(groupedAes.front.front.y)[])(
139                 groupedAes.front.map!((t) => t.y).array);
140             auto coords = zip(xs, ys);
141 
142             immutable flags = groupedAes.front.front;
143             auto f = delegate(cairo.Context context, ColourMap colourMap ) {
144                 auto fr = coords.front;
145                 context.moveTo(fr[0][0], fr[1][0]);
146                 coords.popFront;
147                 foreach (tup; coords)
148                 {
149                     context.lineTo(tup[0][0], tup[1][0]);
150                     context.lineWidth = 2.0*flags.size;
151                 }
152 
153                 auto col = colourMap(ColourID(flags.colour));
154                 import ggplotd.colourspace : RGBA, toCairoRGBA;
155 
156                 context.identityMatrix();
157                 if (flags.fill>0)
158                 {
159                     context.setSourceRGBA(
160                         RGBA(col.r, col.g, col.b, flags.fill)
161                             .toCairoRGBA
162                     );
163                     context.fillPreserve();
164                 }
165                 context.setSourceRGBA(
166                     RGBA(col.r, col.g, col.b, flags.alpha)
167                         .toCairoRGBA
168                 );
169                 context.stroke();
170 
171                 return context;
172             };
173 
174             AdaptiveBounds bounds;
175             coords = zip(xs, ys);
176             auto geom = Geom(groupedAes.front.front);
177             foreach (tup; coords)
178             {
179                 bounds.adapt(Point(tup[0][0], tup[1][0]));
180                 if (!xs.numeric)
181                     geom.xTickLabels ~= tup[0];
182                 if (!ys.numeric)
183                     geom.yTickLabels ~= tup[1];
184             }
185             geom.draw = f;
186             geom.colours ~= ColourID(groupedAes.front.front.colour);
187             geom.bounds = bounds;
188             return geom;
189         }
190 
191         void popFront()
192         {
193             groupedAes.popFront;
194         }
195 
196         @property bool empty()
197         {
198             return groupedAes.empty;
199         }
200 
201     private:
202         typeof(group(T.init)) groupedAes;
203     }
204 
205     return GeomRange!AES(aes);
206 }
207 
208 ///
209 unittest
210 {
211     auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([1.0,
212         2.0, 1.1, 3.0], [3.0, 1.5, 1.1, 1.8], ["a", "b", "a", "b"]);
213 
214     auto gl = geomLine(aes);
215 
216     import std.range : empty;
217 
218     assert(gl.front.xTickLabels.empty);
219     assert(gl.front.yTickLabels.empty);
220 
221     assertEqual(gl.front.colours[0][1], "a");
222     assertEqual(gl.front.bounds.min_x, 1.0);
223     assertEqual(gl.front.bounds.max_x, 1.1);
224     gl.popFront;
225     assertEqual(gl.front.colours[0][1], "b");
226     assertEqual(gl.front.bounds.max_x, 3.0);
227     gl.popFront;
228     assert(gl.empty);
229 }
230 
231 unittest
232 {
233     auto aes = Aes!(string[], "x", string[], "y", string[], "colour")(["a",
234         "b", "c", "b"], ["a", "b", "b", "a"], ["b", "b", "b", "b"]);
235 
236     auto gl = geomLine(aes);
237     assertEqual(gl.front.xTickLabels.length, 4);
238     assertEqual(gl.front.yTickLabels.length, 4);
239 }
240 
241 unittest
242 {
243     auto aes = Aes!(string[], "x", string[], "y", string[], "colour")(["a",
244         "b", "c", "b"], ["a", "b", "b", "a"], ["b", "b", "b", "b"]);
245 
246     auto gl = geomLine(aes);
247     auto aes2 = Aes!(string[], "x", string[], "y", double[], "colour")(["a",
248         "b", "c", "b"], ["a", "b", "b", "a"], [0, 1, 0, 0.1]);
249 
250     auto gl2 = geomLine(aes2);
251 
252     import std.range : chain, walkLength;
253 
254     assertEqual(gl.chain(gl2).walkLength, 4);
255 }
256 
257 // Bin a range of data
258 private auto bin(R)(R xs, double min, double max, size_t noBins = 10)
259 {
260     struct Bin
261     {
262         double[] range;
263         size_t count;
264     }
265 
266     import std.typecons : Tuple;
267     import std.algorithm : group;
268 
269     struct BinRange(Range)
270     {
271         this(Range xs, size_t noBins)
272         {
273             import std.math : floor;
274             import std.algorithm : sort, map;
275             import std.array : array;
276             import std.range : walkLength;
277 
278             _width = (max - min) / (noBins - 1);
279             _noBins = noBins;
280             // If min == max we need to set a custom width
281             if (_width == 0)
282                 _width = 0.1;
283             _min = min - 0.5 * _width;
284 
285             // Count the number of data points that fall in a
286             // bin. This is done by scaling them into whole numbers
287             if (xs.walkLength > 0)
288             {
289                 counts = xs.map!((a) => floor((a - _min) / _width)).array.sort().array.group();
290 
291                 // Initialize our bins
292                 if (counts.front[0] == _binID)
293                     {
294                     _cnt = counts.front[1];
295                     counts.popFront;
296                 }
297             }
298         }
299 
300         /// Return a bin describing the range and number of data points (count) that fall within that range.
301         @property auto front()
302         {
303             return Bin([_min, _min + _width], _cnt);
304         }
305 
306         void popFront()
307         {
308             _min += _width;
309             _cnt = 0;
310             ++_binID;
311             if (!counts.empty && counts.front[0] == _binID)
312             {
313                 _cnt = counts.front[1];
314                 counts.popFront;
315             }
316         }
317 
318         @property bool empty()
319         {
320             return _binID >= _noBins;
321         }
322 
323     private:
324         double _min;
325         double _width;
326         size_t _noBins;
327         size_t _binID = 0;
328         typeof(group(Range.init)) counts;
329         size_t _cnt = 0;
330     }
331 
332     return BinRange!R(xs, noBins);
333 }
334 
335 private auto bin(R)(R xs, size_t noBins = 10)
336 {
337     import std.algorithm : min, max, reduce;
338     import std.range : walkLength;
339     assert(xs.walkLength > 0);
340 
341     auto minmax = xs.reduce!((a, b) => min(a, b), (a, b) => max(a, b));
342     return bin( xs, minmax[0], minmax[1], noBins );
343 }
344  
345 
346 unittest
347 {
348     import std.array : array;
349     import std.range : back, walkLength;
350 
351     auto binR = bin!(double[])([0.5, 0.01, 0.0, 0.9, 1.0, 0.99], 11);
352     assertEqual(binR.walkLength, 11);
353     assertEqual(binR.front.range, [-0.05, 0.05]);
354     assertEqual(binR.front.count, 2);
355     assertLessThan(binR.array.back.range[0], 1);
356     assertGreaterThan(binR.array.back.range[1], 1);
357     assertEqual(binR.array.back.count, 2);
358 
359     binR = bin!(double[])([0.01], 11);
360     assertEqual(binR.walkLength, 11);
361     assertEqual(binR.front.count, 1);
362 
363     binR = bin!(double[])([-0.01, 0, 0, 0, 0.01], 11);
364     assertEqual(binR.walkLength, 11);
365     assertLessThan(binR.front.range[0], -0.01);
366     assertGreaterThan(binR.front.range[1], -0.01);
367     assertEqual(binR.front.count, 1);
368     assertLessThan(binR.array.back.range[0], 0.01);
369     assertGreaterThan(binR.array.back.range[1], 0.01);
370     assertEqual(binR.array.back.count, 1);
371     assertEqual(binR.array[5].count, 3);
372     assertLessThan(binR.array[5].range[0], 0.0);
373     assertGreaterThan(binR.array[5].range[1], 0.0);
374 }
375 
376 
377 /// Draw histograms based on the x coordinates of the data (aes)
378 auto geomHist(AES)(AES aes, size_t noBins = 0)
379 {
380     import std.algorithm : map, max, min;
381     import std.array : Appender, array;
382     import std.range : repeat;
383     import std.typecons : Tuple;
384 
385     // New appender to hold lines for drawing histogram
386     auto appender = Appender!(Geom[])([]);
387 
388     foreach (grouped; group(aes)) // Split data by colour/id
389     {
390 
391         // Extract the x coordinates
392         auto xs = grouped.map!((t) => t.x).array; 
393         if (noBins < 1)
394             noBins = min(30,max(11, xs.length/10));
395         auto bins = xs.bin(noBins); // Bin the data
396 
397         foreach (bin; bins)
398         {
399             // Specifying the boxes for the histogram. The merge is used to keep the colour etc. information
400             // contained in the original merged passed to geomHist.
401             appender.put(
402                 geomLine( [
403                     grouped.front.merge(Tuple!(double, "x", double, "y" )( 
404                             bin.range[0], 0.0 )),
405                     grouped.front.merge(Tuple!(double, "x", double, "y" )( 
406                             bin.range[0], bin.count )),
407                     grouped.front.merge(Tuple!(double, "x", double, "y" )( 
408                             bin.range[1], bin.count )),
409                     grouped.front.merge(Tuple!(double, "x", double, "y" )( 
410                             bin.range[1], 0.0 )),
411                 ] )
412             );
413         }
414     }
415 
416     // Return the different lines 
417     return appender.data;
418 }
419 
420 /// Draw histograms based on the x coordinates of the data (aes)
421 auto geomHist3D(AES)(AES aes, size_t noBinsX = 0, size_t noBinsY = 0)
422 {
423     import std.algorithm : filter, map, reduce, max, min;
424     import std.array : array, Appender;
425     // New appender to hold lines for drawing histogram
426     auto appender = Appender!(Geom[])([]);
427 
428     // Work out min/max of the x and y data
429     auto minmaxX = reduce!("min(a,b.x)","max(a,b.x)")( Tuple!(double,double)(aes.front.x, aes.front.x), aes );
430     auto minmaxY = reduce!("min(a,b.y)","max(a,b.y)")( Tuple!(double,double)(aes.front.y, aes.front.y), aes );
431 
432     // Track maximum z value for colour scaling
433     double maxZ = -1;
434 
435     auto xs = aes.map!((t) => t.x) // Extract the x coordinates
436             .array;
437 
438     if (noBinsX < 1)
439         noBinsX = min(30,max(11, xs.length/25));
440     if (noBinsY < 1)
441         noBinsY = noBinsX;
442 
443 
444     foreach( binX; xs.bin( minmaxX[0], minmaxX[1], noBinsX ) )
445     {
446         // TODO this is not the most efficient way to create 2d bins
447         foreach( binY; aes.filter!( 
448                 (a) => a.x >= binX.range[0] && a.x < binX.range[1] )
449             .map!( (a) => a.y ).array
450             .bin( minmaxY[0], minmaxY[1], noBinsY ) )
451         {
452             maxZ = max( maxZ, binY.count );
453             appender.put(
454                 geomPolygon(
455             [aes.front.merge( 
456                 Tuple!( double, "x", double, "y", double, "colour" )
457                     ( binX.range[0], binY.range[0], binY.count ) ),
458              aes.front.merge( 
459                 Tuple!( double, "x", double, "y", double, "colour" )
460                     ( binX.range[0], binY.range[1], binY.count ) ),
461              aes.front.merge( 
462                 Tuple!( double, "x", double, "y", double, "colour" )
463                     ( binX.range[1], binY.range[1], binY.count ) ),
464              aes.front.merge( 
465                 Tuple!( double, "x", double, "y", double, "colour" )
466                         ( binX.range[1], binY.range[0], binY.count ) )] )
467             );
468         }
469     }
470     // scale colours by max_z
471     return appender.data;
472 }
473 /// Draw axis, first and last location are start/finish
474 /// others are ticks (perpendicular)
475 auto geomAxis(AES)(AES aes, double tickLength, string label)
476 {
477     import std.algorithm : find;
478     import std.array : array;
479     import std.range : chain, empty, repeat;
480     import std.math : sqrt, pow;
481 
482     double[] xs;
483     double[] ys;
484 
485     double[] lxs;
486     double[] lys;
487     double[] langles;
488     string[] lbls;
489 
490     auto merged = DefaultValues.mergeRange(aes);
491 
492     auto colour = merged.front.colour;
493     auto toDir = merged.find!("a.x != b.x || a.y != b.y")(merged.front).front; 
494     auto direction = [toDir.x - merged.front.x, toDir.y - merged.front.y];
495     auto dirLength = sqrt(pow(direction[0], 2) + pow(direction[1], 2));
496     direction[0] *= tickLength / dirLength;
497     direction[1] *= tickLength / dirLength;
498  
499     while (!merged.empty)
500     {
501         auto tick = merged.front;
502         xs ~= tick.x;
503         ys ~= tick.y;
504 
505         merged.popFront;
506 
507         // Draw ticks perpendicular to main axis;
508         if (xs.length > 1 && !merged.empty)
509         {
510             xs ~= [tick.x + direction[1], tick.x];
511             ys ~= [tick.y + direction[0], tick.y];
512 
513             lxs ~= tick.x - 1.5*direction[1];
514             lys ~= tick.y - 1.5*direction[0];
515             lbls ~= tick.label;
516             langles ~= tick.angle;
517         }
518     }
519 
520     // Main label
521     auto xm = xs[0] + 0.5*(xs[$-1]-xs[0]) - 4.0*direction[1];
522     auto ym = ys[0] + 0.5*(ys[$-1]-ys[0]) - 4.0*direction[0];
523     auto aesM = Aes!(double[], "x", double[], "y", string[], "label", 
524         double[], "angle", bool[], "mask")( [xm], [ym], [label], 
525             langles, [false]);
526 
527     return geomLine(Aes!(typeof(xs), "x", typeof(ys), "y", bool[], "mask")(
528         xs, ys, false.repeat(xs.length).array)).chain(
529         geomLabel(Aes!(double[], "x", double[], "y", string[], "label",
530         double[], "angle", bool[], "mask")(lxs, lys, lbls, langles, 
531             false.repeat(lxs.length).array)))
532             .chain( geomLabel(aesM) );
533 }
534 
535 /// Draw Label at given x and y position
536 auto geomLabel(AES)(AES aes)
537 {
538     import std.algorithm : map;
539     auto xsMap = aes.map!("a.x");
540     auto ysMap = aes.map!("a.y");
541     alias CoordX = typeof(NumericLabel!(typeof(xsMap))(xsMap));
542     alias CoordY = typeof(NumericLabel!(typeof(ysMap))(ysMap));
543     alias CoordType = typeof(DefaultValues
544         .mergeRange(aes)
545         .mergeRange( Aes!(CoordX, "x", CoordY, "y")
546             (CoordX(xsMap), CoordY(ysMap))));
547 
548 
549     struct GeomRange(T)
550     {
551         size_t size = 6;
552         this(T aes)
553         {
554             _aes = DefaultValues
555                 .mergeRange(aes)
556                 .mergeRange( Aes!(CoordX, "x", CoordY, "y")(
557                     CoordX(xsMap), CoordY(ysMap)));
558         }
559 
560         @property auto front()
561         {
562             immutable tup = _aes.front;
563             auto f = delegate(cairo.Context context, ColourMap colourMap) {
564                 context.setFontSize(14.0);
565                 context.moveTo(tup.x[0], tup.y[0]);
566                 context.save();
567                 context.identityMatrix;
568                 context.rotate(tup.angle);
569                 auto extents = context.textExtents(tup.label);
570                 auto textSize = cairo.Point!double(0.5 * extents.width, 0.5 * extents.height);
571                 context.relMoveTo(-textSize.x, textSize.y);
572 
573                 auto col = colourMap(ColourID(tup.colour));
574                 import ggplotd.colourspace : RGBA, toCairoRGBA;
575 
576                 context.setSourceRGBA(
577                     RGBA(col.r, col.g, col.b, tup.alpha)
578                         .toCairoRGBA
579                 );
580  
581                 context.showText(tup.label);
582                 context.restore();
583                 return context;
584             };
585 
586             AdaptiveBounds bounds;
587             bounds.adapt(Point(tup.x[0], tup.y[0]));
588 
589             auto geom = Geom( tup );
590             geom.draw = f;
591             geom.colours ~= ColourID(tup.colour);
592             geom.bounds = bounds;
593  
594             return geom;
595         }
596 
597         void popFront()
598         {
599             _aes.popFront();
600         }
601 
602         @property bool empty()
603         {
604             return _aes.empty;
605         }
606 
607     private:
608         CoordType _aes;
609     }
610 
611     return GeomRange!AES(aes);
612 }
613 
614 unittest
615 {
616     auto aes = Aes!(string[], "x", string[], "y", string[], "label")(["a", "b",
617         "c", "b"], ["a", "b", "b", "a"], ["b", "b", "b", "b"]);
618 
619     auto gl = geomLabel(aes);
620     import std.range : walkLength;
621 
622     assertEqual(gl.walkLength, 4);
623 }
624 
625 // geomBox
626 /// Return the limits indicated with different alphas
627 private auto limits( RANGE )( RANGE range, double[] alphas )
628 {
629     import std.algorithm : sort, map, min, max;
630     import std.math : floor;
631     import std.conv : to;
632     auto sorted = range.sort();
633     return alphas.map!( (a) { 
634         auto id = min( sorted.length-2,
635             max(0,floor( a*(sorted.length+1) ).to!int-1 ) );
636         if (a<=0.5)
637             return sorted[id];
638         else
639             return sorted[id+1];
640     });
641 }
642 
643 unittest
644 {
645     import std.range : array, front;
646     assertEqual( [1,2,3,4,5].limits( [0.01, 0.5, 0.99] ).array, 
647             [1,3,5] );
648 
649     assertEqual( [1,2,3,4].limits( [0.41] ).front, 2 );
650     assertEqual( [1,2,3,4].limits( [0.39] ).front, 1 );
651     assertEqual( [1,2,3,4].limits( [0.61] ).front, 4 );
652     assertEqual( [1,2,3,4].limits( [0.59] ).front, 3 );
653 }
654 
655 /// Draw a boxplot. The "x" data is used. If labels are given then the data is grouped by the label
656 auto geomBox(AES)(AES aes)
657 {
658     import std.algorithm : map;
659     import std.array : array;
660     import std.range : Appender;
661 
662     Appender!(Geom[]) result;
663 
664     // If has y, use that
665     auto fr = aes.front;
666     static if (__traits(hasMember, fr, "y"))
667     {
668         auto labels = NumericLabel!(typeof(fr.y)[])( 
669             aes.map!("a.y").array ); // Should use y type
670         auto myAes = aes.mergeRange( Aes!(typeof(labels), "label")( labels ) );
671     } else {
672         static if (__traits(hasMember, fr, "label"))
673         {
674         // esle If has label, use that
675         auto labels = NumericLabel!(string[])( 
676             aes.map!("a.label.to!string").array );
677         auto myAes = aes.mergeRange( Aes!(typeof(labels), "label")( labels ) );
678         } else {
679             import std.range : repeat;
680             auto labels = NumericLabel!(string[])( 
681                 repeat("a", aes.length).array );
682             auto myAes = aes.mergeRange( Aes!(typeof(labels), "label")( labels ) );
683         }
684     }
685     
686     double delta = 0.2;
687     Tuple!(double, string)[] xTickLabels;
688 
689     foreach( grouped; myAes.group() )
690     {
691         auto lims = grouped.map!("a.x")
692             .array.limits( [0.1,0.25,0.5,0.75,0.9] ).array;
693         auto x = grouped.front.label[0];
694         xTickLabels ~= grouped.front.label;
695         // TODO this should be some kind of loop
696         result.put(
697             geomLine( [
698                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
699                     x, lims[0] )),
700                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
701                     x, lims[1] )),
702                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
703                     x+delta, lims[1] )),
704                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
705                     x+delta, lims[2] )),
706                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
707                     x-delta, lims[2] )),
708                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
709                     x-delta, lims[3] )),
710                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
711                     x, lims[3] )),
712                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
713                     x, lims[4] )),
714 
715                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
716                     x, lims[3] )),
717                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
718                     x+delta, lims[3] )),
719                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
720                     x+delta, lims[2] )),
721                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
722                     x-delta, lims[2] )),
723                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
724                     x-delta, lims[1] )),
725                 grouped.front.merge(Tuple!(double, "x", double, "y" )( 
726                     x, lims[1] ))
727              ] )
728         );
729     }
730 
731     import std.algorithm : sort;
732     xTickLabels = xTickLabels.sort!((a,b) => a[0] < b[0]).array;
733 
734     foreach( ref g; result.data )
735     {
736         g.xTickLabels = xTickLabels;
737         g.bounds.min_x = xTickLabels.front[0] - 0.5;
738         g.bounds.max_x = xTickLabels[$-1][0] + 0.5;
739     }
740 
741     return result.data;
742 }
743 
744 ///
745 unittest 
746 {
747     import std.array : array;
748     import std.algorithm : map;
749     import std.range : repeat, iota, chain;
750     import std.random : uniform;
751     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
752     auto cols = "a".repeat(25).chain("b".repeat(25)).array;
753     auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 
754         double[], "fill", typeof(cols), "label" )( 
755             xs, cols, 0.45.repeat(xs.length).array, cols);
756     auto gb = geomBox( aes );
757     assertEqual( gb.front.bounds.min_x, -0.5 );
758 }
759 
760 unittest 
761 {
762     import std.array : array;
763     import std.algorithm : map;
764     import std.range : repeat, iota, chain;
765     import std.random : uniform;
766     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
767     auto cols = "a".repeat(25).chain("b".repeat(25)).array;
768     auto ys = 2.repeat(25).chain(3.repeat(25)).array;
769     auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 
770         double[], "fill", typeof(ys), "y" )( 
771             xs, cols, 0.45.repeat(xs.length).array, ys);
772     auto gb = geomBox( aes );
773     assertEqual( gb.front.bounds.min_x, 1.5 );
774 }
775 
776 unittest 
777 {
778     import std.array : array;
779     import std.algorithm : map;
780     import std.range : repeat, iota, chain;
781     import std.random : uniform;
782     auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array;
783     auto cols = "a".repeat(25).chain("b".repeat(25)).array;
784     auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 
785         double[], "fill")( 
786             xs, cols, 0.45.repeat(xs.length).array);
787     auto gb = geomBox( aes );
788     assertEqual( gb.front.bounds.min_x, -0.5 );
789 }
790 
791 ///
792 auto geomPolygon(AES)(AES aes)
793 {
794     import std.array : array;
795     import std.algorithm : map, swap;
796     import std.conv : to;
797     import ggplotd.geometry;
798 
799     auto merged = DefaultValues.mergeRange(aes);
800     // Turn into vertices.
801     static if (is(typeof(merged.front.colour)==ColourID))
802         auto vertices = merged.map!( (t) => Vertex3D( t.x.to!double, t.y.to!double, 
803                     t.colour[0] ) );
804     else
805         auto vertices = merged.map!( (t) => Vertex3D( t.x.to!double, t.y.to!double, 
806                     t.colour.to!double ) );
807 
808     // Find lowest, highest
809     auto triangle = vertices.array;
810     if (triangle[1].z < triangle[0].z)
811         swap( triangle[1], triangle[0] );
812     if (triangle[2].z < triangle[0].z)
813         swap( triangle[2], triangle[0] );
814     if (triangle[1].z > triangle[2].z)
815         swap( triangle[1], triangle[2] );
816 
817     if (triangle.length > 3)
818         foreach( v; triangle[3..$] )
819         {
820             if (v.z < triangle[0].z)
821                 swap( triangle[0], v );
822             else if ( v.z > triangle[2].z )
823                 swap( triangle[2], v );
824         }
825     auto gV = gradientVector( triangle[0..3] );
826 
827     immutable flags = merged.front;
828 
829     auto geom = Geom( flags );
830 
831     foreach( v; vertices )
832         geom.bounds.adapt(Point(v.x, v.y));
833 
834     // Define drawFunction
835     auto f = delegate(cairo.Context context, ColourMap colourMap ) 
836     {
837         auto gradient = new cairo.LinearGradient( gV[0].x, gV[0].y, 
838             gV[1].x, gV[1].y );
839 
840         auto col0 = colourMap(ColourID(gV[0].z));
841         auto col1 = colourMap(ColourID(gV[1].z));
842         import ggplotd.colourspace : RGBA, toCairoRGBA;
843         gradient.addColorStopRGBA( 0,
844             RGBA(col0.r, col0.g, col0.b, flags.alpha)
845                 .toCairoRGBA
846         );
847         gradient.addColorStopRGBA( 1,
848             RGBA(col1.r, col1.g, col1.b, flags.alpha)
849                 .toCairoRGBA
850         );
851         context.moveTo( vertices.front.x, vertices.front.y );
852         vertices.popFront;
853         foreach( v; vertices )
854             context.lineTo( v.x, v.y );
855         context.closePath;
856         context.setSource( gradient );
857         context.fillPreserve;
858         context.identityMatrix();
859         context.stroke;
860         return context;
861     };
862 
863     geom.draw = f;
864 
865     geom.colours = merged.map!((t) => ColourID(t.colour)).array;
866 
867     return [geom];
868 }