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