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.primitives; 25 26 import std.algorithm : min, max; 27 import std.conv; 28 import std.math; 29 import std.stdio; 30 import std.string; 31 32 version( unittest ) { 33 import std.algorithm : take; 34 } 35 36 /// Color class using rgba representation internally 37 class Color { 38 double r = 1, g = 1, b = 1, a = 1; 39 this( double red, double green, double blue, double alpha ) { 40 r = red; g = green; b = blue; a = alpha; } 41 42 this( string value ) { 43 auto rgba = value.split( "," ); 44 assert( rgba.length == 4 ); 45 r = to!double(rgba[0]); g = to!double(rgba[1]); 46 b = to!double(rgba[2]); a = to!double(rgba[3]); 47 } 48 49 unittest { 50 assert( new Color( "0.1,0.2,0.3,0.4" ) == new Color( 0.1, 0.2, 0.3, 0.4 ) ); 51 } 52 53 override bool opEquals( Object o ) const { 54 auto color = cast( typeof( this ) ) o; 55 return ( r == color.r && 56 g == color.g && 57 b == color.b && 58 a == color.a ); 59 } 60 61 static Color black() { 62 auto color = new Color( 0, 0, 0, 1 ); 63 return color; 64 } 65 66 static Color white() { 67 auto color = new Color( 1, 1, 1, 1 ); 68 return color; 69 } 70 71 unittest { 72 auto col1 = Color.black; 73 auto col2 = Color.black; 74 assert( col1.r == col2.r ); 75 assert( col1.g == col2.g ); 76 assert( col1.b == col2.b ); 77 assert( col1.a == col2.a ); 78 assert ( col1.opEquals( col2 ) ); 79 assert ( col1 == col2 ); 80 } 81 } 82 83 /// Infinite range of colors 84 struct ColorRange { 85 @property bool empty() const 86 { 87 return false; 88 } 89 90 @property Color front() { 91 return new Color( r, g, b, a ); 92 } 93 94 void popFront() { 95 if ( r == 0 && g == 0 && b == 0 ) 96 r = 1; 97 else if ( r == 1 && g == 0 && b == 0 ) 98 { 99 r = 0; // Skip yellow, because difficult to see on white 100 g = 1; 101 } 102 else if ( r == 0 && g == 1 && b == 0 ) 103 b = 1; 104 else if ( r == 0 && g == 1 && b == 1 ) 105 g = 0; 106 else if ( r == 0 && g == 0 && b == 1 ) { 107 b = 0; 108 } 109 } 110 111 private: 112 double r = 0; 113 double g = 0; 114 double b = 0; 115 double a = 1; 116 } 117 118 unittest { 119 Color prevColor = Color.white; 120 ColorRange colorRange; 121 foreach( col ; take( colorRange, 15 ) ) { 122 assert( prevColor != col ); 123 prevColor = col; 124 } 125 } 126 127 /// Bounds struct holding the bounds (min_x, max_x, min_y, max_y) 128 struct Bounds { 129 double min_x; 130 double max_x; 131 double min_y; 132 double max_y; 133 134 this( double my_min_x, double my_max_x, double my_min_y, double my_max_y ) { 135 min_x = my_min_x; 136 max_x = my_max_x; 137 min_y = my_min_y; 138 max_y = my_max_y; 139 } 140 141 this( string value ) { 142 auto bnds = value.split( "," ); 143 assert( bnds.length == 4 ); 144 min_x = to!double(bnds[0]); max_x = to!double(bnds[1]); 145 min_y = to!double(bnds[2]); max_y = to!double(bnds[3]); 146 } 147 148 unittest { 149 assert( Bounds( "0.1,0.2,0.3,0.4" ) == Bounds( 0.1, 0.2, 0.3, 0.4 ) ); 150 } 151 } 152 153 /// Return the height of the given bounds 154 double height( Bounds bounds ) { 155 return bounds.max_y-bounds.min_y; 156 } 157 158 unittest { 159 assert( Bounds(0,1.5,1,5).height == 4 ); 160 } 161 162 /// Return the width of the given bounds 163 double width( Bounds bounds ) { 164 return bounds.max_x-bounds.min_x; 165 } 166 167 unittest { 168 assert( Bounds(0,1.5,1,5).width == 1.5 ); 169 } 170 171 /// Is the point within the Bounds 172 bool withinBounds( Bounds bounds, Point point ) { 173 return ( point.x <= bounds.max_x && point.x >= bounds.min_x 174 && point.y <= bounds.max_y && point.y >= bounds.min_y ); 175 } 176 177 unittest { 178 assert( Bounds( 0, 1, 0, 1 ).withinBounds( Point( 1, 0 ) ) ); 179 assert( Bounds( 0, 1, 0, 1 ).withinBounds( Point( 0, 1 ) ) ); 180 assert( !Bounds( 0, 1, 0, 1 ).withinBounds( Point( 0, 1.1 ) ) ); 181 assert( !Bounds( 0, 1, 0, 1 ).withinBounds( Point( -0.1, 1 ) ) ); 182 assert( !Bounds( 0, 1, 0, 1 ).withinBounds( Point( 1.1, 0.5 ) ) ); 183 assert( !Bounds( 0, 1, 0, 1 ).withinBounds( Point( 0.1, -0.1 ) ) ); 184 } 185 186 /// Returns adjust bounds based on given bounds to include point 187 Bounds adjustedBounds( Bounds bounds, Point point ) { 188 if ( bounds.min_x > point.x ) { 189 bounds.min_x = min( bounds.min_x - 0.1*bounds.width, point.x ); 190 } else if ( bounds.max_x < point.x ) { 191 bounds.max_x = max( bounds.max_x + 0.1*bounds.width, point.x ); 192 } 193 if ( bounds.min_y > point.y ) { 194 bounds.min_y = min( bounds.min_y - 0.1*bounds.height, point.y ); 195 } else if ( bounds.max_y < point.y ) { 196 bounds.max_y = max( bounds.max_y + 0.1*bounds.height, point.y ); 197 } 198 return bounds; 199 } 200 201 unittest { 202 assert( adjustedBounds( Bounds( 0, 1, 0, 1 ), Point( 0, 1.01 ) ) == 203 Bounds( 0, 1, 0, 1.1 ) ); 204 assert( adjustedBounds( Bounds( 0, 1, 0, 1 ), Point( 0, 1.5 ) ) == 205 Bounds( 0, 1, 0, 1.5 ) ); 206 assert( adjustedBounds( Bounds( 0, 1, 0, 1 ), Point( -1, 1.01 ) ) == 207 Bounds( -1, 1, 0, 1.1 ) ); 208 assert( adjustedBounds( Bounds( 0, 1, 0, 1 ), Point( 1.2, -0.01 ) ) == 209 Bounds( 0, 1.2, -0.1, 1 ) ); 210 } 211 212 /// Can we construct valid bounds given these points 213 bool validBounds( Point[] points ) { 214 if (points.length < 2) 215 return false; 216 217 bool validx = false; 218 bool validy = false; 219 double x = points[0].x; 220 double y = points[0].y; 221 222 foreach( point; points[1..$] ) { 223 if ( point.x != x ) 224 validx = true; 225 if ( point.y != y ) 226 validy = true; 227 if (validx && validy) 228 return true; 229 } 230 return false; 231 } 232 233 unittest { 234 assert( validBounds( [ Point( 0, 1 ), Point( 1, 0 ) ] ) ); 235 assert( !validBounds( [ Point( 0, 1 ) ] ) ); 236 assert( !validBounds( [ Point( 0, 1 ), Point( 0, 0 ) ] ) ); 237 assert( !validBounds( [ Point( 0, 1 ), Point( 1, 1 ) ] ) ); 238 } 239 240 Bounds minimalBounds( Point[] points ) { 241 if (points.length == 0) 242 return Bounds( -1,1,-1,1 ); 243 244 double min_x = points[0].x; 245 double max_x = points[0].x; 246 double min_y = points[0].y; 247 double max_y = points[0].y; 248 if (points.length > 1) { 249 foreach( point; points[1..$] ) { 250 if ( point.x < min_x ) 251 min_x = point.x; 252 else if ( point.x > max_x ) 253 max_x = point.x; 254 if ( point.y < min_y ) 255 min_y = point.y; 256 else if ( point.y > max_y ) 257 max_y = point.y; 258 } 259 } 260 if (min_x == max_x) { 261 min_x = min_x - 0.5; 262 max_x = max_x + 0.5; 263 } 264 if (min_y == max_y) { 265 min_y = min_y - 0.5; 266 max_y = max_y + 0.5; 267 } 268 return Bounds( min_x, max_x, min_y, max_y ); 269 } 270 271 unittest { 272 assert( minimalBounds( [] ) == Bounds( -1, 1, -1, 1 ) ); 273 assert( minimalBounds( [Point(0,0)] ) == Bounds( -0.5, 0.5, -0.5, 0.5 ) ); 274 assert( minimalBounds( [Point(0,0),Point(0,0)] ) == Bounds( -0.5, 0.5, -0.5, 0.5 ) ); 275 assert( minimalBounds( [Point(0.1,0),Point(0,0.2)] ) == Bounds( 0, 0.1, 0, 0.2 ) ); 276 } 277 278 struct Point { 279 double x; 280 double y; 281 this( double my_x, double my_y ) { 282 x = my_x; 283 y = my_y; 284 } 285 286 this( string value ) { 287 auto coords = value.split( "," ); 288 assert( coords.length == 2 ); 289 x = to!double(coords[0]); 290 y = to!double(coords[1]); 291 } 292 293 unittest { 294 assert( Point( "1.0,0.1" ) == Point( 1.0, 0.1 ) ); 295 } 296 297 bool opEquals( const Point point ) { 298 return point.x == x && point.y == y; 299 } 300 } 301 302 Point convertCoordinates( const Point point, const Bounds orig_bounds, 303 const Bounds new_bounds ) { 304 double new_x = new_bounds.min_x + (new_bounds.max_x-new_bounds.min_x)*(point.x-orig_bounds.min_x)/(orig_bounds.max_x-orig_bounds.min_x); 305 double new_y = new_bounds.min_y + (new_bounds.max_y-new_bounds.min_y)*(point.y-orig_bounds.min_y)/(orig_bounds.max_y-orig_bounds.min_y); 306 return Point( new_x, new_y ); 307 } 308 309 unittest { 310 assert( convertCoordinates( Point( 0.5, 0.1 ), Bounds( -1, 1, -1, 1 ), 311 Bounds( 50, 100, -100, -50 ) ) == Point( 87.5, -72.5 ) ); 312 } 313 314 alias int LineId; 315 316 class LineState { 317 Color color; 318 Point end_point; 319 LineId id; 320 } 321 322 class Lines { 323 324 /// Returns an unused (new) line_id 325 LineId newLineId() { 326 last_id++; 327 return last_id; 328 } 329 330 /// Add a new line with begin point, or add to an existing line 331 void addLine( LineId id, Point point ) { 332 LineState state; 333 lines.get( id, state ); 334 if ( state is null ) { 335 state = new LineState; 336 state.color = Color.black; 337 state.id = id; 338 lines[id] = state; 339 } 340 lines[id].end_point = point; 341 mylastUsedId = id; // Keeping track of the last used line id 342 } 343 344 unittest { 345 auto lines = new Lines; 346 lines.addLine( 1, Point( 1, 2 ) ); 347 assert( lines.lines.length == 1 ); 348 assert( lines.lines[1].color == Color.black ); // Should implement equals for color 349 assert( lines.lines[1].end_point == Point( 1, 2 ) ); 350 lines.addLine( 1, Point( 2, 2 ) ); 351 assert( lines.lines[1].end_point == Point( 2, 2 ) ); 352 } 353 354 void color( LineId id, Color color ) { 355 lines[id].color = color; 356 mylastUsedId = id; // Keeping track of the last used line id 357 } 358 359 unittest { 360 auto lines = new Lines; 361 lines.addLine( 1, Point( 1, 2 ) ); 362 assert( lines.lines.length == 1 ); 363 assert( lines.lines[1].color == Color.black ); 364 lines.color( 1, new Color( 0.5, 0.5, 0.5, 0.5 ) ); 365 assert( lines.lines[1].color == new Color( 0.5, 0.5, 0.5, 0.5 ) ); 366 } 367 368 /// Return last used id 369 @property LineId lastUsedId() { 370 return mylastUsedId; 371 } 372 373 unittest { 374 auto lines = new Lines; 375 lines.addLine( 1, Point( 1, 2 ) ); 376 assert( lines.lastUsedId == 1 ); 377 lines.addLine( 2, Point( 1, 2 ) ); 378 assert( lines.lastUsedId == 2 ); 379 lines.addLine( 1, Point( 1, 1 ) ); 380 assert( lines.lastUsedId == 1 ); 381 lines.color( 2, new Color( 0.5, 0.5, 0.5, 0.5 ) ); 382 assert( lines.lastUsedId == 2 ); 383 } 384 385 private: 386 LineState[LineId] lines; 387 LineId last_id = 0; 388 LineId mylastUsedId = 0; 389 } 390 391 class Axis { 392 this( double newmin, double newmax ) { 393 min = newmin; 394 max = newmax; 395 min_tick = min; 396 } 397 string label; 398 double min = -1; 399 double max = 1; 400 double min_tick = -1; 401 double tick_width = 0.2; 402 } 403 404 /** 405 Calculate optimal tick width given an axis and an approximate number of ticks 406 */ 407 Axis adjustTickWidth( Axis axis, size_t approx_no_ticks ) { 408 auto axis_width = axis.max-axis.min; 409 auto scale = cast(int) floor(log10( axis_width )); 410 auto acceptables = [ 0.1, 0.2, 0.5, 1.0, 2.0, 5.0 ]; // Only accept ticks of these sizes 411 auto approx_width = pow(10.0, -scale)*(axis_width)/approx_no_ticks; 412 // Find closest acceptable value 413 double best = acceptables[0]; 414 double diff = abs( approx_width - best ); 415 foreach ( accept; acceptables[1..$] ) { 416 if (abs( approx_width - accept ) < diff) { 417 best = accept; 418 diff = abs( approx_width - accept ); 419 } 420 } 421 422 axis.tick_width = best*pow(10.0, scale); 423 424 // Find good min_tick 425 axis.min_tick = ceil(axis.min*pow(10.0, -scale))*pow(10.0, scale); 426 427 //debug writeln( "Here 120 ", axis.min_tick, " ", axis.min, " ", 428 // axis.max, " ", axis.tick_width, " ", scale ); 429 while (axis.min_tick - axis.tick_width > axis.min) 430 axis.min_tick -= axis.tick_width; 431 return axis; 432 } 433 434 unittest { 435 adjustTickWidth( new Axis( 0, .4 ), 5 ); 436 adjustTickWidth( new Axis( 0, 4 ), 8 ); 437 assert( adjustTickWidth( new Axis( 0, 4 ), 5 ).tick_width == 1.0 ); 438 assert( adjustTickWidth( new Axis( 0, 4 ), 8 ).tick_width == 0.5 ); 439 assert( adjustTickWidth( new Axis( 0, 0.4 ), 5 ).tick_width == 0.1 ); 440 assert( adjustTickWidth( new Axis( 0, 40 ), 8 ).tick_width == 5 ); 441 assert( adjustTickWidth( new Axis( -0.1, 4 ), 8 ).tick_width == 0.5 ); 442 443 444 assert( adjustTickWidth( new Axis( -0.1, 4 ), 8 ).min_tick == 0.0 ); 445 assert( adjustTickWidth( new Axis( 0.1, 4 ), 8 ).min_tick == 0.5 ); 446 assert( adjustTickWidth( new Axis( 1, 40 ), 8 ).min_tick == 5 ); 447 448 assert( adjustTickWidth( new Axis( 3, 4 ), 5 ).min_tick == 3 ); 449 assert( adjustTickWidth( new Axis( 3, 4 ), 5 ).tick_width == 0.2 ); 450 451 assert( adjustTickWidth( 452 new Axis( 1.79877e+07, 1.86788e+07 ), 5).min_tick == 1.8e+07 ); 453 assert( adjustTickWidth( 454 new Axis( 1.79877e+07, 1.86788e+07 ), 5).tick_width == 100000 ); 455 } 456 457 /// Calculate tick length 458 double tickLength( const Axis axis ) { 459 return (axis.max-axis.min)/25.0; 460 } 461 462 unittest { 463 auto axis = new Axis( -1, 1 ); 464 assert( tickLength( axis ) == 0.08); 465 }