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 10 version (unittest) 11 { 12 import dunit.toolkit; 13 } 14 15 version (assert) 16 { 17 import std.stdio : writeln; 18 } 19 20 /// Hold the data needed to draw to a plot context 21 struct Geom 22 { 23 import std.typecons : Nullable; 24 25 /// Construct from a tuple 26 this(T)( in T tup ) //if (is(T==Tuple)) 27 { 28 import ggplotd.aes : hasAesField; 29 static if (hasAesField!(T, "x")) 30 xStore.put(tup.x); 31 static if (hasAesField!(T, "y")) 32 yStore.put(tup.y); 33 static if (hasAesField!(T, "colour")) 34 colourStore.put(tup.colour); 35 static if (hasAesField!(T, "sizeStore")) 36 sizeStore.put(tup.sizeStore); 37 mask = tup.mask; 38 } 39 40 import ggplotd.guide : GuideToColourFunction, GuideToDoubleFunction; 41 /// Delegate that takes a context and draws to it 42 alias drawFunction = cairo.Context delegate(cairo.Context context, 43 in GuideToDoubleFunction xFunc, in GuideToDoubleFunction yFunc, 44 in GuideToColourFunction cFunc, in GuideToDoubleFunction sFunc); 45 46 /// Function to draw to a cairo context 47 Nullable!drawFunction draw; 48 49 import ggplotd.guide : GuideStore; 50 GuideStore!"colour" colourStore; 51 GuideStore!"x" xStore; 52 GuideStore!"y" yStore; 53 GuideStore!"size" sizeStore; 54 55 /// Whether to mask/prevent drawing outside plotting area 56 bool mask = true; 57 } 58 59 import ggplotd.colourspace : RGBA; 60 private auto fillAndStroke( cairo.Context context, in RGBA colour, 61 in double fill, in double alpha ) 62 { 63 import ggplotd.colourspace : toCairoRGBA; 64 context.save; 65 66 context.identityMatrix(); 67 if (fill>0) 68 { 69 context.setSourceRGBA( 70 RGBA(colour.r, colour.g, colour.b, fill).toCairoRGBA 71 ); 72 context.fillPreserve(); 73 } 74 context.setSourceRGBA( 75 RGBA(colour.r, colour.g, colour.b, alpha).toCairoRGBA 76 ); 77 context.stroke(); 78 context.restore; 79 return context; 80 } 81 82 /++ 83 General function for drawing geomShapes 84 +/ 85 private template geomShape( string shape, AES ) 86 { 87 import std.algorithm : map; 88 import ggplotd.range : mergeRange; 89 alias CoordType = typeof(DefaultValues 90 .mergeRange(AES.init)); 91 92 struct VolderMort 93 { 94 this(AES aes) 95 { 96 import ggplotd.range : mergeRange; 97 _aes = DefaultValues 98 .mergeRange(aes); 99 } 100 101 @property auto front() 102 { 103 import ggplotd.guide : GuideToDoubleFunction, GuideToColourFunction; 104 immutable tup = _aes.front; 105 immutable f = delegate(cairo.Context context, 106 in GuideToDoubleFunction xFunc, in GuideToDoubleFunction yFunc, 107 in GuideToColourFunction cFunc, in GuideToDoubleFunction sFunc ) { 108 import std.math : isFinite; 109 auto x = xFunc(tup.x); 110 auto y = yFunc(tup.y); 111 auto col = cFunc(tup.colour); 112 if (!isFinite(x) || !isFinite(y)) 113 return context; 114 context.save(); 115 context.translate(x, y); 116 import ggplotd.aes : hasAesField; 117 static if (hasAesField!(typeof(tup), "sizeStore")) { 118 auto width = tup.width*sFunc(tup.sizeStore); 119 auto height = tup.height*sFunc(tup.sizeStore); 120 } else { 121 auto width = tup.width; 122 auto height = tup.height; 123 } 124 125 static if (is(typeof(tup.width)==immutable(Pixel))) 126 auto devP = context.deviceToUserDistance(cairo.Point!double( width, height )); //tup.width.to!double, tup.width.to!double )); 127 context.rotate(tup.angle); 128 static if (shape=="ellipse") 129 { 130 import std.math : PI; 131 static if (is(typeof(tup.width)==immutable(Pixel))) 132 { 133 context.scale( devP.x/2.0, devP.y/2.0 ); 134 } else { 135 context.scale( width/2.0, height/2.0 ); 136 } 137 context.arc(0,0, 1.0, 0,2*PI); 138 } else { 139 static if (is(typeof(tup.width)==immutable(Pixel))) 140 { 141 context.scale( devP.x, devP.y ); 142 } else { 143 context.scale( width, height ); 144 } 145 static if (shape=="triangle") 146 { 147 context.moveTo( -0.5, -0.5 ); 148 context.lineTo( 0.5, -0.5 ); 149 context.lineTo( 0, 0.5 ); 150 } else static if (shape=="diamond") { 151 context.moveTo( 0, -0.5 ); 152 context.lineTo( 0.5, 0 ); 153 context.lineTo( 0, 0.5 ); 154 context.lineTo( -0.5, 0 ); 155 } else { 156 context.moveTo( -0.5, -0.5 ); 157 context.lineTo( -0.5, 0.5 ); 158 context.lineTo( 0.5, 0.5 ); 159 context.lineTo( 0.5, -0.5 ); 160 } 161 context.closePath; 162 } 163 164 context.restore(); 165 context.fillAndStroke( col, tup.fill, tup.alpha ); 166 return context; 167 }; 168 169 auto geom = Geom( tup ); 170 geom.draw = f; 171 172 static if (!is(typeof(tup.width)==immutable(Pixel))) 173 { 174 geom.xStore.put(tup.x, 0.5*tup.width); 175 geom.xStore.put(tup.x, -0.5*tup.width); 176 } 177 static if (!is(typeof(tup.height)==immutable(Pixel))) 178 { 179 geom.yStore.put(tup.y, 0.5*tup.height); 180 geom.yStore.put(tup.y, -0.5*tup.height); 181 } 182 183 return geom; 184 } 185 186 void popFront() 187 { 188 _aes.popFront(); 189 } 190 191 @property bool empty() 192 { 193 return _aes.empty; 194 } 195 196 private: 197 CoordType _aes; 198 } 199 200 auto geomShape(AES aes) 201 { 202 return VolderMort(aes); 203 } 204 } 205 206 unittest 207 { 208 import std.range : walkLength, zip; 209 import std.algorithm : map; 210 211 import ggplotd.aes : aes; 212 auto aesRange = zip([1.0, 2.0], [3.0, 4.0], [1.0,1], [2.0,2]) 213 .map!((a) => aes!("x", "y", "width", "height")( a[0], a[1], a[2], a[3])); 214 auto geoms = geomShape!("rectangle")(aesRange); 215 216 assertEqual(geoms.walkLength, 2); 217 assertEqual(geoms.front.xStore.min, 0.5); 218 assertEqual(geoms.front.xStore.max, 1.5); 219 geoms.popFront; 220 assertEqual(geoms.front.xStore.max, 2.5); 221 } 222 223 /** 224 Draw any type of geom 225 226 The type field is required, which should be a string. Any of the geom* functions in ggplotd.geom 227 can be passed using a lower case string minus the geom prefix, i.e. hist2d calls geomHist2D etc. 228 229 Examples: 230 -------------- 231 import ggplotd.geom : geomType; 232 geomType(Aes!(double[], "x", double[], "y", string[], "type") 233 ( [0.0,1,2], [5.0,6,7], ["line", "point", "line"] )); 234 -------------- 235 236 */ 237 template geomType(AES) 238 { 239 string injectToGeom() 240 { 241 import std.format : format; 242 import std.traits; 243 import std.string : toLower; 244 string str = "auto toGeom(A)( A aes, string type ) {\nimport std.traits; import std.array : array;\n"; 245 foreach( name; __traits(allMembers, ggplotd.geom) ) 246 { 247 static if (name.length > 6 && name[0..4] == "geom" 248 && name != "geomType" 249 ) 250 { 251 str ~= format( "static if(__traits(compiles,(A a) => %s(a))) {\nif (type == q{%s})\n\treturn %s!A(aes).array;\n}\n", name, name[4..$].toLower, name ); 252 } 253 } 254 255 str ~= "assert(0, q{Unknown type passed to geomType});\n}\n"; 256 return str; 257 } 258 259 /** 260 Draw any type of geom 261 262 The type field is required, which should be a string. Any of the geom* functions in ggplotd.geom 263 can be passed using a lower case string minus the geom prefix, i.e. hist2d calls geomHist2D etc. 264 */ 265 auto geomType( AES aes ) 266 { 267 import std.algorithm : map, joiner; 268 269 import ggplotd.aes : group; 270 mixin(injectToGeom()); 271 272 return aes 273 .group!"type" 274 .map!((g) => toGeom(g, g[0].type)).joiner; 275 } 276 } 277 278 /// 279 unittest 280 { 281 import std.range : walkLength; 282 assertEqual( 283 geomType(Aes!(double[], "x", double[], "y", string[], "type") 284 ( [0.0,1,2], [5.0,6,7], ["line", "point", "line"] )).walkLength, 2 285 ); 286 } 287 288 /** 289 Draw rectangle centered at given x,y location 290 291 Aside from x and y also width and height are required. 292 If the type of width is of type Pixel (see aes.d) then dimensions are assumed to be in Pixel (not user coordinates). 293 */ 294 auto geomRectangle(AES)(AES aes) 295 { 296 return geomShape!("rectangle", AES)(aes); 297 } 298 299 /** 300 Draw ellipse centered at given x,y location 301 302 Aside from x and y also width and height are required. 303 If the type of width is of type Pixel (see aes.d) then dimensions are assumed to be in Pixel (not user coordinates). 304 */ 305 auto geomEllipse(AES)(AES aes) 306 { 307 return geomShape!("ellipse", AES)(aes); 308 } 309 310 /** 311 Draw triangle centered at given x,y location 312 313 Aside from x and y also width and height are required. 314 If the type of width is of type Pixel (see aes.d) then dimensions are assumed to be in Pixel (not user coordinates). 315 */ 316 auto geomTriangle(AES)(AES aes) 317 { 318 return geomShape!("triangle", AES)(aes); 319 } 320 321 /** 322 Draw diamond centered at given x,y location 323 324 Aside from x and y also width and height are required. 325 If the type of width is of type Pixel (see aes.d) then dimensions are assumed to be in Pixel (not user coordinates). 326 */ 327 auto geomDiamond(AES)(AES aes) 328 { 329 return geomShape!("diamond", AES)(aes); 330 } 331 332 /// Create points from the data 333 auto geomPoint(AES)(AES aesRange) 334 { 335 import std.algorithm : map; 336 import ggplotd.aes : aes, Pixel; 337 import ggplotd.range : mergeRange; 338 return DefaultValues 339 .mergeRange(aesRange) 340 .map!((a) => a.merge(aes!("sizeStore", "width", "height", "fill") 341 (a.size, Pixel(8), Pixel(8), a.alpha))) 342 .geomEllipse; 343 } 344 345 /// 346 unittest 347 { 348 auto aes = Aes!(double[], "x", double[], "y")([1.0], [2.0]); 349 auto gl = geomPoint(aes); 350 gl.popFront; 351 assert(gl.empty); 352 } 353 354 /// Create lines from data 355 template geomLine(AES) 356 { 357 import std.algorithm : map; 358 import std.range : array, zip; 359 360 import ggplotd.range : mergeRange; 361 362 struct VolderMort 363 { 364 this(AES aes) 365 { 366 groupedAes = DefaultValues.mergeRange(aes).group; 367 } 368 369 @property auto front() 370 { 371 import ggplotd.aes : aes; 372 import ggplotd.guide : GuideToColourFunction, GuideToDoubleFunction; 373 auto coordsZip = groupedAes.front 374 .map!((a) => aes!("x","y")(a.x, a.y)); 375 376 immutable flags = groupedAes.front.front; 377 immutable f = delegate(cairo.Context context, 378 in GuideToDoubleFunction xFunc, in GuideToDoubleFunction yFunc, 379 in GuideToColourFunction cFunc, in GuideToDoubleFunction sFunc ) { 380 381 import std.math : isFinite; 382 auto coords = coordsZip.save; 383 auto fr = coords.front; 384 context.moveTo(xFunc(fr.x), yFunc(fr.y)); 385 coords.popFront; 386 foreach (tup; coords) 387 { 388 auto x = xFunc(tup.x); 389 auto y = yFunc(tup.y); 390 // TODO should we actually move to next coordinate here? 391 if (isFinite(x) && isFinite(y)) 392 { 393 context.lineTo(x, y); 394 context.lineWidth = 2.0*flags.size; 395 } else { 396 context.newSubPath(); 397 } 398 } 399 400 auto col = cFunc(flags.colour); 401 context.fillAndStroke( col, flags.fill, flags.alpha ); 402 return context; 403 }; 404 405 406 auto geom = Geom(groupedAes.front.front); 407 foreach (tup; coordsZip) 408 { 409 geom.xStore.put(tup.x); 410 geom.yStore.put(tup.y); 411 } 412 geom.draw = f; 413 return geom; 414 } 415 416 void popFront() 417 { 418 groupedAes.popFront; 419 } 420 421 @property bool empty() 422 { 423 return groupedAes.empty; 424 } 425 426 private: 427 typeof(group(DefaultValues.mergeRange(AES.init))) groupedAes; 428 } 429 430 auto geomLine(AES aes) 431 { 432 return VolderMort(aes); 433 } 434 } 435 436 /// 437 unittest 438 { 439 auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([1.0, 440 2.0, 1.1, 3.0], [3.0, 1.5, 1.1, 1.8], ["a", "b", "a", "b"]); 441 442 auto gl = geomLine(aes); 443 444 import std.range : empty; 445 446 assertEqual(gl.front.xStore.min(), 1.0); 447 assertEqual(gl.front.xStore.max(), 1.1); 448 gl.popFront; 449 assertEqual(gl.front.xStore.max(), 3.0); 450 gl.popFront; 451 assert(gl.empty); 452 } 453 454 unittest 455 { 456 auto aes = Aes!(string[], "x", string[], "y", string[], "colour")(["a", 457 "b", "c", "b"], ["a", "b", "b", "a"], ["b", "b", "b", "b"]); 458 459 auto gl = geomLine(aes); 460 assertEqual(gl.front.xStore.store.length, 3); 461 assertEqual(gl.front.yStore.store.length, 2); 462 } 463 464 unittest 465 { 466 auto aes = Aes!(string[], "x", string[], "y", string[], "colour")(["a", 467 "b", "c", "b"], ["a", "b", "b", "a"], ["b", "b", "b", "b"]); 468 469 auto gl = geomLine(aes); 470 auto aes2 = Aes!(string[], "x", string[], "y", double[], "colour")(["a", 471 "b", "c", "b"], ["a", "b", "b", "a"], [0, 1, 0, 0.1]); 472 473 auto gl2 = geomLine(aes2); 474 475 import std.range : chain, walkLength; 476 477 assertEqual(gl.chain(gl2).walkLength, 4); 478 } 479 480 /// Draw histograms based on the x coordinates of the data 481 auto geomHist(AES)(AES aes, size_t noBins = 0) 482 { 483 import ggplotd.stat : statHist; 484 return geomRectangle( statHist( aes, noBins ) ); 485 } 486 487 /** 488 Draw histograms based on the x and y coordinates of the data 489 490 Examples: 491 -------------- 492 /// http://blackedder.github.io/ggplotd/images/hist2D.svg 493 import std.array : array; 494 import std.algorithm : map; 495 import std.conv : to; 496 import std.range : repeat, iota; 497 import std.random : uniform; 498 499 import ggplotd.aes : Aes; 500 import ggplotd.colour : colourGradient; 501 import ggplotd.colourspace : XYZ; 502 import ggplotd.geom : geomHist2D; 503 import ggplotd.ggplotd : GGPlotD; 504 505 auto xs = iota(0,500,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)) 506 .array; 507 auto ys = iota(0,500,1).map!((y) => uniform(0.0,5)+uniform(0.0,5)) 508 .array; 509 auto aes = Aes!(typeof(xs), "x", typeof(ys), "y")( xs, ys); 510 auto gg = GGPlotD().put( geomHist2D( aes ) ); 511 // Use a different colour scheme 512 gg.put( colourGradient!XYZ( "white-cornflowerBlue-crimson" ) ); 513 514 gg.save( "hist2D.svg" ); 515 -------------- 516 */ 517 auto geomHist2D(AES)(AES aes, size_t noBinsX = 0, size_t noBinsY = 0) 518 { 519 import std.algorithm : map, joiner; 520 import ggplotd.stat : statHist2D; 521 522 return statHist2D( aes, noBinsX, noBinsY ) 523 .map!( (poly) => geomPolygon( poly ) ).joiner; 524 } 525 526 527 /** 528 Deprecated: superseded by geomHist2D 529 */ 530 deprecated alias geomHist3D = geomHist2D; 531 532 /// Draw axis, first and last location are start/finish 533 /// others are ticks (perpendicular) 534 auto geomAxis(AES)(AES aes, double tickLength, string label) 535 { 536 import std.algorithm : find; 537 import std.array : array; 538 import std.range : chain, empty, repeat; 539 import std.math : sqrt, pow; 540 541 import ggplotd.range : mergeRange; 542 543 double[] xs; 544 double[] ys; 545 546 double[] lxs; 547 double[] lys; 548 double[] langles; 549 string[] lbls; 550 551 auto merged = DefaultValues.mergeRange(aes); 552 553 immutable toDir = 554 merged.find!("a.x != b.x || a.y != b.y")(merged.front).front; 555 auto direction = [toDir.x - merged.front.x, toDir.y - merged.front.y]; 556 immutable dirLength = sqrt(pow(direction[0], 2) + pow(direction[1], 2)); 557 direction[0] *= tickLength / dirLength; 558 direction[1] *= tickLength / dirLength; 559 560 while (!merged.empty) 561 { 562 auto tick = merged.front; 563 xs ~= tick.x; 564 ys ~= tick.y; 565 566 merged.popFront; 567 568 // Draw ticks perpendicular to main axis; 569 if (xs.length > 1 && !merged.empty) 570 { 571 xs ~= [tick.x + direction[1], tick.x]; 572 ys ~= [tick.y + direction[0], tick.y]; 573 574 lxs ~= tick.x - 1.3*direction[1]; 575 lys ~= tick.y - 1.3*direction[0]; 576 lbls ~= tick.label; 577 langles ~= tick.angle; 578 } 579 } 580 581 // Main label 582 auto xm = xs[0] + 0.5*(xs[$-1]-xs[0]) - 4.0*direction[1]; 583 auto ym = ys[0] + 0.5*(ys[$-1]-ys[0]) - 4.0*direction[0]; 584 auto aesM = Aes!(double[], "x", double[], "y", string[], "label", 585 double[], "angle", bool[], "mask")( [xm], [ym], [label], 586 langles, [false]); 587 588 return geomLine(Aes!(typeof(xs), "x", typeof(ys), "y", bool[], "mask")( 589 xs, ys, false.repeat(xs.length).array)).chain( 590 geomLabel(Aes!(double[], "x", double[], "y", string[], "label", 591 double[], "angle", bool[], "mask", double[], "size")(lxs, lys, lbls, langles, 592 false.repeat(lxs.length).array, aes.front.size.repeat(lxs.length).array))) 593 .chain( geomLabel(aesM) ); 594 } 595 596 /** 597 Draw Label at given x and y position 598 599 You can specify justification, by passing a justify field in the passed data (aes). 600 $(UL 601 $(LI "center" (default)) 602 $(LI "left") 603 $(LI "right") 604 $(LI "bottom") 605 $(LI "top")) 606 */ 607 template geomLabel(AES) 608 { 609 import std.algorithm : map; 610 import std.typecons : Tuple; 611 import ggplotd.range : mergeRange; 612 alias CoordType = typeof(DefaultValues 613 .merge(Tuple!(string, "justify").init) 614 .mergeRange(AES.init)); 615 616 struct VolderMort 617 { 618 this(AES aes) 619 { 620 import std.algorithm : map; 621 import ggplotd.range : mergeRange; 622 623 _aes = DefaultValues 624 .merge(Tuple!(string, "justify")("center")) 625 .mergeRange(aes); 626 } 627 628 @property auto front() 629 { 630 import ggplotd.guide : GuideToDoubleFunction, GuideToColourFunction; 631 immutable tup = _aes.front; 632 immutable f = delegate(cairo.Context context, 633 in GuideToDoubleFunction xFunc, in GuideToDoubleFunction yFunc, 634 in GuideToColourFunction cFunc, in GuideToDoubleFunction sFunc ) { 635 auto x = xFunc(tup.x); 636 auto y = yFunc(tup.y); 637 auto col = cFunc(tup.colour); 638 import std.math : ceil, isFinite; 639 if (!isFinite(x) || !isFinite(y)) 640 return context; 641 context.setFontSize(ceil(14.0*tup.size)); 642 context.moveTo(x, y); 643 context.save(); 644 context.identityMatrix; 645 context.rotate(tup.angle); 646 auto extents = context.textExtents(tup.label); 647 auto textSize = cairo.Point!double(extents.width, extents.height); 648 // Justify 649 if (tup.justify == "left") 650 context.relMoveTo(0, 0.5*textSize.y); 651 else if (tup.justify == "right") 652 context.relMoveTo(-textSize.x, 0.5*textSize.y); 653 else if (tup.justify == "bottom") 654 context.relMoveTo(-0.5*textSize.x, 0); 655 else if (tup.justify == "top") 656 context.relMoveTo(-0.5*textSize.x, textSize.y); 657 else 658 context.relMoveTo(-0.5*textSize.x, 0.5*textSize.y); 659 660 import ggplotd.colourspace : RGBA, toCairoRGBA; 661 662 context.setSourceRGBA( 663 RGBA(col.r, col.g, col.b, tup.alpha) 664 .toCairoRGBA 665 ); 666 667 context.showText(tup.label); 668 context.restore(); 669 return context; 670 }; 671 672 auto geom = Geom( tup ); 673 geom.draw = f; 674 675 return geom; 676 } 677 678 void popFront() 679 { 680 _aes.popFront(); 681 } 682 683 @property bool empty() 684 { 685 return _aes.empty; 686 } 687 688 private: 689 CoordType _aes; 690 } 691 692 auto geomLabel(AES aes) 693 { 694 return VolderMort(aes); 695 } 696 } 697 698 unittest 699 { 700 auto aes = Aes!(string[], "x", string[], "y", string[], "label")(["a", "b", 701 "c", "b"], ["a", "b", "b", "a"], ["b", "b", "b", "b"]); 702 703 auto gl = geomLabel(aes); 704 import std.range : walkLength; 705 706 assertEqual(gl.walkLength, 4); 707 } 708 709 // geomBox 710 /// Return the limits indicated with different alphas 711 private auto limits( RANGE )( RANGE range, double[] alphas ) 712 { 713 import std.algorithm : sort, map, min, max; 714 import std.math : floor; 715 import std.conv : to; 716 auto sorted = range.sort(); 717 return alphas.map!( (a) { 718 auto id = min( sorted.length.to!int-2, 719 max(0,floor( a*(sorted.length+1) ).to!int-1 ) ); 720 assert( id >= 0 ); 721 if (a<=0.5) 722 return sorted[id]; 723 else 724 return sorted[id+1]; 725 }); 726 } 727 728 unittest 729 { 730 import std.range : array, front; 731 assertEqual( [1,2,3,4,5].limits( [0.01, 0.5, 0.99] ).array, 732 [1,3,5] ); 733 734 assertEqual( [1,2,3,4].limits( [0.41] ).front, 2 ); 735 assertEqual( [1,2,3,4].limits( [0.39] ).front, 1 ); 736 assertEqual( [1,2,3,4].limits( [0.61] ).front, 4 ); 737 assertEqual( [1,2,3,4].limits( [0.59] ).front, 3 ); 738 } 739 740 /// Draw a boxplot. The "x" data is used. If labels are given then the data is grouped by the label 741 auto geomBox(AES)(AES aesRange) 742 { 743 import std.algorithm : filter, map; 744 import std.array : array; 745 import std.range : Appender, walkLength, ElementType; 746 import std.typecons : Tuple; 747 import ggplotd.aes : aes, hasAesField; 748 import ggplotd.range : mergeRange; 749 750 Appender!(Geom[]) result; 751 752 // If has y, use that 753 static if (hasAesField!(ElementType!AES, "y")) 754 { 755 auto myAes = aesRange.map!((a) => a.merge(aes!("label")(a.y))); 756 } else { 757 static if (!hasAesField!(ElementType!AES, "label")) 758 { 759 import std.range : repeat, walkLength; 760 auto myAes = aesRange.map!((a) => a.merge(aes!("label")(0.0))); 761 } else { 762 auto myAes = aesRange; 763 } 764 } 765 766 // TODO if x (y in the original aesRange) is numerical then this should relly scale 767 // by the range 768 double delta = 0.2; 769 770 foreach( grouped; myAes.group().filter!((a) => a.walkLength > 3) ) 771 { 772 auto lims = grouped.map!("a.x.to!double") 773 .array.limits( [0.1,0.25,0.5,0.75,0.9] ).array; 774 auto x = grouped.front.label; 775 result.put( 776 [grouped.front.merge(aes!("x", "y", "width", "height") 777 (x, (lims[2]+lims[1])/2.0, 2*delta, lims[2]-lims[1])), 778 grouped.front.merge(aes!("x", "y", "width", "height") 779 (x, (lims[3]+lims[2])/2.0, 2*delta, lims[3]-lims[2])) 780 ].geomRectangle 781 ); 782 783 result.put( 784 [grouped.front.merge(aes!("x", "y")(x,lims[0])), 785 grouped.front.merge(aes!("x", "y")(x,lims[1]))].geomLine); 786 result.put( 787 [grouped.front.merge(aes!("x", "y")(x,lims[3])), 788 grouped.front.merge(aes!("x", "y")(x,lims[4]))].geomLine); 789 790 // Increase plot bounds 791 result.data.front.xStore.put(x, 2*delta); 792 result.data.front.xStore.put(x, -2*delta); 793 } 794 795 return result.data; 796 } 797 798 /// 799 unittest 800 { 801 import std.array : array; 802 import std.algorithm : map; 803 import std.range : repeat, iota, chain, zip; 804 import std.random : uniform; 805 auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array; 806 auto cols = "a".repeat(25).chain("b".repeat(25)).array; 807 auto aesRange = zip(xs, cols) 808 .map!((a) => aes!("x", "colour", "fill", "label")(a[0], a[1], 0.45, a[1])); 809 auto gb = geomBox( aesRange ); 810 assertEqual( gb.front.xStore.min(), -0.4 ); 811 } 812 813 unittest 814 { 815 import std.array : array; 816 import std.algorithm : map; 817 import std.range : repeat, iota, chain, zip; 818 import std.random : uniform; 819 auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array; 820 auto cols = "a".repeat(25).chain("b".repeat(25)).array; 821 auto ys = 2.repeat(25).chain(3.repeat(25)).array; 822 auto aesRange = zip(xs, cols, ys) 823 .map!((a) => aes!("x", "colour", "fill", "y")(a[0], a[1], .45, a[2])); 824 auto gb = geomBox( aesRange ); 825 assertEqual( gb.front.xStore.min, 1.6 ); 826 } 827 828 unittest 829 { 830 // Test when passing one data point 831 import std.array : array; 832 import std.algorithm : map; 833 import std.range : repeat, iota, chain; 834 import std.random : uniform; 835 auto xs = iota(0,1,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array; 836 auto cols = "a".repeat(1).array; 837 auto ys = 2.repeat(1).array; 838 auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 839 double[], "fill", typeof(ys), "y" )( 840 xs, cols, 0.45.repeat(xs.length).array, ys); 841 auto gb = geomBox( aes ); 842 assertEqual( gb.length, 0 ); 843 } 844 845 unittest 846 { 847 import std.array : array; 848 import std.algorithm : map; 849 import std.range : repeat, iota, chain, zip; 850 import std.random : uniform; 851 auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array; 852 auto cols = "a".repeat(25).chain("b".repeat(25)).array; 853 auto aesRange = zip(xs, cols) 854 .map!((a) => aes!("x", "colour", "fill")(a[0], a[1], .45)); 855 auto gb = geomBox( aesRange ); 856 assertEqual( gb.front.xStore.min, -0.4 ); 857 } 858 859 /// Draw a polygon 860 auto geomPolygon(AES)(AES aes) 861 { 862 // TODO would be nice to allow grouping of triangles 863 import std.array : array; 864 import std.algorithm : map, swap; 865 import std.conv : to; 866 import ggplotd.geometry : gradientVector, Vertex3D; 867 import ggplotd.range : mergeRange; 868 869 auto merged = DefaultValues.mergeRange(aes); 870 871 immutable flags = merged.front; 872 873 auto geom = Geom( flags ); 874 875 foreach(tup; merged) 876 { 877 geom.xStore.put(tup.x); 878 geom.yStore.put(tup.y); 879 geom.colourStore.put(tup.colour); 880 } 881 882 import ggplotd.guide : GuideToDoubleFunction, GuideToColourFunction; 883 // Define drawFunction 884 immutable f = delegate(cairo.Context context, 885 in GuideToDoubleFunction xFunc, in GuideToDoubleFunction yFunc, 886 in GuideToColourFunction cFunc, in GuideToDoubleFunction sFunc ) 887 { 888 // Turn into vertices. 889 auto vertices = merged.map!((t) => Vertex3D( xFunc(t.x), yFunc(t.y), 890 cFunc.toDouble(t.colour))); 891 892 // Find lowest, highest 893 auto triangle = vertices.array; 894 if (triangle[1].z < triangle[0].z) 895 swap( triangle[1], triangle[0] ); 896 if (triangle[2].z < triangle[0].z) 897 swap( triangle[2], triangle[0] ); 898 if (triangle[1].z > triangle[2].z) 899 swap( triangle[1], triangle[2] ); 900 901 if (triangle.length > 3) 902 { 903 foreach( v; triangle[3..$] ) 904 { 905 if (v.z < triangle[0].z) 906 swap( triangle[0], v ); 907 else if ( v.z > triangle[2].z ) 908 swap( triangle[2], v ); 909 } 910 } 911 auto gV = gradientVector( triangle[0..3] ); 912 913 auto gradient = new cairo.LinearGradient( gV[0].x, gV[0].y, 914 gV[1].x, gV[1].y ); 915 916 context.lineWidth = 0.0; 917 918 /* 919 We add a number of stops to the gradient. Optimally we should only add the top 920 and bottom, but this is not possible for two reasons. First of all we support 921 other colour spaces than rgba, while cairo only support rgba. We _simulate_ 922 the other colourspace in RGBA by taking small steps in the rgba colourspace. 923 Secondly to support multiple colour stops in our own colourgradient we need to 924 add all those. 925 926 The ideal way to solve the second problem would be by using the colourGradient 927 stops here, but that wouldn't solve the first issue, so we go for the stupider 928 solution here. 929 930 Ideally we would see how cairo does their colourgradient and implement the same 931 for other colourspaces. 932 i */ 933 auto no_stops = 10.0; import std.range : iota; 934 import std.array : array; 935 auto stepsize = (gV[1].z - gV[0].z)/no_stops; 936 auto steps = [gV[0].z, gV[1].z]; 937 if (stepsize > 0) 938 steps = iota(gV[0].z, gV[1].z, stepsize).array ~ gV[1].z; 939 940 foreach(i, z; steps) { 941 auto col = cFunc(z); 942 import ggplotd.colourspace : RGBA, toCairoRGBA; 943 gradient.addColorStopRGBA(i/(steps.length-1.0), 944 RGBA(col.r, col.g, col.b, flags.alpha).toCairoRGBA 945 ); 946 } 947 948 context.moveTo( vertices.front.x, vertices.front.y ); 949 vertices.popFront; 950 foreach( v; vertices ) 951 context.lineTo( v.x, v.y ); 952 context.closePath; 953 context.setSource( gradient ); 954 context.fillPreserve; 955 context.identityMatrix(); 956 context.stroke; 957 return context; 958 }; 959 960 geom.draw = f; 961 return [geom]; 962 } 963 964 965 /** 966 Draw kernel density based on the x coordinates of the data 967 968 Examples: 969 -------------- 970 /// http://blackedder.github.io/ggplotd/images/filled_density.svg 971 import std.array : array; 972 import std.algorithm : map; 973 import std.range : repeat, iota, chain; 974 import std.random : uniform; 975 976 import ggplotd.aes : Aes; 977 import ggplotd.geom : geomDensity; 978 import ggplotd.ggplotd : GGPlotD; 979 import ggplotd.legend : discreteLegend; 980 auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array; 981 auto cols = "a".repeat(25).chain("b".repeat(25)); 982 auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 983 double[], "fill" )( 984 xs, cols, 0.45.repeat(xs.length).array); 985 auto gg = GGPlotD().put( geomDensity( aes ) ); 986 gg.put(discreteLegend); 987 gg.save( "filled_density.svg" ); 988 -------------- 989 */ 990 auto geomDensity(AES)(AES aes) 991 { 992 import ggplotd.stat : statDensity; 993 return geomLine( statDensity( aes ) ); 994 } 995 996 /** 997 Draw kernel density based on the x and y coordinates of the data 998 999 Examples: 1000 -------------- 1001 /// http://blackedder.github.io/ggplotd/images/density2D.png 1002 import std.array : array; 1003 import std.algorithm : map; 1004 import std.conv : to; 1005 import std.range : repeat, iota; 1006 import std.random : uniform; 1007 1008 import ggplotd.aes : Aes; 1009 import ggplotd.colour : colourGradient; 1010 import ggplotd.colourspace : XYZ; 1011 import ggplotd.geom : geomDensity2D; 1012 import ggplotd.ggplotd : GGPlotD; 1013 import ggplotd.legend : continuousLegend; 1014 1015 auto xs = iota(0,500,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)) 1016 .array; 1017 auto ys = iota(0,500,1).map!((y) => uniform(0.5,1.5)+uniform(0.5,1.5)) 1018 .array; 1019 auto aes = Aes!(typeof(xs), "x", typeof(ys), "y")( xs, ys); 1020 auto gg = GGPlotD().put( geomDensity2D( aes ) ); 1021 // Use a different colour scheme 1022 gg.put( colourGradient!XYZ( "white-cornflowerBlue-crimson" ) ); 1023 gg.put(continuousLegend); 1024 1025 gg.save( "density2D.png" ); 1026 -------------- 1027 */ 1028 auto geomDensity2D(AES)(AES aes) 1029 { 1030 import std.algorithm : map, joiner; 1031 import ggplotd.stat : statDensity2D; 1032 1033 return statDensity2D( aes ) 1034 .map!( (poly) => geomPolygon( poly ) ).joiner; 1035 }