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