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