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