1 /* 2 ------------------------------------------------------------------- 3 4 Copyright (C) 2014, Edwin van Leeuwen 5 6 This file is part of plotd plotting library. 7 8 Plotd is free software; you can redistribute it and/or modify 9 it under the terms of the GNU General Public License as published by 10 the Free Software Foundation; either version 3 of the License, or 11 (at your option) any later version. 12 13 Plotd is distributed in the hope that it will be useful, 14 but WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 GNU General Public License for more details. 17 18 You should have received a copy of the GNU General Public License 19 along with Plotd. If not, see <http://www.gnu.org/licenses/>. 20 21 ------------------------------------------------------------------- 22 */ 23 24 module plotd.drawing; 25 import std.conv; 26 27 import cairo = cairo; 28 29 import plotd.primitives; 30 import plotd.binning; 31 32 version( unittest ) { 33 import std.stdio; 34 } 35 version( assert ) { 36 import std.stdio; 37 } 38 // Design: One surface per plot (this makes it easier for PDFSurface support 39 // Get axes context 40 // Get plot context ( probably by first getting a subsurface from the main surface ) 41 42 /// Create the plot surface with given width and height in pixels 43 cairo.Surface createPlotSurface( int width = 400, int height = 400 ) { 44 auto surface = new cairo.ImageSurface( 45 cairo.Format.CAIRO_FORMAT_ARGB32, width, height ); 46 auto context = cairo.Context( surface ); 47 clearContext( context ); 48 return surface; 49 } 50 51 /// Save surface to a file 52 void save( cairo.Surface surface, string name = "example.png" ) { 53 (cast(cairo.ImageSurface)( surface )).writeToPNG( name ); 54 } 55 56 /// Get axesContext from a surface 57 cairo.Context axesContextFromSurface( cairo.Surface surface, Bounds plotBounds, 58 Bounds marginBounds = Bounds( 100,400,100,400 )) { 59 auto context = cairo.Context( surface ); 60 61 context.translate( marginBounds.min_x, height( marginBounds ) ); 62 context.scale( marginBounds.width/plotBounds.width, 63 -marginBounds.height/plotBounds.height ); 64 context.translate( -plotBounds.min_x, -plotBounds.min_y ); 65 context.setFontSize( 14.0 ); 66 return context; 67 } 68 69 /// Get plotContext from a surface 70 cairo.Context plotContextFromSurface( cairo.Surface surface, Bounds plotBounds, 71 Bounds marginBounds = Bounds( 100,400,100,400 ) ) { 72 // Create a sub surface. Makes sure everything is plotted within plot surface 73 auto plotSurface = cairo.Surface.createForRectangle( surface, 74 cairo.Rectangle!double( marginBounds.min_x, 0, // No support for margin at top yet. Would need to know the surface dimensions 75 marginBounds.width, marginBounds.height ) ); 76 auto context = cairo.Context( plotSurface ); 77 context.translate( 0, marginBounds.height ); 78 context.scale( marginBounds.width/plotBounds.width, 79 -marginBounds.height/plotBounds.height ); 80 context.translate( -plotBounds.min_x, -plotBounds.min_y ); 81 context.setFontSize( 14.0 ); 82 return context; 83 } 84 85 /** Draw point onto context 86 87 Template function to make it Mockable 88 89 */ 90 CONTEXT drawPoint(CONTEXT)( const Point point, CONTEXT context ) { 91 auto width_height = context.deviceToUserDistance( 92 cairo.Point!double( 6.0, 6.0 ) ); 93 context.rectangle( 94 point.x-width_height.x/2.0, point.y-width_height.y/2.0, 95 width_height.x, width_height.y ); 96 context.fill(); 97 return context; 98 } 99 100 CONTEXT drawLine(CONTEXT)( const Point from, const Point to, CONTEXT context ) { 101 context.moveTo( from.x, from.y ); 102 context.lineTo( to.x, to.y ); 103 context.save(); 104 context.identityMatrix(); 105 context.stroke(); 106 context.restore(); 107 return context; 108 } 109 110 unittest { 111 import dmocks.mocks; 112 auto mocker = new Mocker(); 113 114 auto surface = createPlotSurface(); 115 auto mock = mocker.mockStruct!(cairo.Context, cairo.Surface )( 116 surface ); 117 118 mocker.expect(mock.moveTo( 0.0, 0.0 )).repeat(1); 119 mocker.expect(mock.lineTo( -1.0, -1.0 )).repeat(1); 120 mocker.expect(mock.stroke()).repeat(1); 121 mocker.expect(mock.save()).repeat(1); 122 mocker.expect(mock.identityMatrix()).repeat(1); 123 mocker.expect(mock.restore()).repeat(1); 124 mocker.replay; 125 drawLine( Point( 0, 0 ), Point( -1, -1 ), mock ); 126 mocker.verify; 127 } 128 129 /** 130 Draw axes onto the given context 131 */ 132 CONTEXT drawAxes(CONTEXT)( const Bounds bounds, CONTEXT context ) { 133 134 auto xaxis = new Axis( bounds.min_x, bounds.max_x ); 135 xaxis = adjustTickWidth( xaxis, 5 ); 136 137 auto yaxis = new Axis( bounds.min_y, bounds.max_y ); 138 yaxis = adjustTickWidth( yaxis, 5 ); 139 140 // Draw xaxis 141 context = drawLine( Point( xaxis.min, yaxis.min ), 142 Point( xaxis.max, yaxis.min ), context ); 143 // Draw ticks 144 auto tick_x = xaxis.min_tick; 145 auto tick_size = tickLength(yaxis); 146 while( tick_x < xaxis.max ) { 147 context = drawLine( Point( tick_x, yaxis.min ), 148 Point( tick_x, yaxis.min + tick_size ), context ); 149 150 context.save; 151 context.identityMatrix; 152 auto extents = context.textExtents( tick_x.to!string ); 153 auto textSize = cairo.Point!double( 0.5*extents.width, 154 -extents.height ); 155 context.restore; 156 textSize = context.deviceToUserDistance( textSize ); 157 context = drawText( tick_x.to!string, 158 Point( tick_x - textSize.x, yaxis.min - 1.5*textSize.y ), context ); 159 tick_x += xaxis.tick_width; 160 } 161 162 // Draw yaxis 163 context = drawLine( Point( xaxis.min, yaxis.min ), 164 Point( xaxis.min, yaxis.max ), context ); 165 // Draw ticks 166 auto tick_y = yaxis.min_tick; 167 tick_size = tickLength(xaxis); 168 while( tick_y < yaxis.max ) { 169 context = drawLine( Point( xaxis.min, tick_y ), 170 Point( xaxis.min + tick_size, tick_y ), context ); 171 context.save; 172 context.identityMatrix; 173 auto extents = context.textExtents( tick_y.to!string ); 174 auto textSize = cairo.Point!double( extents.height, 175 -0.5*extents.width ); 176 context.restore; 177 textSize = context.deviceToUserDistance( textSize ); 178 context = drawRotatedText( tick_y.to!string, 179 Point( xaxis.min - 0.5*textSize.x, tick_y-textSize.y ), 180 1.5*3.14, context ); 181 tick_y += yaxis.tick_width; 182 } 183 184 return context; 185 } 186 187 /// Draw xlabel 188 CONTEXT drawXLabel(CONTEXT)( string label, Bounds bounds, CONTEXT context ) { 189 auto extents = context.textExtents( label ); 190 auto textSize = cairo.Point!double( 0.5*extents.width, 191 -extents.height ); 192 textSize = context.deviceToUserDistance( textSize ); 193 context = drawText( label, 194 Point( bounds.min_x + bounds.width/2.0 - textSize.x, 195 bounds.min_y - 3.0*textSize.y ), 196 context ); 197 return context; 198 } 199 200 /// Draw ylabel 201 CONTEXT drawYLabel(CONTEXT)( string label, Bounds bounds, CONTEXT context ) { 202 auto extents = context.textExtents( label ); 203 auto textSize = cairo.Point!double( -extents.height, 204 0.5*extents.width ); 205 textSize = context.deviceToUserDistance( textSize ); 206 context = drawRotatedText( label, 207 Point( bounds.min_x + 2.0*textSize.x, 208 bounds.min_y + bounds.height/2.0 + textSize.y ), 209 1.5*3.14, context ); 210 return context; 211 } 212 213 214 /// Draw text at given location 215 CONTEXT drawText(CONTEXT)( string text, const Point location, CONTEXT context ) { 216 context.moveTo( location.x, location.y ); 217 context.save(); 218 context.identityMatrix(); 219 context.showText( text ); 220 context.restore(); 221 return context; 222 } 223 224 /// Draw rotated text on plot 225 CONTEXT drawRotatedText(CONTEXT)( string text, const Point location, 226 double radians, CONTEXT context ) { 227 context.moveTo( location.x, location.y ); 228 context.save(); 229 context.identityMatrix(); 230 context.rotate( radians ); 231 context.showText( text ); 232 context.restore(); 233 return context; 234 } 235 unittest { 236 import dmocks.mocks; 237 auto mocker = new Mocker(); 238 239 auto surface = createPlotSurface(); 240 auto mock = mocker.mockStruct!(cairo.Context, cairo.Surface )( 241 surface ); 242 243 mocker.expect(mock.moveTo( 0.0, 0.0 )).repeat(1); 244 mocker.expect(mock.save()).repeat(1); 245 mocker.expect(mock.identityMatrix()).repeat(1); 246 mocker.expect(mock.showText( "text" )).repeat(1); 247 mocker.expect(mock.restore()).repeat(1); 248 mocker.replay; 249 drawText( "text", Point( 0, 0 ), mock ); 250 mocker.verify; 251 } 252 253 CONTEXT drawBins( T : size_t, CONTEXT )( CONTEXT context, Bins!T bins ) { 254 foreach( x, count; bins ) { 255 context = drawLine( Point( x, 0 ), 256 Point( x, count.to!double ), 257 context ); 258 context = drawLine( Point( x, count.to!double ), 259 Point( x + bins.width, count.to!double ), 260 context ); 261 context = drawLine( 262 Point( x + bins.width, count.to!double ), 263 Point( x + bins.width, 0 ), 264 context ); 265 } 266 return context; 267 } 268 269 CONTEXT clearContext( CONTEXT )( CONTEXT context ) { 270 context.save(); 271 context = color( context, Color.white ); 272 context.paint(); 273 context.restore(); 274 return context; 275 } 276 277 unittest { 278 import dmocks.mocks; 279 auto mocker = new Mocker(); 280 281 auto surface = createPlotSurface(); 282 auto mock = mocker.mockStruct!(cairo.Context, cairo.Surface )( 283 surface ); 284 mocker.expect( mock.save() ).repeat(1); 285 mocker.expect( mock.setSourceRGBA( 1, 1, 1, 1 ) ).repeat(1); 286 mocker.expect( mock.paint() ).repeat(1); 287 mocker.expect( mock.restore() ).repeat(1); 288 mocker.replay; 289 clearContext( mock ); 290 mocker.verify; 291 } 292 293 CONTEXT color( CONTEXT )( CONTEXT context, const Color color ) { 294 context.setSourceRGBA( color.r, color.g, color.b, color.a ); 295 return context; 296 }