The OpenD Programming Language

1 module ggplotd.axes;
2 
3 import std.typecons : Tuple;
4 
5 version (unittest)
6 {
7     import dunit.toolkit;
8 }
9 
10 /++
11 Struct holding details on axis
12 +/
13 struct Axis
14 {
15     /// Creating axis giving a minimum and maximum value
16     this(double newmin, double newmax)
17     {
18         min = newmin;
19         max = newmax;
20         min_tick = min;
21     }
22 
23     /// Label of the axis
24     string label;
25     double textAngle = 0.0;
26 
27     /// Minimum value of the axis
28     double min;
29     /// Maximum value of the axis
30     double max;
31     /// Location of the lowest tick
32     double min_tick = -1;
33     /// Distance between ticks
34     double tick_width = 0.2;
35 
36     /// Offset of the axis
37     double offset;
38 
39     /// Show the axis or hide it
40     bool show = true;
41 }
42 
43 /// XAxis
44 struct XAxis {
45     /// The general Axis struct
46     Axis axis;
47     double textAngle = 0.0;
48     alias axis this;
49 }
50 
51 /// YAxis
52 struct YAxis {
53     /// The general Axis struct
54     Axis axis;
55     double textAngle = -1.5708;
56     alias axis this;
57 }
58 
59 /**
60     Is the axis properly initialized? Valid range.
61 */
62 bool initialized( in Axis axis )
63 {
64     import std.math : isNaN;
65     if ( isNaN(axis.min) || isNaN(axis.max) || axis.max <= axis.min )
66         return false;
67     return true;
68 }
69 
70 unittest
71 {
72     auto ax = Axis();
73     assert( !initialized( ax ) );
74     ax.min = -1;
75     assert( !initialized( ax ) );
76     ax.max = -1;
77     assert( !initialized( ax ) );
78     ax.max = 1;
79     assert( initialized( ax ) );
80 }
81 
82 /**
83     Calculate optimal tick width given an axis and an approximate number of ticks
84     */
85 Axis adjustTickWidth(Axis axis, size_t approx_no_ticks)
86 {
87     import std.math : abs, floor, ceil, pow, log10, round;
88     assert( initialized(axis), "Axis range has not been set" );
89 
90     auto axis_width = axis.max - axis.min;
91     auto scale = cast(int) floor(log10(axis_width));
92     auto acceptables = [0.1, 0.2, 0.5, 1.0, 2.0, 5.0]; // Only accept ticks of these sizes
93     auto approx_width = pow(10.0, -scale) * (axis_width) / approx_no_ticks;
94     // Find closest acceptable value
95     double best = acceptables[0];
96     double diff = abs(approx_width - best);
97     foreach (accept; acceptables[1 .. $])
98     {
99         if (abs(approx_width - accept) < diff)
100         {
101             best = accept;
102             diff = abs(approx_width - accept);
103         }
104     }
105 
106     if (round(best/approx_width)>1)
107         best /= round(best/approx_width);
108     if (round(approx_width/best)>1)
109         best *= round(approx_width/best);
110     axis.tick_width = best * pow(10.0, scale);
111     // Find good min_tick
112     axis.min_tick = ceil(axis.min * pow(10.0, -scale)) * pow(10.0, scale);
113     //debug writeln( "Here 120 ", axis.min_tick, " ", axis.min, " ", 
114     //		axis.max,	" ", axis.tick_width, " ", scale );
115     while (axis.min_tick - axis.tick_width > axis.min)
116         axis.min_tick -= axis.tick_width;
117     return axis;
118 }
119 
120 unittest
121 {
122     adjustTickWidth(Axis(0, .4), 5);
123     adjustTickWidth(Axis(0, 4), 8);
124     assert(adjustTickWidth(Axis(0, 4), 5).tick_width == 1.0);
125     assert(adjustTickWidth(Axis(0, 4), 8).tick_width == 0.5);
126     assert(adjustTickWidth(Axis(0, 0.4), 5).tick_width == 0.1);
127     assert(adjustTickWidth(Axis(0, 40), 8).tick_width == 5);
128     assert(adjustTickWidth(Axis(-0.1, 4), 8).tick_width == 0.5);
129     assert(adjustTickWidth(Axis(-0.1, 4), 8).min_tick == 0.0);
130     assert(adjustTickWidth(Axis(0.1, 4), 8).min_tick == 0.5);
131     assert(adjustTickWidth(Axis(1, 40), 8).min_tick == 5);
132     assert(adjustTickWidth(Axis(3, 4), 5).min_tick == 3);
133     assert(adjustTickWidth(Axis(3, 4), 5).tick_width == 0.2);
134     assert(adjustTickWidth(Axis(1.79877e+07, 1.86788e+07), 5).min_tick == 1.8e+07);
135     assert(adjustTickWidth(Axis(1.79877e+07, 1.86788e+07), 5).tick_width == 100_000);
136 }
137 
138 private struct Ticks
139 {
140     double currentPosition;
141     Axis axis;
142 
143     @property double front()
144     {
145         import std.math : abs;
146         if (currentPosition >= axis.max)
147             return axis.max;
148         // Special case for zero, because a small numerical error results in
149         // wrong label, i.e. 0 + small numerical error (of 5.5e-17) is 
150         // displayed as 5.5e-17, while any other numerical error falls 
151         // away in rounding
152         if (abs(currentPosition - 0) < axis.tick_width/1.0e5)
153             return 0.0;
154         return currentPosition;
155     }
156 
157     void popFront()
158     {
159         if (currentPosition < axis.min_tick)
160             currentPosition = axis.min_tick;
161         else
162             currentPosition += axis.tick_width;
163     }
164 
165     @property bool empty()
166     {
167         if (currentPosition - axis.tick_width >= axis.max)
168             return true;
169         return false;
170     }
171 }
172 
173 /// Returns a range starting at axis.min, ending axis.max and with
174 /// all the tick locations in between
175 auto axisTicks(Axis axis)
176 {
177     return Ticks(axis.min, axis);
178 }
179 
180 unittest
181 {
182     import std.array : array, front, back;
183 
184     auto ax1 = adjustTickWidth(Axis(0, .4), 5).axisTicks;
185     auto ax2 = adjustTickWidth(Axis(0, 4), 8).axisTicks;
186     assertEqual(ax1.array.front, 0);
187     assertEqual(ax1.array.back, .4);
188     assertEqual(ax2.array.front, 0);
189     assertEqual(ax2.array.back, 4);
190     assertGreaterThan(ax1.array.length, 3);
191     assertLessThan(ax1.array.length, 8);
192 
193     assertGreaterThan(ax2.array.length, 5);
194     assertLessThan(ax2.array.length, 10);
195 
196     auto ax3 = adjustTickWidth(Axis(1.1, 2), 5).axisTicks;
197     assertEqual(ax3.array.front, 1.1);
198     assertEqual(ax3.array.back, 2);
199 }
200 
201 /// Calculate tick length
202 double tickLength(in Axis axis)
203 {
204     return (axis.max - axis.min) / 25.0;
205 }
206 
207 unittest
208 {
209     auto axis = Axis(-1, 1);
210     assert(tickLength(axis) == 0.08);
211 }
212 
213 /** Print (axis) value, uses scientific notation for higher decimals
214 
215 TODO: Could generate code to support decimals > 3
216 */
217 string scalePrint(in double value, in uint scaleMin, in uint scaleMax) {
218     import std.math : abs;
219     import std.format : format;
220     auto diff = abs(scaleMax - scaleMin);
221     if (diff == 0)
222         return format( "%.1g", value );
223     else if (diff == 1)
224         return format( "%.2g", value );
225     else if (diff == 2)
226         return format( "%.3g", value );
227     else if (diff == 3)
228         return format( "%.4g", value );
229     else if (diff == 4)
230         return format( "%.5g", value );
231     else if (diff == 5)
232         return format( "%.6g", value );
233     else if (diff == 6)
234         return format( "%.7g", value );
235     else if (diff == 7)
236         return format( "%.8g", value );
237     return format( "%g", value );
238 }
239 
240 unittest {
241     assertEqual(1.23456.scalePrint(-1, 1), "1.23");
242 }
243 
244 /// Convert a value to an axis label
245 string toAxisLabel( double value, double max_value, double tick_width)
246 {
247     import std.math : ceil, floor, log10;
248     auto scaleMin = cast(int) floor(log10(tick_width));
249     auto scaleMax = cast(int) ceil(log10(max_value));
250     // Special rules for values that are human readible whole numbers 
251     // (i.e. smaller than 10000)
252     if (scaleMax <= 4 && scaleMin >= 0) {
253         scaleMax = 4;
254         scaleMin = 0;
255     }
256     return value.scalePrint(scaleMin, scaleMax);
257 }
258 
259 unittest {
260     assertEqual(10.toAxisLabel(20, 10), "10");
261     assertEqual(10.toAxisLabel(10, 10), "10");
262 }
263 
264 /// Calculate tick length in plot units
265 auto tickLength(double plotSize, size_t deviceSize, double scalingX, double scalingY)
266 {
267     // We want ticks to be same size irrespcetvie of aspect ratio
268     auto scaling = (scalingX+scalingY)/2.0;
269     return scaling*10.0*plotSize/deviceSize;
270 }
271 
272 unittest
273 {
274     assertEqual(tickLength(10.0, 100, 1, 0.5), tickLength(10.0, 100, 0.5, 1));
275     assertEqual(tickLength(10.0, 100, 1, 0.5), 2.0*tickLength(5.0, 100, 0.5, 1));
276 }
277 
278 /// Aes describing the axis and its tick locations
279 auto axisAes(string type, double minC, double maxC, double lvl, double scaling = 1, Tuple!(double, string)[] ticks = [])
280 {
281     import std.algorithm : sort, uniq, map;
282     import std.array : array;
283     import std.conv : to; 
284     import std.range : empty, repeat, take, popFront, walkLength, front;
285 
286     import ggplotd.aes : Aes;
287 
288     double[] ticksLoc;
289     auto sortedAxisTicks = ticks.sort().uniq;
290 
291     string[] labels;
292 
293     if (!sortedAxisTicks.empty)
294     {
295         ticksLoc = [minC] ~ sortedAxisTicks.map!((t) => t[0]).array ~ [maxC];
296         // add voldermort type.. Using ticksLock and sortedAxisTicks
297         import std.stdio : writeln;
298         struct LabelRange(R) {
299             bool init = false;
300             double[] ticksLoc;
301             string[] ticksLab;
302             this(double[] tl, R sortedAxisTicks) {
303                 ticksLoc = tl;
304                 ticksLab = [""] ~ sortedAxisTicks.map!((t) => t[1]).array ~ [""];
305             }
306             @property bool empty() 
307             {
308                 return ticksLoc.empty;
309             }
310             @property auto front()
311             {
312                 import std.range : back;
313                 if (!init || ticksLoc.length == 1)
314                     return "";
315                 if (!ticksLab.front.empty)
316                     return ticksLab.front;
317                 return toAxisLabel(ticksLoc.front, ticksLoc.back, ticksLoc[1] - ticksLoc[0]);
318             }
319             void popFront() {
320                 ticksLoc.popFront;
321                 ticksLab.popFront;
322                 if (!init) {
323                     init = true;
324                 }
325             }
326         }
327         auto lr = LabelRange!(typeof(sortedAxisTicks))(ticksLoc, sortedAxisTicks);
328         foreach(lab ; lr)
329             labels ~= lab;
330     }
331     else
332     {
333         import std.math : round;
334         import std.conv : to;
335         auto axis = Axis(minC, maxC).adjustTickWidth(round(6.0*scaling).to!size_t);
336         ticksLoc = axis.axisTicks.array;
337         labels = ticksLoc.map!((a) => a.to!double.toAxisLabel(axis.max, axis.tick_width)).array;
338     }
339 
340     if (type == "x")
341     {
342         return Aes!(double[], "x", double[], "y", string[], "label", double[], "angle",
343             double[], "size")(
344             ticksLoc, lvl.repeat().take(ticksLoc.walkLength).array, labels,
345             (0.0).repeat(labels.walkLength).array,
346             (scaling).repeat(labels.walkLength).array);
347     }
348     else
349     {
350         import std.math : PI;
351 
352         return Aes!(double[], "x", double[], "y", string[], "label", double[], "angle",
353             double[], "size")(
354             lvl.repeat().take(ticksLoc.walkLength).array, ticksLoc, labels,
355             ((-0.5 * PI).to!double).repeat(labels.walkLength).array,
356             (scaling).repeat(labels.walkLength).array);
357     }
358 }
359 
360 unittest
361 {
362     import std.stdio : writeln;
363 
364     auto aes = axisAes("x", 0.0, 1.0, 2.0);
365     assertEqual(aes.front.x, 0.0);
366     assertEqual(aes.front.y, 2.0);
367     assertEqual(aes.front.label, "0");
368 
369     aes = axisAes("y", 0.0, 1.0, 2.0, 1.0, [Tuple!(double, string)(0.2, "lbl")]);
370     aes.popFront;
371     assertEqual(aes.front.x, 2.0);
372     assertEqual(aes.front.y, 0.2);
373     assertEqual(aes.front.label, "lbl");
374 }
375 
376 private string ctReplaceAll( string orig, string pattern, string replacement )
377 {
378 
379     import std.string : split;
380     auto spl = orig.split( pattern );
381     string str = spl[0];
382     foreach( sp; spl[1..$] )
383         str ~= replacement ~ sp;
384     return str;
385 }
386 
387 // Create a specialised x and y axis version of a given function.
388 private string xy( string func )
389 {
390     import std.format : format;
391     return format( "///\n%s\n\n///\n%s",
392         func
393             .ctReplaceAll( "axis", "xaxis" )
394             .ctReplaceAll( "Axis", "XAxis" ),
395         func
396             .ctReplaceAll( "axis", "yaxis" )
397             .ctReplaceAll( "Axis", "YAxis" ) );
398 }
399 
400 alias XAxisFunction = XAxis delegate(XAxis);
401 alias YAxisFunction = YAxis delegate(YAxis);
402 
403 // Below are the external functions to be used by library users.
404 
405 // Set the range of an axis
406 mixin( xy( q{auto axisRange( double min, double max ) 
407 { 
408     AxisFunction func = ( Axis axis ) { axis.min = min; axis.max = max; return axis; }; 
409     return func;
410 }} ) );
411 
412 ///
413 unittest
414 {
415     XAxis ax;
416     auto f = xaxisRange( 0, 1 );
417     assertEqual( f(ax).min, 0 );
418     assertEqual( f(ax).max, 1 );
419 
420     YAxis yax;
421     auto yf = yaxisRange( 0, 1 );
422     assertEqual( yf(yax).min, 0 );
423     assertEqual( yf(yax).max, 1 );
424 }
425 
426 // Set the label of an axis
427 mixin( xy( q{auto axisLabel( string label ) 
428 { 
429     // Need to declare it as an X/YAxisFunction for the GGPlotD + overload
430     AxisFunction func = ( Axis axis ) { axis.label = label; return axis; }; 
431     return func;
432 }} ) );
433 
434 ///
435 unittest
436 {
437     XAxis xax;
438     auto xf = xaxisLabel( "x" );
439     assertEqual( xf(xax).label, "x" );
440 
441     YAxis yax;
442     auto yf = yaxisLabel( "y" );
443     assertEqual( yf(yax).label, "y" );
444 }
445 
446 // Set the range of an axis
447 mixin( xy( q{auto axisOffset( double offset ) 
448 { 
449     AxisFunction func = ( Axis axis ) { axis.offset = offset; return axis; }; 
450     return func;
451 }} ) );
452 
453 ///
454 unittest
455 {
456     XAxis xax;
457     auto xf = xaxisOffset( 1 );
458     assertEqual( xf(xax).offset, 1 );
459 
460     YAxis yax;
461     auto yf = yaxisOffset( 2 );
462     assertEqual( yf(yax).offset, 2 );
463 }
464 
465 // Hide the axis 
466 mixin( xy( q{auto axisShow( bool show ) 
467 { 
468     // Need to declare it as an X/YAxisFunction for the GGPlotD + overload
469     AxisFunction func = ( Axis axis ) { axis.show = show; return axis; }; 
470     return func;
471 }} ) );
472 
473 // Set the angle of an axis label
474 mixin( xy( q{auto axisTextAngle( double angle ) 
475 { 
476     import std.math : PI;
477     AxisFunction func = ( Axis axis ) {
478         axis.textAngle = angle * PI / 180.0;
479         return axis; 
480     }; 
481     return func;
482 }} ) );
483 
484 ///
485 unittest {
486   import std.stdio : writeln;
487   import std.math : isClose;
488   XAxis xax;
489   auto xf = xaxisTextAngle(90.0);
490   writeln(xf(xax).textAngle);
491   assert(isClose(xf(xax).textAngle, 1.5708, 0.01, 1e-5));
492 
493   YAxis yax;
494   auto yf = yaxisTextAngle(45.0);
495   assert(isClose(xf(xax).textAngle, 1.5708, 0.01, 1e-5));
496 }