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 if (currentPosition >= axis.max) 134 return axis.max; 135 return currentPosition; 136 } 137 138 void popFront() 139 { 140 if (currentPosition < axis.min_tick) 141 currentPosition = axis.min_tick; 142 else 143 currentPosition += axis.tick_width; 144 } 145 146 @property bool empty() 147 { 148 if (currentPosition - axis.tick_width >= axis.max) 149 return true; 150 return false; 151 } 152 } 153 154 return Ticks(axis.min, axis); 155 } 156 157 unittest 158 { 159 import std.array : array, front, back; 160 161 auto ax1 = adjustTickWidth(Axis(0, .4), 5).axisTicks; 162 auto ax2 = adjustTickWidth(Axis(0, 4), 8).axisTicks; 163 assertEqual(ax1.array.front, 0); 164 assertEqual(ax1.array.back, .4); 165 assertEqual(ax2.array.front, 0); 166 assertEqual(ax2.array.back, 4); 167 assertGreaterThan(ax1.array.length, 3); 168 assertLessThan(ax1.array.length, 8); 169 170 assertGreaterThan(ax2.array.length, 5); 171 assertLessThan(ax2.array.length, 10); 172 173 auto ax3 = adjustTickWidth(Axis(1.1, 2), 5).axisTicks; 174 assertEqual(ax3.array.front, 1.1); 175 assertEqual(ax3.array.back, 2); 176 } 177 178 /// Calculate tick length 179 double tickLength(in Axis axis) 180 { 181 return (axis.max - axis.min) / 25.0; 182 } 183 184 unittest 185 { 186 auto axis = Axis(-1, 1); 187 assert(tickLength(axis) == 0.08); 188 } 189 190 /// 191 auto axisAes(string type, double minC, double maxC, double lvl, Tuple!(double, string)[] ticks = []) 192 { 193 import ggplotd.aes; 194 195 import std.algorithm : sort, uniq, map; 196 import std.array : array; 197 import std.conv : to; 198 import std.range : empty, repeat, take, popFront, walkLength; 199 200 double[] ticksLoc; 201 auto sortedAxisTicks = ticks.sort().uniq; 202 203 string[] labels; 204 205 if (sortedAxisTicks.walkLength > 0) 206 { 207 ticksLoc = [minC] ~ sortedAxisTicks.map!((t) => t[0]).array ~ [maxC]; 208 labels = [""] ~ sortedAxisTicks.map!((t) { 209 if (t[1].empty) 210 return t[0].to!string; 211 else 212 return t[1]; 213 }).array ~ [""]; 214 } 215 else 216 { 217 ticksLoc = Axis(minC, maxC).adjustTickWidth(5).axisTicks.array; 218 labels = ticksLoc.map!((a) => a.to!string).array; 219 } 220 221 if (type == "x") 222 { 223 return Aes!(double[], "x", double[], "y", string[], "label", double[], "angle")( 224 ticksLoc, lvl.repeat().take(ticksLoc.walkLength).array, labels, 225 (0.0).repeat(labels.walkLength).array); 226 } 227 else 228 { 229 import std.math : PI; 230 231 return Aes!(double[], "x", double[], "y", string[], "label", double[], "angle")( 232 lvl.repeat().take(ticksLoc.walkLength).array, ticksLoc, labels, 233 ((-0.5 * PI).to!double).repeat(labels.walkLength).array); 234 } 235 } 236 237 unittest 238 { 239 import std.stdio : writeln; 240 241 auto aes = axisAes("x", 0.0, 1.0, 2.0); 242 assertEqual(aes.front.x, 0.0); 243 assertEqual(aes.front.y, 2.0); 244 assertEqual(aes.front.label, "0"); 245 246 aes = axisAes("y", 0.0, 1.0, 2.0, [Tuple!(double, string)(0.2, "lbl")]); 247 aes.popFront; 248 assertEqual(aes.front.x, 2.0); 249 assertEqual(aes.front.y, 0.2); 250 assertEqual(aes.front.label, "lbl"); 251 } 252 253 private string ctReplaceAll( string orig, string pattern, string replacement ) 254 { 255 256 import std.string : split; 257 auto spl = orig.split( pattern ); 258 string str = spl[0]; 259 foreach( sp; spl[1..$] ) 260 str ~= replacement ~ sp; 261 return str; 262 } 263 264 // Create a specialised x and y axis version of a given function. 265 private string xy( string func ) 266 { 267 import std.string : split; 268 string str = "///\n" ~ // Three slashes should result in documentation? 269 func 270 .ctReplaceAll( "axis", "xaxis" ) 271 .ctReplaceAll( "Axis", "XAxis" ) ~ 272 "\n\n///\n" ~ 273 func 274 .ctReplaceAll( "axis", "yaxis" ) 275 .ctReplaceAll( "Axis", "YAxis" ); 276 return str; 277 } 278 279 alias XAxisFunction = XAxis delegate(XAxis); 280 alias YAxisFunction = YAxis delegate(YAxis); 281 282 // Below are the external functions to be used by library users. 283 284 // Set the range of an axis 285 mixin( xy( q{auto axisRange( double min, double max ) 286 { 287 AxisFunction func = ( Axis axis ) { axis.min = min; axis.max = max; return axis; }; 288 return func; 289 }} ) ); 290 291 /// 292 unittest 293 { 294 XAxis ax; 295 auto f = xaxisRange( 0, 1 ); 296 assertEqual( f(ax).min, 0 ); 297 assertEqual( f(ax).max, 1 ); 298 299 YAxis yax; 300 auto yf = yaxisRange( 0, 1 ); 301 assertEqual( yf(yax).min, 0 ); 302 assertEqual( yf(yax).max, 1 ); 303 } 304 305 // Set the label of an axis 306 mixin( xy( q{auto axisLabel( string label ) 307 { 308 // Need to declare it as an X/YAxisFunction for the GGPlotD + overload 309 AxisFunction func = ( Axis axis ) { axis.label = label; return axis; }; 310 return func; 311 }} ) ); 312 313 /// 314 unittest 315 { 316 XAxis xax; 317 auto xf = xaxisLabel( "x" ); 318 assertEqual( xf(xax).label, "x" ); 319 320 YAxis yax; 321 auto yf = yaxisLabel( "y" ); 322 assertEqual( yf(yax).label, "y" ); 323 } 324 325 // Set the range of an axis 326 mixin( xy( q{auto axisOffset( double offset ) 327 { 328 AxisFunction func = ( Axis axis ) { axis.offset = offset; return axis; }; 329 return func; 330 }} ) ); 331 332 /// 333 unittest 334 { 335 XAxis xax; 336 auto xf = xaxisOffset( 1 ); 337 assertEqual( xf(xax).offset, 1 ); 338 339 YAxis yax; 340 auto yf = yaxisOffset( 2 ); 341 assertEqual( yf(yax).offset, 2 ); 342 } 343 344