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