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