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