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 }