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 }