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.get()(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 return 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 // Set scaling 250 import ggplotd.guide : guideFunction; 251 import ggplotd.scale : applyScaleFunction, applyScale; 252 auto xFunc = guideFunction(xStore); 253 auto yFunc = guideFunction(yStore); 254 auto cFunc = guideFunction(colourStore, this.colourGradient()); 255 auto sFunc = guideFunction(sizeStore); 256 foreach (scale; scaleFunctions) 257 scale.applyScaleFunction(xFunc, yFunc, cFunc, sFunc); 258 259 AdaptiveBounds bounds; 260 bounds = bounds.applyScale(xFunc, xStore, yFunc, yStore); 261 262 import std.algorithm : map; 263 import std.array : array; 264 import std.typecons : tuple; 265 if (xStore.hasDiscrete) 266 xAxisTicks = xStore 267 .storeHash 268 .byKeyValue() 269 .map!((kv) => tuple(xFunc(kv.value), kv.key)) 270 .array; 271 if (yStore.hasDiscrete) 272 yAxisTicks = yStore 273 .storeHash 274 .byKeyValue() 275 .map!((kv) => tuple(yFunc(kv.value), kv.key)) 276 .array; 277 278 // Axis 279 import std.algorithm : sort, uniq, min, max; 280 import std.range : chain; 281 import std.array : array; 282 283 import ggplotd.axes : initialized, axisAes; 284 285 // TODO move this out of here and add some tests 286 // If ticks are provided then we make sure the bounds include them 287 auto xSortedTicks = xAxisTicks.sort().uniq.array; 288 if (!xSortedTicks.empty) 289 { 290 bounds.min_x = min( bounds.min_x, xSortedTicks[0][0] ); 291 bounds.max_x = max( bounds.max_x, xSortedTicks[$-1][0] ); 292 } 293 if (initialized(xaxis)) 294 { 295 bounds.min_x = xaxis.min; 296 bounds.max_x = xaxis.max; 297 } 298 299 // This needs to happen before the offset of x axis is set 300 auto ySortedTicks = yAxisTicks.sort().uniq.array; 301 if (!ySortedTicks.empty) 302 { 303 bounds.min_y = min( bounds.min_y, ySortedTicks[0][0] ); 304 bounds.max_y = max( bounds.max_y, ySortedTicks[$-1][0] ); 305 } 306 if (initialized(yaxis)) 307 { 308 bounds.min_y = yaxis.min; 309 bounds.max_y = yaxis.max; 310 } 311 312 import std.math : isNaN; 313 auto offset = bounds.min_y; 314 if (!isNaN(xaxis.offset)) 315 offset = xaxis.offset; 316 if (!xaxis.show) // Trixk to draw the axis off screen if it is hidden 317 offset = yaxis.min - bounds.height; 318 319 // TODO: Should really take separate scaling for number of ticks (defaultScaling(width)) 320 // and for font: defaultScaling(widht, height) 321 auto aesX = axisAes("x", bounds.min_x, bounds.max_x, offset, defaultScaling(width, height), 322 xSortedTicks ); 323 324 offset = bounds.min_x; 325 if (!isNaN(yaxis.offset)) 326 offset = yaxis.offset; 327 if (!yaxis.show) // Trixk to draw the axis off screen if it is hidden 328 offset = xaxis.min - bounds.width; 329 auto aesY = axisAes("y", bounds.min_y, bounds.max_y, offset, defaultScaling(height, width), 330 ySortedTicks ); 331 332 import ggplotd.geom : geomAxis; 333 import ggplotd.axes : tickLength; 334 335 auto currentMargins = margins(width, height); 336 337 auto gR = chain( 338 geomAxis(aesX, 339 bounds.height.tickLength(height - currentMargins.bottom - currentMargins.top, 340 defaultScaling(width), defaultScaling(height)), 341 xaxis.label, xaxis.textAngle), 342 geomAxis(aesY, 343 bounds.width.tickLength(width - currentMargins.left - currentMargins.right, 344 defaultScaling(width), defaultScaling(height)), 345 yaxis.label, yaxis.textAngle), 346 ); 347 auto plotMargins = Margins(currentMargins); 348 if (!legends.empty) 349 plotMargins.right += legends[0].width; 350 351 foreach (geom; chain(geomRange.data, gR) ) 352 { 353 surface = geom.drawGeom( surface, 354 xFunc, yFunc, cFunc, sFunc, 355 scale(), bounds, 356 plotMargins, width, height ); 357 } 358 359 // Plot title 360 surface = title.drawTitle( surface, currentMargins, width ); 361 362 import std.range : iota, zip, dropOne; 363 foreach(ly; zip(legends, iota(0.0, height, height/(legends.length+1.0)).dropOne)) 364 { 365 auto legend = ly[0]; 366 auto y = ly[1] - legend.height*.5; 367 if (legend.type == "continuous") { 368 import ggplotd.legend : drawContinuousLegend; 369 auto legendSurface = cairo.Surface.createForRectangle(surface, 370 cairo.Rectangle!double(width - currentMargins.right - legend.width, 371 y, legend.width, legend.height ));//margins.right, margins.right)); 372 legendSurface = drawContinuousLegend( legendSurface, 373 legend.width, legend.height, 374 colourStore, this.colourGradient ); 375 } else if (legend.type == "discrete") { 376 import ggplotd.legend : drawDiscreteLegend; 377 auto legendSurface = cairo.Surface.createForRectangle(surface, 378 cairo.Rectangle!double(width - currentMargins.right - legend.width, 379 y, legend.width, legend.height ));//margins.right, margins.right)); 380 legendSurface = drawDiscreteLegend( legendSurface, 381 legend.width, legend.height, 382 colourStore, this.colourGradient ); 383 } 384 } 385 386 return surface; 387 } 388 389 version(ggplotdGTK) 390 { 391 import gtkdSurface = cairo.Surface; // cairo surface module in GtkD package. 392 393 /** 394 Draw the plot to a GtkD cairo surface. 395 396 Params: 397 surface = Surface object of type cairo.Surface from GtkD library, on top of which this plot is drawn. 398 width = Width of the given surface. 399 height = Height of the given surface. 400 401 Returns: 402 Resulting surface of the same type as input surface, with this plot drawn on top of it. 403 */ 404 auto drawToSurface( ref gtkdSurface.Surface surface, int width, int height ) const 405 { 406 import gtkc = gtkc.cairotypes; 407 import cairod = cairo.c.cairo; 408 409 alias gtkd_surface_t = gtkc.cairo_surface_t; 410 alias cairod_surface_t = cairod.cairo_surface_t; 411 412 cairo.Surface cairodSurface = new cairo.Surface(cast(cairod_surface_t*)surface.getSurfaceStruct()); 413 drawToSurface(cairodSurface, width, height); 414 415 return surface; 416 } 417 } 418 419 /// save the plot to a file 420 void save( string fname, int width = 470, int height = 470 ) const 421 { 422 bool pngWrite = false; 423 auto surface = createEmptySurface( fname, width, height, 424 theme.backgroundColour ); 425 426 surface = drawToSurface( surface, width, height ); 427 428 if (fname[$ - 3 .. $] == "png") 429 { 430 pngWrite = true; 431 } 432 433 if (pngWrite) 434 (cast(cairo.ImageSurface)(surface)).writeToPNG(fname); 435 } 436 437 /// Using + to extend the plot for compatibility to ggplot2 in R 438 ref GGPlotD opBinary(string op, T)(T rhs) if (op == "+") 439 { 440 import ggplotd.axes : XAxisFunction, YAxisFunction; 441 import ggplotd.colour : ColourGradientFunction; 442 static if (is(ElementType!T==Geom)) 443 { 444 geomRange.put( rhs ); 445 } 446 static if (is(T==ScaleType)) 447 { 448 scaleFunction = rhs; 449 } 450 static if (is(T==XAxisFunction)) 451 { 452 xaxis = rhs( xaxis ); 453 } 454 static if (is(T==YAxisFunction)) 455 { 456 yaxis = rhs( yaxis ); 457 } 458 static if (is(T==TitleFunction)) 459 { 460 title = rhs( title ); 461 } 462 static if (is(T==ThemeFunction)) 463 { 464 theme = rhs( theme ); 465 } 466 static if (is(T==Margins)) 467 { 468 _margins = rhs; 469 } 470 static if (is(T==Legend)) 471 { 472 legends ~= rhs; 473 } 474 static if (is(T==ColourGradientFunction)) { 475 colourGradientFunction = rhs; 476 } 477 static if (is(T==ScaleFunction)) { 478 scaleFunctions ~= rhs; 479 } 480 return this; 481 } 482 /// put/add to the plot 483 ref GGPlotD put(T)(T rhs) 484 { 485 return this.opBinary!("+", T)(rhs); 486 } 487 488 /// Active scale 489 ScaleType scale() const 490 { 491 import ggplotd.scale : defaultScale = scale; 492 // Return active function or the default 493 if (!scaleFunction.isNull) 494 return scaleFunction.get(); 495 else 496 return defaultScale(); 497 } 498 499 /// Active colourGradient 500 ColourGradientFunction colourGradient() const 501 { 502 import ggplotd.colour : defaultColourGradient = colourGradient; 503 import ggplotd.colourspace : HCY; 504 if (!colourGradientFunction.isNull) 505 return colourGradientFunction.get(); 506 else 507 return defaultColourGradient!HCY(""); 508 } 509 510 /// Active margins 511 Margins margins(int width, int height) const 512 { 513 if (!_margins.isNull) 514 return _margins.get(); 515 else 516 return defaultMargins(width, height); 517 } 518 519 private: 520 import std.range : Appender; 521 import ggplotd.theme : Theme, ThemeFunction; 522 import ggplotd.legend : Legend; 523 Appender!(Geom[]) geomRange; 524 525 import ggplotd.scale : ScaleFunction; 526 ScaleFunction[] scaleFunctions; 527 528 import ggplotd.axes : XAxis, YAxis; 529 XAxis xaxis; 530 YAxis yaxis; 531 532 533 Title title; 534 Theme theme; 535 536 import std.typecons : Nullable; 537 Nullable!(Margins) _margins; 538 Nullable!(ScaleType) scaleFunction; 539 Nullable!(ColourGradientFunction) colourGradientFunction; 540 541 Legend[] legends; 542 } 543 544 unittest 545 { 546 import std.range : zip; 547 import std.algorithm : map; 548 import ggplotd.geom; 549 import ggplotd.aes; 550 551 const win_width = 1024; 552 const win_height = 1024; 553 554 const radius = 400.; 555 556 557 auto gg = GGPlotD(); 558 gg = zip([ 0, radius*0.45 ], [ 0, radius*0.45]) 559 .map!((a) => aes!("x","y")(a[0], a[1])) 560 .geomLine.putIn(gg); 561 gg = zip([ 300, radius*0.45 ], [ 210, radius*0.45]) 562 .map!((a) => aes!("x","y")(a[0], a[1])) 563 .geomLine.putIn(gg); 564 565 import ggplotd.theme : Theme, ThemeFunction; 566 Theme theme; 567 568 auto surface = createEmptySurface( "test.png", win_width, win_height, 569 theme.backgroundColour ); 570 571 auto dim = gg.geomRange.data.length; 572 surface = gg.drawToSurface( surface, win_width, win_height ); 573 assertEqual( dim, gg.geomRange.data.length ); 574 surface = gg.drawToSurface( surface, win_width, win_height ); 575 assertEqual( dim, gg.geomRange.data.length ); 576 surface = gg.drawToSurface( surface, win_width, win_height ); 577 assertEqual( dim, gg.geomRange.data.length ); 578 } 579 580 version(ggplotdGTK) 581 { 582 unittest 583 { 584 import std.range : zip; 585 import std.algorithm : map; 586 // Draw same plot on cairod.ImageSurface, and on gtkd.cairo.ImageSurface, 587 // and prove resulting images are the same. 588 589 import ggplotd.geom; 590 import ggplotd.aes; 591 592 import gtkSurface = cairo.Surface; 593 import gtkImageSurface = cairo.ImageSurface; 594 import gtkCairoTypes = gtkc.cairotypes; 595 596 const win_width = 1024; 597 const win_height = 1024; 598 599 const radius = 400.; 600 601 auto gg = GGPlotD(); 602 gg = zip([ 0, radius*0.45 ], [ 0, radius*0.45]) 603 .map!((a) => aes!("x","y")(a[0], a[1])) 604 .geomLine.putIn(gg); 605 gg = zip([ 300, radius*0.45 ], [ 210, radius*0.45]) 606 .map!((a) => aes!("x","y")(a[0], a[1])) 607 .geomLine.putIn(gg); 608 609 cairo.Surface cairodSurface = 610 new cairo.ImageSurface(cairo.Format.CAIRO_FORMAT_RGB24, win_width, win_height); 611 gtkSurface.Surface gtkdSurface = 612 gtkImageSurface.ImageSurface.create(gtkCairoTypes.cairo_format_t.RGB24, 613 win_width, win_height); 614 615 auto cairodImageSurface = cast(cairo.ImageSurface)cairodSurface; 616 auto gtkdImageSurface = cast(gtkImageSurface.ImageSurface)gtkdSurface; 617 618 gg.drawToSurface(cairodSurface, win_width, win_height); 619 gg.drawToSurface(gtkdSurface, win_width, win_height); 620 621 auto byteSize = win_width*win_height*4; 622 623 assertEqual(cairodImageSurface.getData()[0..byteSize], 624 gtkdImageSurface.getData()[0..byteSize]); 625 } 626 } 627 628 unittest 629 { 630 import ggplotd.axes : yaxisLabel, yaxisRange; 631 auto gg = GGPlotD() 632 .put( yaxisLabel( "My ylabel" ) ) 633 .put( yaxisRange( 0, 2.0 ) ); 634 assertEqual( gg.yaxis.max, 2.0 ); 635 assertEqual( gg.yaxis.label, "My ylabel" ); 636 637 gg = GGPlotD(); 638 gg.put( yaxisLabel( "My ylabel" ) ) 639 .put( yaxisRange( 0, 2.0 ) ); 640 assertEqual( gg.yaxis.max, 2.0 ); 641 assertEqual( gg.yaxis.label, "My ylabel" ); 642 } 643 644 645 /// 646 unittest 647 { 648 import std.range : zip; 649 import std.algorithm : map; 650 651 import ggplotd.aes : aes; 652 import ggplotd.geom : geomLine; 653 import ggplotd.scale : scale; 654 auto gg = zip(["a", "b", "c", "b"], ["x", "y", "y", "x"], ["b", "b", "b", "b"]) 655 .map!((a) => aes!("x", "y", "colour")(a[0], a[1], a[2])) 656 .geomLine 657 .putIn(GGPlotD()); 658 gg + scale(); 659 gg.save( "test6.png"); 660 } 661 662 /// 663 unittest 664 { 665 // http://blackedder.github.io/ggplotd/images/noise.png 666 import std.array : array; 667 import std.math : sqrt; 668 import std.algorithm : map; 669 import std.range : zip, iota; 670 import std.random : uniform; 671 672 import ggplotd.aes : aes; 673 import ggplotd.geom : geomLine, geomPoint; 674 // Generate some noisy data with reducing width 675 auto f = (double x) { return x/(1+x); }; 676 auto width = (double x) { return sqrt(0.1/(1+x)); }; 677 auto xs = iota( 0, 10, 0.1 ).array; 678 679 auto ysfit = xs.map!((x) => f(x)); 680 auto ysnoise = xs.map!((x) => f(x) + uniform(-width(x),width(x))).array; 681 682 auto gg = xs.zip(ysnoise) 683 .map!((a) => aes!("x", "y", "colour")(a[0], a[1], "a")) 684 .geomPoint 685 .putIn(GGPlotD()); 686 687 gg = xs.zip(ysfit).map!((a) => aes!("x", "y")(a[0], a[1])).geomLine.putIn(gg); 688 689 // 690 auto ys2fit = xs.map!((x) => 1-f(x)); 691 auto ys2noise = xs.map!((x) => 1-f(x) + uniform(-width(x),width(x))).array; 692 693 gg = xs.zip(ys2fit).map!((a) => aes!("x", "y")(a[0], a[1])) 694 .geomLine 695 .putIn(gg); 696 gg = xs.zip(ys2noise) 697 .map!((a) => aes!("x", "y", "colour")(a[0], a[1], "b")) 698 .geomPoint 699 .putIn(gg); 700 701 gg.save( "noise.png" ); 702 } 703 704 /// 705 unittest 706 { 707 // http://blackedder.github.io/ggplotd/images/hist.png 708 import std.array : array; 709 import std.algorithm : map; 710 import std.range : iota, zip; 711 import std.random : uniform; 712 713 import ggplotd.aes : aes; 714 import ggplotd.geom : geomHist, geomPoint; 715 import ggplotd.range : mergeRange; 716 717 auto xs = iota(0,25,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array; 718 auto gg = xs 719 .map!((a) => aes!("x")(a)) 720 .geomHist 721 .putIn(GGPlotD()); 722 723 gg = xs.map!((a) => aes!("x", "y")(a, 0.0)) 724 .geomPoint 725 .putIn(gg); 726 727 gg.save( "hist.png" ); 728 } 729 730 /// Setting background colour 731 unittest 732 { 733 /// http://blackedder.github.io/ggplotd/images/background.svg 734 import std.range : zip; 735 import std.algorithm : map; 736 import ggplotd.aes : aes; 737 import ggplotd.theme : background; 738 import ggplotd.geom : geomPoint; 739 740 // http://blackedder.github.io/ggplotd/images/polygon.png 741 auto gg = zip([1, 0, 0.0], [1, 1, 0.0], [1, 0.1, 0]) 742 .map!((a) => aes!("x", "y", "colour")(a[0], a[1], a[2])) 743 .geomPoint 744 .putIn(GGPlotD()); 745 gg.put(background(RGBA(0.7, 0.7, 0.7, 1))); 746 gg.save( "background.svg" ); 747 } 748 749 /// Other data type 750 unittest 751 { 752 /// http://blackedder.github.io/ggplotd/images/data.png 753 import std.array : array; 754 import std.math : sqrt; 755 import std.algorithm : map; 756 import std.range : iota; 757 import std.random : uniform; 758 759 import ggplotd.geom : geomPoint; 760 761 struct Point { double x; double y; } 762 // Generate some noisy data with reducing width 763 auto f = (double x) { return x/(1+x); }; 764 auto width = (double x) { return sqrt(0.1/(1+x)); }; 765 immutable xs = iota( 0, 10, 0.1 ).array; 766 767 auto points = xs.map!((x) => Point(x, 768 f(x) + uniform(-width(x),width(x)))); 769 770 auto gg = GGPlotD().put( geomPoint( points ) ); 771 772 gg.save( "data.png" ); 773 } 774 775 import std.range : ElementType; 776 777 /** 778 Put an element into a plot/facets struct 779 780 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. 781 782 Examples: 783 -------------------- 784 auto gg = data.aes.geomPoint.putIn(GGPlotD()); 785 // instead of 786 auto gg = GGPlotD().put(geomPoint(aes(data))); 787 -------------------- 788 */ 789 ref auto putIn(T, U)(T t, U u) 790 { 791 return u.put(t); 792 } 793 794 /** 795 Plot multiple (sub) plots 796 */ 797 struct Facets 798 { 799 /// 800 ref Facets put(GGPlotD facet) return 801 { 802 ggs.put( facet ); 803 return this; 804 } 805 806 /// 807 auto drawToSurface( ref cairo.Surface surface, int dimX, int dimY, 808 int width, int height ) const 809 { 810 import std.conv : to; 811 import std.math : floor; 812 import std.range : save, empty, front, popFront; 813 import cairo.cairo : Rectangle; 814 int w = floor( width.to!double/dimX ).to!int; 815 int h = floor( height.to!double/dimY ).to!int; 816 817 auto gs = ggs.data.save; 818 foreach( i; 0..dimX ) 819 { 820 foreach( j; 0..dimY ) 821 { 822 if (!gs.empty) 823 { 824 auto rect = Rectangle!double( w*i, h*j, w, h ); 825 auto subS = cairo.Surface.createForRectangle( surface, rect ); 826 gs.front.drawToSurface( subS, w, h ), 827 gs.popFront; 828 } 829 } 830 } 831 832 return surface; 833 } 834 835 /// 836 auto drawToSurface( ref cairo.Surface surface, 837 int width, int height ) const 838 { 839 import std.conv : to; 840 // Calculate dimX/dimY from width/height 841 auto grid = gridLayout( ggs.data.length, width.to!double/height ); 842 return drawToSurface( surface, grid[0], grid[1], width, height ); 843 } 844 845 846 /// 847 void save( string fname, int dimX, int dimY, int width = 470, int height = 470 ) const 848 { 849 bool pngWrite = false; 850 auto surface = createEmptySurface( fname, width, height, 851 RGBA(1,1,1,1) ); 852 853 surface = drawToSurface( surface, dimX, dimY, width, height ); 854 855 if (fname[$ - 3 .. $] == "png") 856 { 857 pngWrite = true; 858 } 859 860 if (pngWrite) 861 (cast(cairo.ImageSurface)(surface)).writeToPNG(fname); 862 } 863 864 /// 865 void save( string fname, int width = 470, int height = 470 ) const 866 { 867 import std.conv : to; 868 // Calculate dimX/dimY from width/height 869 auto grid = gridLayout( ggs.data.length, width.to!double/height ); 870 save( fname, grid[0], grid[1], width, height ); 871 } 872 873 import std.range : Appender; 874 875 Appender!(GGPlotD[]) ggs; 876 } 877 878 auto gridLayout( size_t length, double ratio ) 879 { 880 import std.conv : to; 881 import std.math : ceil, sqrt; 882 import std.typecons : Tuple; 883 auto h = ceil( sqrt(length/ratio) ); 884 auto w = ceil(length/h); 885 return Tuple!(int, int)( w.to!int, h.to!int ); 886 } 887 888 unittest 889 { 890 import std.typecons : Tuple; 891 assertEqual(gridLayout(4, 1), Tuple!(int, int)(2, 2)); 892 assertEqual(gridLayout(2, 1), Tuple!(int, int)(1, 2)); 893 assertEqual(gridLayout(3, 1), Tuple!(int, int)(2, 2)); 894 assertEqual(gridLayout(2, 2), Tuple!(int, int)(2, 1)); 895 } 896 897 /// 898 unittest 899 { 900 // Drawing different shapes 901 import ggplotd.aes : aes, Pixel; 902 import ggplotd.axes : xaxisRange, yaxisRange; 903 import ggplotd.geom : geomDiamond, geomRectangle; 904 905 auto gg = GGPlotD(); 906 907 auto aes1 = [aes!("x", "y", "width", "height")(1.0, -1.0, 3.0, 5.0)]; 908 gg.put( geomDiamond( aes1 ) ); 909 gg.put( geomRectangle( aes1 ) ); 910 gg.put( xaxisRange( -5, 11.0 ) ); 911 gg.put( yaxisRange( -9, 9.0 ) ); 912 913 914 auto aes2 = [aes!("x", "y", "width", "height")(8.0, 5.0, Pixel(10), Pixel(20))]; 915 gg.put( geomDiamond( aes2 ) ); 916 gg.put( geomRectangle( aes2 ) ); 917 918 auto aes3 = [aes!("x", "y", "width", "height")(6.0, -5.0, Pixel(25), Pixel(25))]; 919 gg.put( geomDiamond( aes3 ) ); 920 gg.put( geomRectangle( aes3 ) ); 921 922 gg.save( "shapes1.png", 300, 300 ); 923 } 924 925 /// 926 unittest 927 { 928 // Drawing different shapes 929 import ggplotd.aes : aes, Pixel; 930 import ggplotd.axes : xaxisRange, yaxisRange; 931 932 import ggplotd.geom : geomEllipse, geomTriangle; 933 934 auto gg = GGPlotD(); 935 936 auto aes1 = [aes!("x", "y", "width", "height")( 1.0, -1.0, 3.0, 5.0 )]; 937 gg.put( geomEllipse( aes1 ) ); 938 gg.put( geomTriangle( aes1 ) ); 939 gg.put( xaxisRange( -5, 11.0 ) ); 940 gg.put( yaxisRange( -9, 9.0 ) ); 941 942 943 auto aes2 = [aes!("x", "y", "width", "height")(8.0, 5.0, Pixel(10), Pixel(20))]; 944 gg.put( geomEllipse( aes2 ) ); 945 gg.put( geomTriangle( aes2 ) ); 946 947 auto aes3 = [aes!("x", "y", "width", "height")( 6.0, -5.0, Pixel(25), Pixel(25))]; 948 gg.put( geomEllipse( aes3 ) ); 949 gg.put( geomTriangle( aes3 ) ); 950 951 gg.save( "shapes2.png", 300, 300 ); 952 } 953 954 unittest 955 { 956 import std.typecons; 957 958 void testTwoClassPlot(Tuple!(int, string)[] data, string fileName, string titleName) { 959 import ggplotd.aes : aes; 960 import ggplotd.geom; 961 import ggplotd.ggplotd : putIn, GGPlotD, title, Margins; 962 import ggplotd.legend: discreteLegend; 963 import std.algorithm: map; 964 auto gg = data 965 .map!(a => aes!("x", "colour", "fill")(a[0], a[1], 0.45)) 966 .geomHist 967 .putIn(GGPlotD().put(title(titleName))); 968 gg.put(discreteLegend); 969 gg.save(fileName~".svg"); 970 } 971 972 import std.array; 973 import std.range : chain, zip, repeat; 974 import std.random : uniform; 975 import std.range : generate, take; 976 auto class1 = generate!(() => uniform(0, 20)).take(1000).array; 977 auto class2 = generate!(() => uniform(0, 20)).take(1000).array; 978 auto class12 = class1.chain(class2); 979 auto class12Labels = "A".repeat(class1.length).chain("B".repeat(class2.length)); 980 auto plotData = class12.zip(class12Labels).array; 981 testTwoClassPlot(plotData, "issue_63", "Fake data"); 982 }