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