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