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 }