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.aes; 9 import ggplotd.axes; 10 import ggplotd.colour; 11 import ggplotd.geom; 12 import ggplotd.bounds; 13 import ggplotd.scale; 14 import ggplotd.theme; 15 16 version (unittest) 17 { 18 import dunit.toolkit; 19 } 20 21 alias TitleFunction = Title delegate(Title); 22 23 // Currently only holds the title. In the future could also be used to store details on location etc. 24 struct Title 25 { 26 /// The actual title 27 string title; 28 } 29 30 /// 31 TitleFunction title( string title ) 32 { 33 return delegate(Title t) { t.title = title; return t; }; 34 } 35 36 private auto createEmptySurface( string fname, int width, int height, 37 RGBA colour ) 38 { 39 cairo.Surface surface; 40 41 static if (cconfig.CAIRO_HAS_PDF_SURFACE) 42 { 43 if (fname[$ - 3 .. $] == "pdf") 44 { 45 surface = new cpdf.PDFSurface(fname, width, height); 46 } 47 } 48 else 49 { 50 if (fname[$ - 3 .. $] == "pdf") 51 assert(0, "PDF support not enabled by cairoD"); 52 } 53 static if (cconfig.CAIRO_HAS_SVG_SURFACE) 54 { 55 if (fname[$ - 3 .. $] == "svg") 56 { 57 surface = new csvg.SVGSurface(fname, width, height); 58 } 59 } 60 else 61 { 62 if (fname[$ - 3 .. $] == "svg") 63 assert(0, "SVG support not enabled by cairoD"); 64 } 65 if (fname[$ - 3 .. $] == "png") 66 { 67 surface = new cairo.ImageSurface(cairo.Format.CAIRO_FORMAT_ARGB32, width, height); 68 } 69 70 auto backcontext = cairo.Context(surface); 71 backcontext.setSourceRGBA(colour); 72 backcontext.paint; 73 74 return surface; 75 } 76 77 /// 78 auto drawTitle( in Title title, ref cairo.Surface surface, 79 in Margins margins, int width, int height ) 80 { 81 auto context = cairo.Context(surface); 82 context.setFontSize(16.0); 83 context.moveTo( width/2, margins.top/2 ); 84 auto extents = context.textExtents(title.title); 85 86 auto textSize = cairo.Point!double(0.5 * extents.width, 0.5 * extents.height); 87 context.relMoveTo(-textSize.x, textSize.y); 88 89 context.showText(title.title); 90 return surface; 91 } 92 93 auto drawGeom( Geom geom, ref cairo.Surface surface, 94 ColourMap colourMap, ScaleType scaleFunction, in Bounds bounds, 95 in Margins margins, int width, int height ) 96 { 97 cairo.Context context; 98 if (geom.mask) { 99 auto plotSurface = cairo.Surface.createForRectangle(surface, 100 cairo.Rectangle!double(margins.left, margins.top, 101 width - (margins.left+margins.right), 102 height - (margins.top+margins.bottom))); 103 context = cairo.Context(plotSurface); 104 } else { 105 context = cairo.Context(surface); 106 context.translate(margins.left, margins.top); 107 } 108 import std.conv : to; 109 context = scaleFunction(context, bounds, 110 width.to!double - (margins.left+margins.right), 111 height.to!double - (margins.top+margins.bottom)); 112 context = geom.draw(context, colourMap); 113 return surface; 114 } 115 116 /// 117 struct Margins 118 { 119 size_t left = 50; /// 120 size_t right = 20; /// 121 size_t bottom = 50; /// 122 size_t top = 40; /// 123 } 124 125 /// 126 struct GGPlotD 127 { 128 Geom[] geomRange; 129 130 XAxis xaxis; 131 YAxis yaxis; 132 133 Margins margins; 134 135 Title title; 136 Theme theme; 137 138 ScaleType scaleFunction; 139 140 /// 141 auto drawToSurface( ref cairo.Surface surface, int width, int height ) 142 { 143 if (!initScale) 144 scaleFunction = scale(); // This needs to be removed later 145 import std.range : empty, front; 146 147 AdaptiveBounds bounds; 148 ColourID[] colourIDs; 149 Tuple!(double, string)[] xAxisTicks; 150 Tuple!(double, string)[] yAxisTicks; 151 152 foreach (geom; geomRange) 153 { 154 bounds.adapt(geom.bounds); 155 colourIDs ~= geom.colours; 156 xAxisTicks ~= geom.xTickLabels; 157 yAxisTicks ~= geom.yTickLabels; 158 } 159 160 auto colourMap = createColourMap(colourIDs); 161 162 // Axis 163 import std.algorithm : sort, uniq, min, max; 164 import std.range : chain; 165 import std.array : array; 166 import ggplotd.axes; 167 168 // If ticks are provided then we make sure the bounds include them 169 auto sortedTicks = xAxisTicks.sort().uniq.array; 170 if (!sortedTicks.empty) 171 { 172 bounds.min_x = min( bounds.min_x, sortedTicks[0][0] ); 173 bounds.max_x = max( bounds.max_x, sortedTicks[$-1][0] ); 174 } 175 if (initialized(xaxis)) 176 { 177 bounds.min_x = xaxis.min; 178 bounds.max_x = xaxis.max; 179 } 180 181 import std.math : isNaN; 182 auto offset = bounds.min_y; 183 if (!isNaN(xaxis.offset)) 184 offset = xaxis.offset; 185 auto aesX = axisAes("x", bounds.min_x, bounds.max_x, offset, 186 sortedTicks ); 187 188 sortedTicks = yAxisTicks.sort().uniq.array; 189 if (!sortedTicks.empty) 190 { 191 bounds.min_y = min( bounds.min_y, sortedTicks[0][0] ); 192 bounds.max_y = max( bounds.max_y, sortedTicks[$-1][0] ); 193 } 194 if (initialized(yaxis)) 195 { 196 bounds.min_y = yaxis.min; 197 bounds.max_y = yaxis.max; 198 } 199 200 offset = bounds.min_x; 201 if (!isNaN(yaxis.offset)) 202 offset = yaxis.offset; 203 auto aesY = axisAes("y", bounds.min_y, bounds.max_y, offset, 204 sortedTicks ); 205 206 auto gR = chain(geomAxis(aesX, 10.0*bounds.height / height, xaxis.label), geomAxis(aesY, 10.0*bounds.width / width, yaxis.label)); 207 208 // Plot axis and geomRange 209 foreach (geom; chain(geomRange, gR) ) 210 { 211 surface = geom.drawGeom( surface, 212 colourMap, scaleFunction, bounds, 213 margins, width, height ); 214 } 215 216 // Plot title 217 surface = title.drawTitle( surface, margins, width, height ); 218 return surface; 219 } 220 221 222 /// 223 void save( string fname, int width = 470, int height = 470 ) 224 { 225 bool pngWrite = false; 226 auto surface = createEmptySurface( fname, width, height, 227 theme.backgroundColour ); 228 229 surface = drawToSurface( surface, width, height ); 230 231 if (fname[$ - 3 .. $] == "png") 232 { 233 pngWrite = true; 234 } 235 236 if (pngWrite) 237 (cast(cairo.ImageSurface)(surface)).writeToPNG(fname); 238 } 239 240 /// Using + to extend the plot for compatibility to ggplot2 in R 241 ref GGPlotD opBinary(string op, T)(T rhs) if (op == "+") 242 { 243 static if (is(ElementType!T==Geom)) 244 { 245 import std.array : array; 246 geomRange ~= rhs.array; 247 } 248 static if (is(T==ScaleType)) 249 { 250 initScale = true; 251 scaleFunction = rhs; 252 } 253 static if (is(T==XAxisFunction)) 254 { 255 xaxis = rhs( xaxis ); 256 } 257 static if (is(T==YAxisFunction)) 258 { 259 yaxis = rhs( yaxis ); 260 } 261 static if (is(T==TitleFunction)) 262 { 263 title = rhs( title ); 264 } 265 static if (is(T==ThemeFunction)) 266 { 267 theme = rhs( theme ); 268 } 269 static if (is(T==Margins)) 270 { 271 margins = rhs; 272 } 273 return this; 274 } 275 276 /// 277 ref GGPlotD put(T)(T rhs) 278 { 279 return this.opBinary!("+", T)(rhs); 280 } 281 282 private: 283 bool initScale = false; 284 } 285 286 unittest 287 { 288 auto gg = GGPlotD() 289 .put( yaxisLabel( "My ylabel" ) ) 290 .put( yaxisRange( 0, 2.0 ) ); 291 assertEqual( gg.yaxis.max, 2.0 ); 292 assertEqual( gg.yaxis.label, "My ylabel" ); 293 294 gg = GGPlotD(); 295 gg.put( yaxisLabel( "My ylabel" ) ) 296 .put( yaxisRange( 0, 2.0 ) ); 297 assertEqual( gg.yaxis.max, 2.0 ); 298 assertEqual( gg.yaxis.label, "My ylabel" ); 299 } 300 301 302 /// 303 unittest 304 { 305 auto aes = Aes!(string[], "x", string[], "y", string[], "colour")(["a", 306 "b", "c", "b"], ["x", "y", "y", "x"], ["b", "b", "b", "b"]); 307 auto gg = GGPlotD(); 308 gg + geomLine(aes) + scale(); 309 gg.save( "test6.png"); 310 } 311 312 /// 313 unittest 314 { 315 /// http://blackedder.github.io/ggplotd/images/noise.png 316 import std.array : array; 317 import std.math : sqrt; 318 import std.algorithm : map; 319 import std.range : repeat, iota; 320 import std.random : uniform; 321 // Generate some noisy data with reducing width 322 auto f = (double x) { return x/(1+x); }; 323 auto width = (double x) { return sqrt(0.1/(1+x)); }; 324 auto xs = iota( 0, 10, 0.1 ).array; 325 326 auto ysfit = xs.map!((x) => f(x)); 327 auto ysnoise = xs.map!((x) => f(x) + uniform(-width(x),width(x))).array; 328 329 auto aes = Aes!(typeof(xs), "x", 330 typeof(ysnoise), "y", string[], "colour" )( xs, ysnoise, ("a").repeat(xs.length).array ); 331 auto gg = GGPlotD().put( geomPoint( aes ) ); 332 gg.put( geomLine( Aes!(typeof(xs), "x", 333 typeof(ysfit), "y" )( xs, ysfit ) ) ); 334 335 // 336 auto ys2fit = xs.map!((x) => 1-f(x)); 337 auto ys2noise = xs.map!((x) => 1-f(x) + uniform(-width(x),width(x))).array; 338 339 gg.put( geomLine( Aes!(typeof(xs), "x", typeof(ys2fit), "y" )( xs, 340 ys2fit) ) ) 341 .put( 342 geomPoint( Aes!(typeof(xs), "x", typeof(ys2noise), "y", string[], 343 "colour" )( xs, ys2noise, ("b").repeat(xs.length).array) ) ); 344 345 gg.save( "noise.png" ); 346 } 347 348 /// 349 unittest 350 { 351 /// http://blackedder.github.io/ggplotd/images/hist.png 352 import std.array : array; 353 import std.algorithm : map; 354 import std.range : repeat, iota; 355 import std.random : uniform; 356 auto xs = iota(0,25,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array; 357 auto aes = Aes!(typeof(xs), "x")( xs ); 358 auto gg = GGPlotD().put( geomHist( aes ) ); 359 360 auto ys = (0.0).repeat( xs.length ).array; 361 auto aesPs = aes.mergeRange( Aes!(double[], "y", double[], "colour" ) 362 ( ys, ys ) ); 363 gg.put( geomPoint( aesPs ) ); 364 365 gg.save( "hist.png" ); 366 } 367 368 /// 369 unittest 370 { 371 /// http://blackedder.github.io/ggplotd/images/filled_hist.svg 372 import std.array : array; 373 import std.algorithm : map; 374 import std.range : repeat, iota, chain; 375 import std.random : uniform; 376 auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array; 377 auto cols = "a".repeat(25).chain("b".repeat(25)); 378 auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 379 double[], "fill" )( 380 xs, cols, 0.45.repeat(xs.length).array); 381 auto gg = GGPlotD().put( geomHist( aes ) ); 382 gg.save( "filled_hist.svg" ); 383 } 384 385 /// Boxplot example 386 unittest 387 { 388 /// http://blackedder.github.io/ggplotd/images/boxplot.svg 389 import std.array : array; 390 import std.algorithm : map; 391 import std.range : repeat, iota, chain; 392 import std.random : uniform; 393 auto xs = iota(0,50,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array; 394 auto cols = "a".repeat(25).chain("b".repeat(25)).array; 395 auto aes = Aes!(typeof(xs), "x", typeof(cols), "colour", 396 double[], "fill", typeof(cols), "label" )( 397 xs, cols, 0.45.repeat(xs.length).array, cols); 398 auto gg = GGPlotD().put( geomBox( aes ) ); 399 gg.save( "boxplot.svg" ); 400 } 401 402 /// 403 unittest 404 { 405 /// http://blackedder.github.io/ggplotd/images/hist3D.svg 406 import std.array : array; 407 import std.algorithm : map; 408 import std.range : repeat, iota; 409 import std.random : uniform; 410 411 auto xs = iota(0,100,1).map!((x) => uniform(0.0,5)+uniform(0.0,5)).array; 412 auto ys = iota(0,100,1).map!((y) => uniform(0.0,5)+uniform(0.0,5)).array; 413 auto aes = Aes!(typeof(xs), "x", typeof(ys), "y")( xs, ys); 414 auto gg = GGPlotD().put( geomHist3D( aes ) ); 415 416 gg.save( "hist3D.svg" ); 417 } 418 419 420 421 /// Changing axes details 422 unittest 423 { 424 /// http://blackedder.github.io/ggplotd/images/axes.svg 425 import std.array : array; 426 import std.math : sqrt; 427 import std.algorithm : map; 428 import std.range : iota; 429 // Generate some noisy data with reducing width 430 auto f = (double x) { return x/(1+x); }; 431 auto width = (double x) { return sqrt(0.1/(1+x)); }; 432 auto xs = iota( 0, 10, 0.1 ).array; 433 434 auto ysfit = xs.map!((x) => f(x)).array; 435 436 auto gg = GGPlotD().put( geomLine( Aes!(typeof(xs), "x", 437 typeof(ysfit), "y" )( xs, ysfit ) ) ); 438 439 // Setting range and label for xaxis 440 gg.put( xaxisRange( 0, 8 ) ).put( xaxisLabel( "My xlabel" ) ); 441 assertEqual( gg.xaxis.min, 0 ); 442 // Setting range and label for yaxis 443 gg.put( yaxisRange( 0, 2.0 ) ).put( yaxisLabel( "My ylabel" ) ); 444 assertEqual( gg.yaxis.max, 2.0 ); 445 assertEqual( gg.yaxis.label, "My ylabel" ); 446 447 // change offset 448 gg.put( xaxisOffset( 0.25 ) ).put( yaxisOffset( 0.5 ) ); 449 450 // Change Margins 451 gg.put( Margins( 60, 60, 40, 30 ) ); 452 453 // Set a title 454 gg.put( title( "And now for something completely different" ) ); 455 assertEqual( gg.title.title, "And now for something completely different" ); 456 457 // Saving on a 500x300 pixel surface 458 gg.save( "axes.svg", 500, 300 ); 459 } 460 461 /// Polygon 462 unittest 463 { 464 /// http://blackedder.github.io/ggplotd/images/polygon.png 465 auto gg = GGPlotD().put( geomPolygon( 466 Aes!( 467 double[], "x", 468 double[], "y", 469 double[], "colour" )( 470 [1,0,0], [ 1, 1, 0 ], [1,0.1,0] ) ) ); 471 gg.save( "polygon.png" ); 472 } 473 474 /// Setting background colour 475 unittest 476 { 477 /// http://blackedder.github.io/ggplotd/images/background.svg 478 import ggplotd.theme; 479 auto gg = GGPlotD().put( background( RGBA(0.7,0.7,0.7,1) ) ); 480 gg.put( geomPoint( 481 Aes!( 482 double[], "x", 483 double[], "y", 484 double[], "colour" )( 485 [1,0,0], [ 1, 1, 0 ], [1,0.1,0] ) ) ); 486 gg.save( "background.svg" ); 487 } 488 489 /// Other data type 490 unittest 491 { 492 /// http://blackedder.github.io/ggplotd/images/data.png 493 import std.array : array; 494 import std.math : sqrt; 495 import std.algorithm : map; 496 import std.range : repeat, iota; 497 import std.random : uniform; 498 struct Point { double x; double y; } 499 // Generate some noisy data with reducing width 500 auto f = (double x) { return x/(1+x); }; 501 auto width = (double x) { return sqrt(0.1/(1+x)); }; 502 auto xs = iota( 0, 10, 0.1 ).array; 503 504 auto points = xs.map!((x) => Point(x, 505 f(x) + uniform(-width(x),width(x)))); 506 507 auto gg = GGPlotD().put( geomPoint( points ) ); 508 509 gg.save( "data.png" ); 510 } 511 512