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