1 module ggplotd.axes; 2 3 import std.typecons : Tuple; 4 5 version (unittest) 6 { 7 import dunit.toolkit; 8 } 9 10 /++ 11 Struct holding details on axis 12 +/ 13 struct Axis 14 { 15 /// Creating axis giving a minimum and maximum value 16 this(double newmin, double newmax) 17 { 18 min = newmin; 19 max = newmax; 20 min_tick = min; 21 } 22 23 /// Label of the axis 24 string label; 25 double textAngle = 0.0; 26 27 /// Minimum value of the axis 28 double min; 29 /// Maximum value of the axis 30 double max; 31 /// Location of the lowest tick 32 double min_tick = -1; 33 /// Distance between ticks 34 double tick_width = 0.2; 35 36 /// Offset of the axis 37 double offset; 38 39 /// Show the axis or hide it 40 bool show = true; 41 } 42 43 /// XAxis 44 struct XAxis { 45 /// The general Axis struct 46 Axis axis; 47 double textAngle = 0.0; 48 alias axis this; 49 } 50 51 /// YAxis 52 struct YAxis { 53 /// The general Axis struct 54 Axis axis; 55 double textAngle = -1.5708; 56 alias axis this; 57 } 58 59 /** 60 Is the axis properly initialized? Valid range. 61 */ 62 bool initialized( in Axis axis ) 63 { 64 import std.math : isNaN; 65 if ( isNaN(axis.min) || isNaN(axis.max) || axis.max <= axis.min ) 66 return false; 67 return true; 68 } 69 70 unittest 71 { 72 auto ax = Axis(); 73 assert( !initialized( ax ) ); 74 ax.min = -1; 75 assert( !initialized( ax ) ); 76 ax.max = -1; 77 assert( !initialized( ax ) ); 78 ax.max = 1; 79 assert( initialized( ax ) ); 80 } 81 82 /** 83 Calculate optimal tick width given an axis and an approximate number of ticks 84 */ 85 Axis adjustTickWidth(Axis axis, size_t approx_no_ticks) 86 { 87 import std.math : abs, floor, ceil, pow, log10, round; 88 assert( initialized(axis), "Axis range has not been set" ); 89 90 auto axis_width = axis.max - axis.min; 91 auto scale = cast(int) floor(log10(axis_width)); 92 auto acceptables = [0.1, 0.2, 0.5, 1.0, 2.0, 5.0]; // Only accept ticks of these sizes 93 auto approx_width = pow(10.0, -scale) * (axis_width) / approx_no_ticks; 94 // Find closest acceptable value 95 double best = acceptables[0]; 96 double diff = abs(approx_width - best); 97 foreach (accept; acceptables[1 .. $]) 98 { 99 if (abs(approx_width - accept) < diff) 100 { 101 best = accept; 102 diff = abs(approx_width - accept); 103 } 104 } 105 106 if (round(best/approx_width)>1) 107 best /= round(best/approx_width); 108 if (round(approx_width/best)>1) 109 best *= round(approx_width/best); 110 axis.tick_width = best * pow(10.0, scale); 111 // Find good min_tick 112 axis.min_tick = ceil(axis.min * pow(10.0, -scale)) * pow(10.0, scale); 113 //debug writeln( "Here 120 ", axis.min_tick, " ", axis.min, " ", 114 // axis.max, " ", axis.tick_width, " ", scale ); 115 while (axis.min_tick - axis.tick_width > axis.min) 116 axis.min_tick -= axis.tick_width; 117 return axis; 118 } 119 120 unittest 121 { 122 adjustTickWidth(Axis(0, .4), 5); 123 adjustTickWidth(Axis(0, 4), 8); 124 assert(adjustTickWidth(Axis(0, 4), 5).tick_width == 1.0); 125 assert(adjustTickWidth(Axis(0, 4), 8).tick_width == 0.5); 126 assert(adjustTickWidth(Axis(0, 0.4), 5).tick_width == 0.1); 127 assert(adjustTickWidth(Axis(0, 40), 8).tick_width == 5); 128 assert(adjustTickWidth(Axis(-0.1, 4), 8).tick_width == 0.5); 129 assert(adjustTickWidth(Axis(-0.1, 4), 8).min_tick == 0.0); 130 assert(adjustTickWidth(Axis(0.1, 4), 8).min_tick == 0.5); 131 assert(adjustTickWidth(Axis(1, 40), 8).min_tick == 5); 132 assert(adjustTickWidth(Axis(3, 4), 5).min_tick == 3); 133 assert(adjustTickWidth(Axis(3, 4), 5).tick_width == 0.2); 134 assert(adjustTickWidth(Axis(1.79877e+07, 1.86788e+07), 5).min_tick == 1.8e+07); 135 assert(adjustTickWidth(Axis(1.79877e+07, 1.86788e+07), 5).tick_width == 100_000); 136 } 137 138 private struct Ticks 139 { 140 double currentPosition; 141 Axis axis; 142 143 @property double front() 144 { 145 import std.math : abs; 146 if (currentPosition >= axis.max) 147 return axis.max; 148 // Special case for zero, because a small numerical error results in 149 // wrong label, i.e. 0 + small numerical error (of 5.5e-17) is 150 // displayed as 5.5e-17, while any other numerical error falls 151 // away in rounding 152 if (abs(currentPosition - 0) < axis.tick_width/1.0e5) 153 return 0.0; 154 return currentPosition; 155 } 156 157 void popFront() 158 { 159 if (currentPosition < axis.min_tick) 160 currentPosition = axis.min_tick; 161 else 162 currentPosition += axis.tick_width; 163 } 164 165 @property bool empty() 166 { 167 if (currentPosition - axis.tick_width >= axis.max) 168 return true; 169 return false; 170 } 171 } 172 173 /// Returns a range starting at axis.min, ending axis.max and with 174 /// all the tick locations in between 175 auto axisTicks(Axis axis) 176 { 177 return Ticks(axis.min, axis); 178 } 179 180 unittest 181 { 182 import std.array : array, front, back; 183 184 auto ax1 = adjustTickWidth(Axis(0, .4), 5).axisTicks; 185 auto ax2 = adjustTickWidth(Axis(0, 4), 8).axisTicks; 186 assertEqual(ax1.array.front, 0); 187 assertEqual(ax1.array.back, .4); 188 assertEqual(ax2.array.front, 0); 189 assertEqual(ax2.array.back, 4); 190 assertGreaterThan(ax1.array.length, 3); 191 assertLessThan(ax1.array.length, 8); 192 193 assertGreaterThan(ax2.array.length, 5); 194 assertLessThan(ax2.array.length, 10); 195 196 auto ax3 = adjustTickWidth(Axis(1.1, 2), 5).axisTicks; 197 assertEqual(ax3.array.front, 1.1); 198 assertEqual(ax3.array.back, 2); 199 } 200 201 /// Calculate tick length 202 double tickLength(in Axis axis) 203 { 204 return (axis.max - axis.min) / 25.0; 205 } 206 207 unittest 208 { 209 auto axis = Axis(-1, 1); 210 assert(tickLength(axis) == 0.08); 211 } 212 213 /** Print (axis) value, uses scientific notation for higher decimals 214 215 TODO: Could generate code to support decimals > 3 216 */ 217 string scalePrint(in double value, in uint scaleMin, in uint scaleMax) { 218 import std.math : abs; 219 import std.format : format; 220 auto diff = abs(scaleMax - scaleMin); 221 if (diff == 0) 222 return format( "%.1g", value ); 223 else if (diff == 1) 224 return format( "%.2g", value ); 225 else if (diff == 2) 226 return format( "%.3g", value ); 227 else if (diff == 3) 228 return format( "%.4g", value ); 229 else if (diff == 4) 230 return format( "%.5g", value ); 231 else if (diff == 5) 232 return format( "%.6g", value ); 233 else if (diff == 6) 234 return format( "%.7g", value ); 235 else if (diff == 7) 236 return format( "%.8g", value ); 237 return format( "%g", value ); 238 } 239 240 unittest { 241 assertEqual(1.23456.scalePrint(-1, 1), "1.23"); 242 } 243 244 /// Convert a value to an axis label 245 string toAxisLabel( double value, double max_value, double tick_width) 246 { 247 import std.math : ceil, floor, log10; 248 auto scaleMin = cast(int) floor(log10(tick_width)); 249 auto scaleMax = cast(int) ceil(log10(max_value)); 250 // Special rules for values that are human readible whole numbers 251 // (i.e. smaller than 10000) 252 if (scaleMax <= 4 && scaleMin >= 0) { 253 scaleMax = 4; 254 scaleMin = 0; 255 } 256 return value.scalePrint(scaleMin, scaleMax); 257 } 258 259 unittest { 260 assertEqual(10.toAxisLabel(20, 10), "10"); 261 assertEqual(10.toAxisLabel(10, 10), "10"); 262 } 263 264 /// Calculate tick length in plot units 265 auto tickLength(double plotSize, size_t deviceSize, double scalingX, double scalingY) 266 { 267 // We want ticks to be same size irrespcetvie of aspect ratio 268 auto scaling = (scalingX+scalingY)/2.0; 269 return scaling*10.0*plotSize/deviceSize; 270 } 271 272 unittest 273 { 274 assertEqual(tickLength(10.0, 100, 1, 0.5), tickLength(10.0, 100, 0.5, 1)); 275 assertEqual(tickLength(10.0, 100, 1, 0.5), 2.0*tickLength(5.0, 100, 0.5, 1)); 276 } 277 278 /// Aes describing the axis and its tick locations 279 auto axisAes(string type, double minC, double maxC, double lvl, double scaling = 1, Tuple!(double, string)[] ticks = []) 280 { 281 import std.algorithm : sort, uniq, map; 282 import std.array : array; 283 import std.conv : to; 284 import std.range : empty, repeat, take, popFront, walkLength, front; 285 286 import ggplotd.aes : Aes; 287 288 double[] ticksLoc; 289 auto sortedAxisTicks = ticks.sort().uniq; 290 291 string[] labels; 292 293 if (!sortedAxisTicks.empty) 294 { 295 ticksLoc = [minC] ~ sortedAxisTicks.map!((t) => t[0]).array ~ [maxC]; 296 // add voldermort type.. Using ticksLock and sortedAxisTicks 297 import std.stdio : writeln; 298 struct LabelRange(R) { 299 bool init = false; 300 double[] ticksLoc; 301 string[] ticksLab; 302 this(double[] tl, R sortedAxisTicks) { 303 ticksLoc = tl; 304 ticksLab = [""] ~ sortedAxisTicks.map!((t) => t[1]).array ~ [""]; 305 } 306 @property bool empty() 307 { 308 return ticksLoc.empty; 309 } 310 @property auto front() 311 { 312 import std.range : back; 313 if (!init || ticksLoc.length == 1) 314 return ""; 315 if (!ticksLab.front.empty) 316 return ticksLab.front; 317 return toAxisLabel(ticksLoc.front, ticksLoc.back, ticksLoc[1] - ticksLoc[0]); 318 } 319 void popFront() { 320 ticksLoc.popFront; 321 ticksLab.popFront; 322 if (!init) { 323 init = true; 324 } 325 } 326 } 327 auto lr = LabelRange!(typeof(sortedAxisTicks))(ticksLoc, sortedAxisTicks); 328 foreach(lab ; lr) 329 labels ~= lab; 330 } 331 else 332 { 333 import std.math : round; 334 import std.conv : to; 335 auto axis = Axis(minC, maxC).adjustTickWidth(round(6.0*scaling).to!size_t); 336 ticksLoc = axis.axisTicks.array; 337 labels = ticksLoc.map!((a) => a.to!double.toAxisLabel(axis.max, axis.tick_width)).array; 338 } 339 340 if (type == "x") 341 { 342 return Aes!(double[], "x", double[], "y", string[], "label", double[], "angle", 343 double[], "size")( 344 ticksLoc, lvl.repeat().take(ticksLoc.walkLength).array, labels, 345 (0.0).repeat(labels.walkLength).array, 346 (scaling).repeat(labels.walkLength).array); 347 } 348 else 349 { 350 import std.math : PI; 351 352 return Aes!(double[], "x", double[], "y", string[], "label", double[], "angle", 353 double[], "size")( 354 lvl.repeat().take(ticksLoc.walkLength).array, ticksLoc, labels, 355 ((-0.5 * PI).to!double).repeat(labels.walkLength).array, 356 (scaling).repeat(labels.walkLength).array); 357 } 358 } 359 360 unittest 361 { 362 import std.stdio : writeln; 363 364 auto aes = axisAes("x", 0.0, 1.0, 2.0); 365 assertEqual(aes.front.x, 0.0); 366 assertEqual(aes.front.y, 2.0); 367 assertEqual(aes.front.label, "0"); 368 369 aes = axisAes("y", 0.0, 1.0, 2.0, 1.0, [Tuple!(double, string)(0.2, "lbl")]); 370 aes.popFront; 371 assertEqual(aes.front.x, 2.0); 372 assertEqual(aes.front.y, 0.2); 373 assertEqual(aes.front.label, "lbl"); 374 } 375 376 private string ctReplaceAll( string orig, string pattern, string replacement ) 377 { 378 379 import std..string : split; 380 auto spl = orig.split( pattern ); 381 string str = spl[0]; 382 foreach( sp; spl[1..$] ) 383 str ~= replacement ~ sp; 384 return str; 385 } 386 387 // Create a specialised x and y axis version of a given function. 388 private string xy( string func ) 389 { 390 import std.format : format; 391 return format( "///\n%s\n\n///\n%s", 392 func 393 .ctReplaceAll( "axis", "xaxis" ) 394 .ctReplaceAll( "Axis", "XAxis" ), 395 func 396 .ctReplaceAll( "axis", "yaxis" ) 397 .ctReplaceAll( "Axis", "YAxis" ) ); 398 } 399 400 alias XAxisFunction = XAxis delegate(XAxis); 401 alias YAxisFunction = YAxis delegate(YAxis); 402 403 // Below are the external functions to be used by library users. 404 405 // Set the range of an axis 406 mixin( xy( q{auto axisRange( double min, double max ) 407 { 408 AxisFunction func = ( Axis axis ) { axis.min = min; axis.max = max; return axis; }; 409 return func; 410 }} ) ); 411 412 /// 413 unittest 414 { 415 XAxis ax; 416 auto f = xaxisRange( 0, 1 ); 417 assertEqual( f(ax).min, 0 ); 418 assertEqual( f(ax).max, 1 ); 419 420 YAxis yax; 421 auto yf = yaxisRange( 0, 1 ); 422 assertEqual( yf(yax).min, 0 ); 423 assertEqual( yf(yax).max, 1 ); 424 } 425 426 // Set the label of an axis 427 mixin( xy( q{auto axisLabel( string label ) 428 { 429 // Need to declare it as an X/YAxisFunction for the GGPlotD + overload 430 AxisFunction func = ( Axis axis ) { axis.label = label; return axis; }; 431 return func; 432 }} ) ); 433 434 /// 435 unittest 436 { 437 XAxis xax; 438 auto xf = xaxisLabel( "x" ); 439 assertEqual( xf(xax).label, "x" ); 440 441 YAxis yax; 442 auto yf = yaxisLabel( "y" ); 443 assertEqual( yf(yax).label, "y" ); 444 } 445 446 // Set the range of an axis 447 mixin( xy( q{auto axisOffset( double offset ) 448 { 449 AxisFunction func = ( Axis axis ) { axis.offset = offset; return axis; }; 450 return func; 451 }} ) ); 452 453 /// 454 unittest 455 { 456 XAxis xax; 457 auto xf = xaxisOffset( 1 ); 458 assertEqual( xf(xax).offset, 1 ); 459 460 YAxis yax; 461 auto yf = yaxisOffset( 2 ); 462 assertEqual( yf(yax).offset, 2 ); 463 } 464 465 // Hide the axis 466 mixin( xy( q{auto axisShow( bool show ) 467 { 468 // Need to declare it as an X/YAxisFunction for the GGPlotD + overload 469 AxisFunction func = ( Axis axis ) { axis.show = show; return axis; }; 470 return func; 471 }} ) ); 472 473 // Set the angle of an axis label 474 mixin( xy( q{auto axisTextAngle( double angle ) 475 { 476 import std.math : PI; 477 AxisFunction func = ( Axis axis ) { 478 axis.textAngle = angle * PI / 180.0; 479 return axis; 480 }; 481 return func; 482 }} ) ); 483 484 /// 485 unittest { 486 import std.stdio : writeln; 487 import std.math : isClose; 488 XAxis xax; 489 auto xf = xaxisTextAngle(90.0); 490 writeln(xf(xax).textAngle); 491 assert(isClose(xf(xax).textAngle, 1.5708, 0.01, 1e-5)); 492 493 YAxis yax; 494 auto yf = yaxisTextAngle(45.0); 495 assert(isClose(xf(xax).textAngle, 1.5708, 0.01, 1e-5)); 496 }