1 module ggplotd.ggplotd; 2 3 import cconfig = cairo.c.config; 4 import cpdf = cairo.pdf; 5 import csvg = cairo.svg; 6 import cairo = cairo; 7 8 import ggplotd.colour; 9 import ggplotd.geom : Geom; 10 import ggplotd.bounds : Bounds; 11 import ggplotd.colourspace : RGBA; 12 13 version (unittest) 14 { 15 import dunit.toolkit; 16 } 17 18 /// delegate that takes a Title struct and returns a changed Title struct 19 alias TitleFunction = Title delegate(Title); 20 21 /// Currently only holds the title. In the future could also be used to store details on location etc. 22 struct Title 23 { 24 /// The actual title 25 string[] title; 26 } 27 28 /** 29 Draw the title 30 31 Examples: 32 -------------------- 33 GGPlotD().put( title( "My title" ) ); 34 -------------------- 35 */ 36 TitleFunction title( string title ) 37 { 38 return delegate(Title t) { t.title = [title]; return t; }; 39 } 40 41 /** 42 Draw the multiline title 43 44 Examples: 45 -------------------- 46 GGPlotD().put( title( ["My title line1", "line2", "line3"] ) ); 47 -------------------- 48 */ 49 TitleFunction title( string[] title ) 50 { 51 return delegate(Title t) { t.title = title; return t; }; 52 } 53 54 private auto createEmptySurface( string fname, int width, int height, 55 RGBA colour ) 56 { 57 cairo.Surface surface; 58 59 static if (cconfig.CAIRO_HAS_PDF_SURFACE) 60 { 61 if (fname[$ - 3 .. $] == "pdf") 62 { 63 surface = new cpdf.PDFSurface(fname, width, height); 64 } 65 } 66 else 67 { 68 if (fname[$ - 3 .. $] == "pdf") 69 assert(0, "PDF support not enabled by cairoD"); 70 } 71 static if (cconfig.CAIRO_HAS_SVG_SURFACE) 72 { 73 if (fname[$ - 3 .. $] == "svg") 74 { 75 surface = new csvg.SVGSurface(fname, width, height); 76 } 77 } 78 else 79 { 80 if (fname[$ - 3 .. $] == "svg") 81 assert(0, "SVG support not enabled by cairoD"); 82 } 83 if (fname[$ - 3 .. $] == "png") 84 { 85 surface = new cairo.ImageSurface(cairo.Format.CAIRO_FORMAT_ARGB32, width, height); 86 } 87 88 import ggplotd.colourspace : toCairoRGBA; 89 auto backcontext = cairo.Context(surface); 90 backcontext.setSourceRGBA(colour.toCairoRGBA); 91 backcontext.paint; 92 93 return surface; 94 } 95 96 /// 97 private auto drawTitle( in Title title, ref cairo.Surface surface, 98 in Margins margins, int width ) 99 { 100 auto context = cairo.Context(surface); 101 context.setFontSize(16.0); 102 context.moveTo( width/2, margins.top/2 ); 103 104 auto f = context.fontExtents(); 105 foreach(t; title.title) 106 { 107 auto e = context.textExtents(t); 108 context.relMoveTo( -e.width/2, 0 ); 109 context.showText(t); 110 context.relMoveTo( -e.width/2, f.height ); 111 } 112 113 return surface; 114 } 115 116 import ggplotd.scale : ScaleType; 117 import ggplotd.guide : GuideToDoubleFunction, GuideToColourFunction; 118 private auto drawGeom( in Geom geom, ref cairo.Surface surface, 119 in GuideToDoubleFunction xFunc, in GuideToDoubleFunction yFunc, 120 in GuideToColourFunction cFunc, in GuideToDoubleFunction sFunc, 121 in ScaleType scaleFunction, 122 in Bounds bounds, 123 in Margins margins, int width, int height ) 124 { 125 if (geom.draw.isNull) 126 return surface; 127 cairo.Context context; 128 if (geom.mask) { 129 auto plotSurface = cairo.Surface.createForRectangle(surface, 130 cairo.Rectangle!double(margins.left, margins.top, 131 width - (margins.left+margins.right), 132 height - (margins.top+margins.bottom))); 133 context = cairo.Context(plotSurface); 134 } else { 135 context = cairo.Context(surface); 136 context.translate(margins.left, margins.top); 137 } 138 import std.conv : to; 139 context = scaleFunction(context, bounds, 140 width.to!double - (margins.left+margins.right), 141 height.to!double - (margins.top+margins.bottom)); 142 context = geom.draw(context, xFunc, yFunc, cFunc, sFunc); 143 return surface; 144 } 145 146 /// Specify margins in number of pixels 147 struct Margins 148 { 149 /// Create new Margins object based on old one 150 this(in Margins copy) { 151 this(copy.left, copy.right, copy.bottom, copy.top); 152 } 153 154 /// Create new Margins object based on specified sizes 155 this(in size_t l, in size_t r, in size_t b, in size_t t) { 156 left = l; 157 right = r; 158 bottom = b; 159 top = t; 160 } 161 162 /// left margin 163 size_t left = 50; 164 /// right margin 165 size_t right = 20; 166 /// bottom margin 167 size_t bottom = 50; 168 /// top margin 169 size_t top = 40; 170 } 171 172 Margins defaultMargins(int size1, int size2) 173 { 174 import std.conv : to; 175 Margins margins; 176 auto scale = defaultScaling(size1, size2); 177 margins.left = (margins.left*scale).to!size_t; 178 margins.right = (margins.right*scale).to!size_t; 179 margins.top = (margins.top*scale).to!size_t; 180 margins.bottom = (margins.bottom*scale).to!size_t; 181 return margins; 182 } 183 184 private auto defaultScaling( int size ) 185 { 186 if (size > 500) 187 return 1; 188 if (size < 100) 189 return 0.6; 190 return 0.6+(1.0-0.6)*(size-100)/(500-100); 191 } 192 193 private auto defaultScaling( int size1, int size2 ) 194 { 195 return (defaultScaling(size1) + defaultScaling(size2))/2.0; 196 } 197 198 unittest 199 { 200 assertEqual(defaultScaling(50), 0.6); 201 assertEqual(defaultScaling(600), 1.0); 202 assertEqual(defaultScaling(100), 0.6); 203 assertEqual(defaultScaling(500), 1.0); 204 assertEqual(defaultScaling(300), 0.8); 205 } 206 207 /// GGPlotD contains the needed information to create a plot 208 struct GGPlotD 209 { 210 import ggplotd.bounds : height, width; 211 import ggplotd.colour : ColourGradientFunction; 212 import ggplotd.scale : ScaleType; 213 214 /** 215 Draw the plot to a cairoD cairo surface. 216 217 Params: 218 surface = Surface object of type cairo.Surface from cairoD library, on top of which this plot is drawn. 219 width = Width of the given surface. 220 height = Height of the given surface. 221 222 Returns: 223 Resulting surface of the same type as input surface, with this plot drawn on top of it. 224 */ 225 ref cairo.Surface drawToSurface( ref cairo.Surface surface, int width, int height ) const 226 { 227 import std.range : empty, front; 228 import std.typecons : Tuple; 229 230 import ggplotd.bounds : AdaptiveBounds; 231 import ggplotd.guide : GuideStore; 232 233 Tuple!(double, string)[] xAxisTicks; 234 Tuple!(double, string)[] yAxisTicks; 235 236 GuideStore!"x" xStore; 237 GuideStore!"y" yStore; 238 GuideStore!"colour" colourStore; 239 GuideStore!"size" sizeStore; 240 241 foreach (geom; geomRange.data) 242 { 243 xStore.put(geom.xStore); 244 yStore.put(geom.yStore); 245 colourStore.put(geom.colourStore); 246 sizeStore.put(geom.sizeStore); 247 } 248 249 AdaptiveBounds bounds; 250 bounds.adapt(xStore.min(), yStore.min()); 251 bounds.adapt(xStore.max(), yStore.max()); 252 253 import std.algorithm : map; 254 import std.array : array; 255 import std.typecons : tuple; 256 if (xStore.hasDiscrete) 257 xAxisTicks = xStore 258 .storeHash 259 .byKeyValue() 260 .map!((kv) => tuple(kv.value, kv.key)) 261 .array; 262 if (yStore.hasDiscrete) 263 yAxisTicks = yStore 264 .storeHash 265 .byKeyValue() 266 .map!((kv) => tuple(kv.value, kv.key)) 267 .array; 268 269 // Axis 270 import std.algorithm : sort, uniq, min, max; 271 import std.range : chain; 272 import std.array : array; 273 274 import ggplotd.axes : initialized, axisAes; 275 276 // TODO move this out of here and add some tests 277 // If ticks are provided then we make sure the bounds include them 278 auto xSortedTicks = xAxisTicks.sort().uniq.array; 279 if (!xSortedTicks.empty) 280 { 281 bounds.min_x = min( bounds.min_x, xSortedTicks[0][0] ); 282 bounds.max_x = max( bounds.max_x, xSortedTicks[$-1][0] ); 283 } 284 if (initialized(xaxis)) 285 { 286 bounds.min_x = xaxis.min; 287 bounds.max_x = xaxis.max; 288 } 289 290 // This needs to happen before the offset of x axis is set 291 auto ySortedTicks = yAxisTicks.sort().uniq.array; 292 if (!ySortedTicks.empty) 293 { 294 bounds.min_y = min( bounds.min_y, ySortedTicks[0][0] ); 295 bounds.max_y = max( bounds.max_y, ySortedTicks[$-1][0] ); 296 } 297 if (initialized(yaxis)) 298 { 299 bounds.min_y = yaxis.min; 300 bounds.max_y = yaxis.max; 301 } 302 303 import std.math : isNaN; 304 auto offset = bounds.min_y; 305 if (!isNaN(xaxis.offset)) 306 offset = xaxis.offset; 307 if (!xaxis.show) // Trixk to draw the axis off screen if it is hidden 308 offset = yaxis.min - bounds.height; 309 310 // TODO: Should really take separate scaling for number of ticks (defaultScaling(width)) 311 // and for font: defaultScaling(widht, height) 312 auto aesX = axisAes("x", bounds.min_x, bounds.max_x, offset, defaultScaling(width, height), 313 xSortedTicks ); 314 315 offset = bounds.min_x; 316 if (!isNaN(yaxis.offset)) 317 offset = yaxis.offset; 318 if (!yaxis.show) // Trixk to draw the axis off screen if it is hidden 319 offset = xaxis.min - bounds.width; 320 auto aesY = axisAes("y", bounds.min_y, bounds.max_y, offset, defaultScaling(height, width), 321 ySortedTicks ); 322 323 import ggplotd.geom : geomAxis; 324 import ggplotd.axes : tickLength; 325 326 auto currentMargins = margins(width, height); 327 328 auto gR = chain( 329 geomAxis(aesX, 330 bounds.height.tickLength(height - currentMargins.bottom - currentMargins.top, 331 defaultScaling(width), defaultScaling(height)), xaxis.label), 332 geomAxis(aesY, 333 bounds.width.tickLength(width - currentMargins.left - currentMargins.right, 334 defaultScaling(width), defaultScaling(height)), yaxis.label), 335 ); 336 auto plotMargins = Margins(currentMargins); 337 if (!legends.empty) 338 plotMargins.right += legends[0].width; 339 340 // Plot axis and geomRange 341 import ggplotd.guide : guideFunction; 342 auto xFunc = guideFunction(xStore); 343 auto yFunc = guideFunction(yStore); 344 auto cFunc = guideFunction(colourStore, this.colourGradient()); 345 auto sFunc = guideFunction(sizeStore); 346 347 foreach (geom; chain(geomRange.data, gR) ) 348 { 349 surface = geom.drawGeom( surface, 350 xFunc, yFunc, cFunc, sFunc, 351 scale(), bounds, 352 plotMargins, width, height ); 353 } 354 355 // Plot title 356 surface = title.drawTitle( surface, currentMargins, width ); 357 358 import std.range : iota, zip, dropOne; 359 foreach(ly; zip(legends, iota(0.0, height, height/(legends.length+1.0)).dropOne)) 360 { 361 auto legend = ly[0]; 362 auto y = ly[1] - legend.height*.5; 363 if (legend.type == "continuous") { 364 import ggplotd.legend : drawContinuousLegend; 365 auto legendSurface = cairo.Surface.createForRectangle(surface, 366 cairo.Rectangle!double(width - currentMargins.right - legend.width, 367 y, legend.width, legend.height ));//margins.right, margins.right)); 368 legendSurface = drawContinuousLegend( legendSurface, 369 legend.width, legend.height, 370 colourStore, this.colourGradient ); 371 } else if (legend.type == "discrete") { 372 import ggplotd.legend : drawDiscreteLegend; 373 auto legendSurface = cairo.Surface.createForRectangle(surface, 374 cairo.Rectangle!double(width - currentMargins.right - legend.width, 375 y, legend.width, legend.height ));//margins.right, margins.right)); 376 legendSurface = drawDiscreteLegend( legendSurface, 377 legend.width, legend.height, 378 colourStore, this.colourGradient ); 379 } 380 } 381 382 return surface; 383 } 384 385 version(ggplotdGTK) 386 { 387 import gtkdSurface = cairo.Surface; // cairo surface module in GtkD package. 388 389 /** 390 Draw the plot to a GtkD cairo surface. 391 392 Params: 393 surface = Surface object of type cairo.Surface from GtkD library, on top of which this plot is drawn. 394 width = Width of the given surface. 395 height = Height of the given surface. 396 397 Returns: 398 Resulting surface of the same type as input surface, with this plot drawn on top of it. 399 */ 400 auto drawToSurface( ref gtkdSurface.Surface surface, int width, int height ) const 401 { 402 import gtkc = gtkc.cairotypes; 403 import cairod = cairo.c.cairo; 404 405 alias gtkd_surface_t = gtkc.cairo_surface_t; 406 alias cairod_surface_t = cairod.cairo_surface_t; 407 408 cairo.Surface cairodSurface = new cairo.Surface(cast(cairod_surface_t*)surface.getSurfaceStruct()); 409 drawToSurface(cairodSurface, width, height); 410 411 return surface; 412 } 413 } 414 415 /// save the plot to a file 416 void save( string fname, int width = 470, int height = 470 ) const 417 { 418 bool pngWrite = false; 419 auto surface = createEmptySurface( fname, width, height, 420 theme.backgroundColour ); 421 422 surface = drawToSurface( surface, width, height ); 423 424 if (fname[$ - 3 .. $] == "png") 425 { 426 pngWrite = true; 427 } 428 429 if (pngWrite) 430 (cast(cairo.ImageSurface)(surface)).writeToPNG(fname); 431 } 432 433 /// Using + to extend the plot for compatibility to ggplot2 in R 434 ref GGPlotD opBinary(string op, T)(T rhs) if (op == "+") 435 { 436 import ggplotd.axes : XAxisFunction, YAxisFunction; 437 import ggplotd.colour : ColourGradientFunction; 438 static if (is(ElementType!T==Geom)) 439 { 440 geomRange.put( rhs ); 441 } 442 static if (is(T==ScaleType)) 443 { 444 scaleFunction = rhs; 445 } 446 static if (is(T==XAxisFunction)) 447 { 448 xaxis = rhs( xaxis ); 449 } 450 static if (is(T==YAxisFunction)) 451 { 452 yaxis = rhs( yaxis ); 453 } 454 static if (is(T==TitleFunction)) 455 { 456 title = rhs( title ); 457 } 458 static if (is(T==ThemeFunction)) 459 { 460 theme = rhs( theme ); 461 } 462 static if (is(T==Margins)) 463 { 464 _margins = rhs; 465 } 466 static if (is(T==Legend)) 467 { 468 legends ~= rhs; 469 } 470 static if (is(T==ColourGradientFunction)) { 471 colourGradientFunction = rhs; 472 } 473 return this; 474 } 475 /// put/add to the plot 476 ref GGPlotD put(T)(T rhs) 477 { 478 return this.opBinary!("+", T)(rhs); 479 } 480 481 /// Active scale 482 ScaleType scale() const 483 { 484 import ggplotd.scale : defaultScale = scale; 485 // Return active function or the default 486 if (!scaleFunction.isNull) 487 return scaleFunction; 488 else 489 return defaultScale(); 490 } 491 492 /// Active colourGradient 493 ColourGradientFunction colourGradient() const 494 { 495 import ggplotd.colour : defaultColourGradient = colourGradient; 496 import ggplotd.colourspace : HCY; 497 if (!colourGradientFunction.isNull) 498 return colourGradientFunction; 499 else 500 return defaultColourGradient!HCY(""); 501 } 502 503 /// Active margins 504 Margins margins(int width, int height) const 505 { 506 if (!_margins.isNull) 507 return _margins; 508 else 509 return defaultMargins(width, height); 510 } 511 512 private: 513 import std.range : Appender; 514 import ggplotd.theme : Theme, ThemeFunction; 515 import ggplotd.legend : Legend; 516 Appender!(Geom[]) geomRange; 517 518 import ggplotd.axes : XAxis, YAxis; 519 XAxis xaxis; 520 YAxis yaxis; 521 522 523 Title title; 524 Theme theme; 525 526 import std.typecons : Nullable; 527 Nullable!(Margins) _margins; 528 Nullable!(ScaleType) scaleFunction; 529 Nullable!(ColourGradientFunction) colourGradientFunction; 530 531 Legend[] legends; 532 } 533 534 unittest 535 { 536 import std.range : zip; 537 import std.algorithm : map; 538 import ggplotd.geom; 539 import ggplotd.aes; 540 541 const win_width = 1024; 542 const win_height = 1024; 543 544 const radius = 400.; 545 546 547 auto gg = GGPlotD(); 548 gg = zip([ 0, radius*0.45 ], [ 0, radius*0.45]) 549 .map!((a) => aes!("x","y")(a[0], a[1])) 550 .geomLine.putIn(gg); 551 gg = zip([ 300, radius*0.45 ], [ 210, radius*0.45]) 552 .map!((a) => aes!("x","y")(a[0], a[1])) 553 .geomLine.putIn(gg); 554 555 import ggplotd.theme : Theme, ThemeFunction; 556 Theme theme; 557 558 auto surface = createEmptySurface( "test.png", win_width, win_height, 559 theme.backgroundColour ); 560 561 auto dim = gg.geomRange.data.length; 562 surface = gg.drawToSurface( surface, win_width, win_height ); 563 assertEqual( dim, gg.geomRange.data.length ); 564 surface = gg.drawToSurface( surface, win_width, win_height ); 565 assertEqual( dim, gg.geomRange.data.length ); 566 surface = gg.drawToSurface( surface, win_width, win_height ); 567 assertEqual( dim, gg.geomRange.data.length ); 568 } 569 570 version(ggplotdGTK) 571 { 572 unittest 573 { 574 import std.range : zip; 575 import std.algorithm : map; 576 // Draw same plot on cairod.ImageSurface, and on gtkd.cairo.ImageSurface, 577 // and prove resulting images are the same. 578 579 import ggplotd.geom; 580 import ggplotd.aes; 581 582 import gtkSurface = cairo.Surface; 583 import gtkImageSurface = cairo.ImageSurface; 584 import gtkCairoTypes = gtkc.cairotypes; 585 586 const win_width = 1024; 587 const win_height = 1024; 588 589 const radius = 400.; 590 591 auto gg = GGPlotD(); 592 gg = zip([ 0, radius*0.45 ], [ 0, radius*0.45]) 593 .map!((a) => aes!("x","y")(a[0], a[1])) 594 .geomLine.putIn(gg); 595 gg = zip([ 300, radius*0.45 ], [ 210, radius*0.45]) 596 .map!((a) => aes!("x","y")(a[0], a[1])) 597 .geomLine.putIn(gg); 598 599 cairo.Surface cairodSurface = 600 new cairo.ImageSurface(cairo.Format.CAIRO_FORMAT_RGB24, win_width, win_height); 601 gtkSurface.Surface gtkdSurface = 602 gtkImageSurface.ImageSurface.create(gtkCairoTypes.cairo_format_t.RGB24, 603 win_width, win_height); 604 605 auto cairodImageSurface = cast(cairo.ImageSurface)cairodSurface; 606 auto gtkdImageSurface = cast(gtkImageSurface.ImageSurface)gtkdSurface; 607 608 gg.drawToSurface(cairodSurface, win_width, win_height); 609 gg.drawToSurface(gtkdSurface, win_width, win_height); 610 611 auto byteSize = win_width*win_height*4; 612 613 assertEqual(cairodImageSurface.getData()[0..byteSize], 614 gtkdImageSurface.getData()[0..byteSize]); 615 } 616 } 617 618 unittest 619 { 620 import ggplotd.axes : yaxisLabel, yaxisRange; 621 auto gg = GGPlotD() 622 .put( yaxisLabel( "My ylabel" ) ) 623 .put( yaxisRange( 0, 2.0 ) ); 624 assertEqual( gg.yaxis.max, 2.0 ); 625 assertEqual( gg.yaxis.label, "My ylabel" ); 626 627 gg = GGPlotD(); 628 gg.put( yaxisLabel( "My ylabel" ) ) 629 .put( yaxisRange( 0, 2.0 ) ); 630 assertEqual( gg.yaxis.max, 2.0 ); 631 assertEqual( gg.yaxis.label, "My ylabel" ); 632 } 633 634 635 /// 636 unittest 637 { 638 import std.range : zip; 639 import std.algorithm : map; 640 641 import ggplotd.aes : aes; 642 import ggplotd.geom : geomLine; 643 import ggplotd.scale : scale; 644 auto gg = zip(["a", "b", "c", "b"], ["x", "y", "y", "x"], ["b", "b", "b", "b"]) 645 .map!((a) => aes!("x", "y", "colour")(a[0], a[1], a[2])) 646 .geomLine 647 .putIn(GGPlotD()); 648 gg + scale(); 649 gg.save( "test6.png"); 650 } 651 652 /// 653 unittest 654 { 655 // http://blackedder.github.io/ggplotd/images/noise.png 656 import std.array : array; 657 import std.math : sqrt; 658 import std.algorithm : map; 659 import std.range : zip, iota; 660 import std.random : uniform; 661 662 import ggplotd.aes : aes; 663 import ggplotd.geom : geomLine, geomPoint; 664 // Generate some noisy data with reducing width 665 auto f = (double x) { return x/(1+x); }; 666 auto width = (double x) { return sqrt(0.1/(1+x)); }; 667 auto xs = iota( 0, 10, 0.1 ).array; 668 669 auto ysfit = xs.map!((x) => f(x)); 670 auto ysnoise = xs.map!((x) => f(x) + uniform(-width(x),width(x))).array; 671 672 auto gg = xs.zip(ysnoise) 673 .map!((a) => aes!("x", "y", "colour")(a[0], a[1], "a")) 674 .geomPoint 675 .putIn(GGPlotD()); 676 677 gg = xs.zip(ysfit).map!((a) => aes!("x", "y")(a[0], a[1])).geomLine.putIn(gg); 678 679 // 680 auto ys2fit = xs.map!((x) => 1-f(x)); 681 auto ys2noise = xs.map!((x) => 1-f(x) + uniform(-width(x),width(x))).array; 682 683 gg = xs.zip(ys2fit).map!((a) => aes!("x", "y")(a[0], a[1])) 684 .geomLine 685 .putIn(gg); 686 gg = xs.zip(ys2noise) 687 .map!((a) => aes!("x", "y", "colour")(a[0], a[1], "b")) 688 .geomPoint 689 .putIn(gg); 690 691 gg.save( "noise.png" ); 692 } 693 694 /// 695 unittest 696 { 697 // http://blackedder.github.io/ggplotd/images/hist.png 698 import std.array : array; 699 import std.algorithm : map; 700 import std.range : iota, zip; 701 import std.random : uniform; 702 703 import ggplotd.aes : aes; 704 import ggplotd.geom : geomHist, geomPoint; 705 import ggplotd.range : mergeRange; 706 707 auto xs = iota(0,25,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array; 708 auto gg = xs 709 .map!((a) => aes!("x")(a)) 710 .geomHist 711 .putIn(GGPlotD()); 712 713 gg = xs.map!((a) => aes!("x", "y")(a, 0.0)) 714 .geomPoint 715 .putIn(gg); 716 717 gg.save( "hist.png" ); 718 } 719 720 /// Setting background colour 721 unittest 722 { 723 /// http://blackedder.github.io/ggplotd/images/background.svg 724 import std.range : zip; 725 import std.algorithm : map; 726 import ggplotd.aes : aes; 727 import ggplotd.theme : background; 728 import ggplotd.geom : geomPoint; 729 730 // http://blackedder.github.io/ggplotd/images/polygon.png 731 auto gg = zip([1, 0, 0.0], [1, 1, 0.0], [1, 0.1, 0]) 732 .map!((a) => aes!("x", "y", "colour")(a[0], a[1], a[2])) 733 .geomPoint 734 .putIn(GGPlotD()); 735 gg.put(background(RGBA(0.7, 0.7, 0.7, 1))); 736 gg.save( "background.svg" ); 737 } 738 739 /// Other data type 740 unittest 741 { 742 /// http://blackedder.github.io/ggplotd/images/data.png 743 import std.array : array; 744 import std.math : sqrt; 745 import std.algorithm : map; 746 import std.range : iota; 747 import std.random : uniform; 748 749 import ggplotd.geom : geomPoint; 750 751 struct Point { double x; double y; } 752 // Generate some noisy data with reducing width 753 auto f = (double x) { return x/(1+x); }; 754 auto width = (double x) { return sqrt(0.1/(1+x)); }; 755 immutable xs = iota( 0, 10, 0.1 ).array; 756 757 auto points = xs.map!((x) => Point(x, 758 f(x) + uniform(-width(x),width(x)))); 759 760 auto gg = GGPlotD().put( geomPoint( points ) ); 761 762 gg.save( "data.png" ); 763 } 764 765 import std.range : ElementType; 766 767 /** 768 Put an element into a plot/facets struct 769 770 This basically reverses a call to put and allows one to write more idiomatic D code where code flows from left to right instead of right to left. 771 772 Examples: 773 -------------------- 774 auto gg = data.aes.geomPoint.putIn(GGPlotD()); 775 // instead of 776 auto gg = GGPlotD().put(geomPoint(aes(data))); 777 -------------------- 778 */ 779 ref auto putIn(T, U)(T t, U u) 780 { 781 return u.put(t); 782 } 783 784 /** 785 Plot multiple (sub) plots 786 */ 787 struct Facets 788 { 789 /// 790 ref Facets put(GGPlotD facet) 791 { 792 ggs.put( facet ); 793 return this; 794 } 795 796 /// 797 auto drawToSurface( ref cairo.Surface surface, int dimX, int dimY, 798 int width, int height ) const 799 { 800 import std.conv : to; 801 import std.math : floor; 802 import std.range : save, empty, front, popFront; 803 import cairo.cairo : Rectangle; 804 int w = floor( width.to!double/dimX ).to!int; 805 int h = floor( height.to!double/dimY ).to!int; 806 807 auto gs = ggs.data.save; 808 foreach( i; 0..dimX ) 809 { 810 foreach( j; 0..dimY ) 811 { 812 if (!gs.empty) 813 { 814 auto rect = Rectangle!double( w*i, h*j, w, h ); 815 auto subS = cairo.Surface.createForRectangle( surface, rect ); 816 gs.front.drawToSurface( subS, w, h ), 817 gs.popFront; 818 } 819 } 820 } 821 822 return surface; 823 } 824 825 /// 826 auto drawToSurface( ref cairo.Surface surface, 827 int width, int height ) const 828 { 829 import std.conv : to; 830 // Calculate dimX/dimY from width/height 831 auto grid = gridLayout( ggs.data.length, width.to!double/height ); 832 return drawToSurface( surface, grid[0], grid[1], width, height ); 833 } 834 835 836 /// 837 void save( string fname, int dimX, int dimY, int width = 470, int height = 470 ) const 838 { 839 bool pngWrite = false; 840 auto surface = createEmptySurface( fname, width, height, 841 RGBA(1,1,1,1) ); 842 843 surface = drawToSurface( surface, dimX, dimY, width, height ); 844 845 if (fname[$ - 3 .. $] == "png") 846 { 847 pngWrite = true; 848 } 849 850 if (pngWrite) 851 (cast(cairo.ImageSurface)(surface)).writeToPNG(fname); 852 } 853 854 /// 855 void save( string fname, int width = 470, int height = 470 ) const 856 { 857 import std.conv : to; 858 // Calculate dimX/dimY from width/height 859 auto grid = gridLayout( ggs.data.length, width.to!double/height ); 860 save( fname, grid[0], grid[1], width, height ); 861 } 862 863 import std.range : Appender; 864 865 Appender!(GGPlotD[]) ggs; 866 } 867 868 auto gridLayout( size_t length, double ratio ) 869 { 870 import std.conv : to; 871 import std.math : ceil, sqrt; 872 import std.typecons : Tuple; 873 auto h = ceil( sqrt(length/ratio) ); 874 auto w = ceil(length/h); 875 return Tuple!(int, int)( w.to!int, h.to!int ); 876 } 877 878 unittest 879 { 880 import std.typecons : Tuple; 881 assertEqual(gridLayout(4, 1), Tuple!(int, int)(2, 2)); 882 assertEqual(gridLayout(2, 1), Tuple!(int, int)(1, 2)); 883 assertEqual(gridLayout(3, 1), Tuple!(int, int)(2, 2)); 884 assertEqual(gridLayout(2, 2), Tuple!(int, int)(2, 1)); 885 } 886 887 /// 888 unittest 889 { 890 // Drawing different shapes 891 import ggplotd.aes : aes, Pixel; 892 import ggplotd.axes : xaxisRange, yaxisRange; 893 import ggplotd.geom : geomDiamond, geomRectangle; 894 895 auto gg = GGPlotD(); 896 897 auto aes1 = [aes!("x", "y", "width", "height")(1.0, -1.0, 3.0, 5.0)]; 898 gg.put( geomDiamond( aes1 ) ); 899 gg.put( geomRectangle( aes1 ) ); 900 gg.put( xaxisRange( -5, 11.0 ) ); 901 gg.put( yaxisRange( -9, 9.0 ) ); 902 903 904 auto aes2 = [aes!("x", "y", "width", "height")(8.0, 5.0, Pixel(10), Pixel(20))]; 905 gg.put( geomDiamond( aes2 ) ); 906 gg.put( geomRectangle( aes2 ) ); 907 908 auto aes3 = [aes!("x", "y", "width", "height")(6.0, -5.0, Pixel(25), Pixel(25))]; 909 gg.put( geomDiamond( aes3 ) ); 910 gg.put( geomRectangle( aes3 ) ); 911 912 gg.save( "shapes1.png", 300, 300 ); 913 } 914 915 /// 916 unittest 917 { 918 // Drawing different shapes 919 import ggplotd.aes : aes, Pixel; 920 import ggplotd.axes : xaxisRange, yaxisRange; 921 922 import ggplotd.geom : geomEllipse, geomTriangle; 923 924 auto gg = GGPlotD(); 925 926 auto aes1 = [aes!("x", "y", "width", "height")( 1.0, -1.0, 3.0, 5.0 )]; 927 gg.put( geomEllipse( aes1 ) ); 928 gg.put( geomTriangle( aes1 ) ); 929 gg.put( xaxisRange( -5, 11.0 ) ); 930 gg.put( yaxisRange( -9, 9.0 ) ); 931 932 933 auto aes2 = [aes!("x", "y", "width", "height")(8.0, 5.0, Pixel(10), Pixel(20))]; 934 gg.put( geomEllipse( aes2 ) ); 935 gg.put( geomTriangle( aes2 ) ); 936 937 auto aes3 = [aes!("x", "y", "width", "height")( 6.0, -5.0, Pixel(25), Pixel(25))]; 938 gg.put( geomEllipse( aes3 ) ); 939 gg.put( geomTriangle( aes3 ) ); 940 941 gg.save( "shapes2.png", 300, 300 ); 942 }