The OpenD Programming Language

1 module ggplotd.aes;
2 
3 import std.range : front, popFront, empty;
4 
5 version (unittest)
6 {
7     import dunit.toolkit;
8 }
9 
10 import std.typecons : Tuple, Typedef;
11 
12 /**
13   Number of pixels
14 
15   Mainly used to differentiate between drawing in plot coordinates or in pixel based coordinates.
16   */
17 struct Pixel
18 {
19     /// Number of pixels in int
20     this( int val ) { value = val; }
21 
22     /// Copy constructor
23     this( Pixel val ) { value = val; }
24 
25 
26     alias value this;
27 
28     /// Number of pixels
29     int value;
30 }
31 
32 unittest
33 {
34     static if (is(typeof(Pixel(10))==Pixel))
35         {} else 
36         assert(false);
37 }
38 
39 import std.typecons : tuple;
40 /++
41     Map data fields to "aesthetic" fields understood by the ggplotd geom functions
42 
43     The most commonly used aesthetic fields in ggplotd are "x" and "y". Which further data
44     fields are used/required depends on the geom function being called. 
45     
46     Other common fields: 
47     $(UL
48         $(LI "colour": Identifier for the colour. In general data points with different colour ids get different colours. This can be almost any type. You can also specify the colour by name or cairo.Color type if you want to specify an exact colour (any type that isNumeric, cairo.Color.RGB(A), or can be converted to string))
49         $(LI "size": Gives the relative size of points/lineWidth etc.)
50         $(LI "label": Text labels (string))
51         $(LI "angle": Angle of printed labels in radians (double))
52         $(LI "alpha": Alpha value of the drawn object (double))
53         $(LI "mask": Mask the area outside the axes. Prevents you from drawing outside of the area (bool))
54         $(LI "fill": Whether to fill the object/holds the alpha value to fill with (double).))
55 
56     In practice aes is an alias for std.typecons.tuple.
57 
58 Examples:
59 ---------------------------
60 struct Diamond 
61 {
62     string clarity = "SI2";
63     double carat = 0.23;
64     double price = 326;
65 }
66 
67 Diamond diamond;
68 
69 auto mapped = aes!("colour", "x", "y")(diamond.clarity, diamond.carat, diamond.price);
70 assert(mapped.colour == "SI2");
71 assert(mapped.x == 0.23);
72 assert(mapped.y == 326);
73 ---------------------------
74 
75 Examples:
76 ---------------------------
77 import std.typecons : Tuple;
78 // aes returns a named tuple
79 assert(aes!("x", "y")(1.0, 2.0) == Tuple!(double, "x", double, "y")(1.0, 2.0));
80 ---------------------------
81  
82     +/
83 alias aes = tuple;
84 
85 unittest
86 {
87     struct Diamond 
88     {
89         string clarity = "SI2";
90         double carat = 0.23;
91         double price = 326;
92     }
93 
94     Diamond diamond;
95 
96     auto mapped = aes!("colour", "x", "y")(diamond.clarity, diamond.carat, diamond.price);
97     assertEqual(mapped.colour, "SI2");
98     assertEqual(mapped.x, 0.23);
99     assertEqual(mapped.y, 326);
100 
101 
102     import std.typecons : Tuple;
103     // aes is a convenient alternative to a named tuple
104     assert(aes!("x", "y")(1.0, 2.0) == Tuple!(double, "x", double, "y")(1.0, 2.0));
105 }
106 
107 ///
108 unittest
109 {
110     auto a = aes!(int, "y", int, "x")(1, 2);
111     assertEqual( a.y, 1 );
112     assertEqual( a.x, 2 );
113 
114     auto a1 = aes!("y", "x")(1, 2);
115     assertEqual( a1.y, 1 );
116     assertEqual( a1.x, 2 );
117 
118     auto a2 = aes!("y")(1);
119     assertEqual( a2.y, 1 );
120 
121 
122     import std.range : zip;
123     import std.algorithm : map;
124     auto xs = [0,1];
125     auto ys = [2,3];
126     auto points = xs.zip(ys).map!((t) => aes!("x", "y")(t[0], t[1]));
127     assertEqual(points.front.x, 0);
128     assertEqual(points.front.y, 2);
129     points.popFront;
130     assertEqual(points.front.x, 1);
131     assertEqual(points.front.y, 3);
132 }
133 
134 // TODO Also update default grouping if appropiate
135 /// Default values for most settings
136 static auto DefaultValues = aes!(
137     "label", "colour", "size",
138     "angle", "alpha", "mask", "fill")
139     ("", "black", 1.0, 0.0, 1.0, true, 0.0);
140 
141 /// Returns field if it exists, otherwise uses the passed default
142 auto fieldWithDefault(alias field, AES, T)(AES aes, T theDefault)
143 {
144     static if (hasAesField!(AES, field))
145         return __traits(getMember, aes, field);
146     else
147         return theDefault;
148 }
149 
150 unittest 
151 {
152     struct Point { double x; double y; string label = "Point"; }
153     auto point = Point(1.0, 2.0);
154     assertEqual(fieldWithDefault!("x")(point, "1"), 1.0);
155     assertEqual(fieldWithDefault!("z")(point, "1"), "1");
156 }
157 
158 /++
159     Aes is used to store and access data for plotting
160 
161     Aes is an InputRange, with named Tuples as the ElementType. The names
162     refer to certain fields, such as x, y, colour etc.
163 
164     The fields commonly used are data fields, such as "x" and "y". Which data
165     fields are required depends on the geom function being called. 
166     
167     Other common fields: 
168     $(UL
169         $(LI "label": Text labels (string))
170         $(LI "colour": Identifier for the colour. In general data points with different colour ids get different colours. This can be almost any type. You can also specify the colour by name or cairo.Color type if you want to specify an exact colour (any type that isNumeric, cairo.Color.RGB(A), or can be converted to string))
171         $(LI "size": Gives the relative size of points/lineWidth etc.)
172         $(LI "angle": Angle of printed labels in radians (double))
173         $(LI "alpha": Alpha value of the drawn object (double))
174         $(LI "mask": Mask the area outside the axes. Prevents you from drawing outside of the area (bool))
175         $(LI "fill": Whether to fill the object/holds the alpha value to fill with (double).))
176     +/
177 template Aes(Specs...)
178 {
179     import std.meta : AliasSeq;
180     template parseSpecs(Specs...)
181     {
182         import std.range : isInputRange, ElementType;
183         static if (Specs.length < 2)
184         {
185             alias parseSpecs = AliasSeq!();
186         }
187         else static if (
188              isInputRange!(Specs[0])
189              && is(typeof(Specs[1]) : string)
190         )
191         {
192             alias parseSpecs = AliasSeq!(
193             ElementType!(Specs[0]), Specs[1],
194                 parseSpecs!(Specs[2 .. $]));
195         }
196         else
197         {
198             pragma(msg, Specs);
199             static assert(0,
200                 "Attempted to instantiate Tuple with an " ~ "invalid argument: " ~ Specs[0].stringof);
201         }
202     }
203 
204     template parseTypes(Specs...)
205     {
206         import std.range : isInputRange;
207         static if (Specs.length < 2)
208         {
209             alias parseTypes = AliasSeq!();
210         }
211         else static if (
212              isInputRange!(Specs[0])
213              && is(typeof(Specs[1]) : string)
214         )
215         {
216             alias parseTypes = AliasSeq!(
217                 Specs[0], 
218                 parseTypes!(Specs[2 .. $]));
219         }
220         else
221         {
222             pragma(msg, Specs);
223             static assert(0,
224                 "Attempted to instantiate Tuple with an " ~ "invalid argument: " ~ Specs[0].stringof);
225         }
226     }
227 
228     // maps a type to its init value 
229     private auto init(T)()
230     {
231         return T.init; 
232     }
233 
234     // ArgsCall taken from https://dlang.org/library/std/meta/alias_seq.html
235     private auto ref ArgCall(alias Func, arg)()
236     {
237         return Func!(arg)();
238     }
239 
240     // Map taken from https://dlang.org/library/std/meta/alias_seq.html
241     private template Map(alias Func, args...)
242     {
243         static if (args.length > 1)
244         {
245             alias Map = AliasSeq!(ArgCall!(Func, args[0]), Map!(Func, args[1 .. $]));
246         }
247         else
248         {
249             alias Map = ArgCall!(Func, args[0]);
250         }
251     }
252 
253     alias elementsType = parseSpecs!Specs;
254     alias types = parseTypes!Specs;
255 
256     struct Aes
257     {
258         import std.range : zip;
259         
260         // from 2.080.0 on zip can return not only Zip, but also ZipShortest. On top of that it is not accessible from
261         // outside. Therefore, use "typeof" the accessible convenience template function "zip" only. 
262         private typeof(zip!types(Map!(init, types))) aes;
263 
264         // use explicit types as they are known from template argument deduction stage
265         this(types args)
266         {
267             import std.range : zip;
268             aes = zip(args);
269         }
270 
271         void popFront()
272         {
273             aes.popFront;
274         }
275 
276         auto @property empty() 
277         {
278             return aes.empty;
279         }
280 
281         auto @property front()
282         {
283             return Tuple!(elementsType)( aes.front.expand );
284         }
285     }
286 }
287 
288 /// Basic Aes usage
289 unittest
290 {
291     auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([0.0, 1],
292         [2, 1.0], ["white", "white2"]);
293 
294     aes.popFront;
295     assertEqual(aes.front.y, 1);
296     assertEqual(aes.front.colour, "white2");
297 
298     auto aes2 = Aes!(double[], "x", double[], "y")([0.0, 1], [2.0, 1]);
299     assertEqual(aes2.front.y, 2);
300 
301     import std.range : repeat;
302 
303     auto xs = repeat(0);
304     auto aes3 = Aes!(typeof(xs), "x", double[], "y")(xs, [2.0, 1]);
305 
306     assertEqual(aes3.front.x, 0);
307     aes3.popFront;
308     aes3.popFront;
309     assertEqual(aes3.empty, true);
310 }
311 
312 
313 import std.typetuple : TypeTuple;
314 private template fieldValues( T, Specs... )
315 {
316     import std.typecons : Tuple, tuple;
317     auto fieldValues( T t )
318     {
319         static if (Specs.length == 0)
320             return tuple();
321         else
322             return tuple( __traits(getMember, t, Specs[0]),
323                 (fieldValues!(typeof(t), Specs[1..$])(t)).expand );
324     }
325 }
326 
327 unittest 
328 {
329     struct Point { double x; double y; string label = "Point"; }
330     auto pnt = Point( 1.0, 2.0 );
331     auto fv = fieldValues!(Point, "x","y","label")(pnt);
332     assertEqual(fv[0], 1.0);
333     assertEqual(fv[1], 2.0);
334     assertEqual(fv[2], "Point");
335     auto fv2 = fieldValues!(Point, "x","label")(pnt);
336     assertEqual(fv2[0], 1.0);
337     assertEqual(fv2[1], "Point");
338 }
339 
340 private template typeAndFields( T, Specs... )
341 {
342     import std.meta : AliasSeq;
343     static if (Specs.length == 0)
344         alias typeAndFields = AliasSeq!();
345     else
346         alias typeAndFields = AliasSeq!( 
347             typeof(__traits(getMember, T, Specs[0])), 
348             Specs[0], typeAndFields!(T, Specs[1..$]) );
349 }
350 
351 unittest 
352 {
353     struct Point { double x; double y; string label = "Point"; }
354     alias fts = typeAndFields!(Point, "x","y","label");
355 
356     auto pnt = Point( 1.0, 2.0 );
357     auto fv = fieldValues!(Point, "x","y","label")(pnt);
358     auto tp = Tuple!( fts )( fv.expand );
359     assertEqual(tp.x, 1.0);
360     assertEqual(tp.y, 2.0);
361     assertEqual(tp.label, "Point");
362  }
363 
364 // Default fields to group by
365 alias DefaultGroupFields = TypeTuple!("alpha","colour","label");
366 
367 /++
368     Groups data by colour label etc.
369 
370     Will also add DefaultValues for label etc to the data. It is also possible to specify exactly what to group by on as a template parameter. See example.
371 +/
372 template group(Specs...)
373 {
374     static if (Specs.length == 0)
375     {
376         alias Specs = DefaultGroupFields;
377     }
378 
379     auto extractKey(T)(T a)
380     {
381         import ggplotd.meta : ApplyLeft;
382         import std.meta : Filter;
383         alias hasFieldT = ApplyLeft!(hasAesField, T);
384         alias fields = Filter!(hasFieldT, Specs);
385         static if (fields.length == 0)
386             return 1;
387         else
388             return fieldValues!(T, fields)(a);
389     } 
390 
391     auto group(AES)(AES aes)
392     {
393         import ggplotd.range : groupBy;
394         return aes.groupBy!((a) => extractKey(a)).values;
395     }
396 }
397 
398 ///
399 unittest
400 {
401     import std.range : walkLength;
402     auto aes = Aes!(double[], "x", string[], "colour", double[], "alpha")
403         ([0.0,1,2,3], ["a","a","b","b"], [0.0,1,0,1]);
404 
405     assertEqual(group!("colour","alpha")(aes).walkLength,4);
406     assertEqual(group!("alpha")(aes).walkLength,2);
407 
408     // Ignores field that does not exist
409     assertEqual(group!("alpha","abcdef")(aes).walkLength,2);
410 
411     // Should return one group holding them all
412     assertEqual(group!("abcdef")(aes)[0].walkLength,4);
413 
414     assertEqual(group(aes).walkLength,4);
415 }
416 
417 ///
418 unittest
419 {
420     auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([1.0,
421         2.0, 1.1], [3.0, 1.5, 1.1], ["a", "b", "a"]);
422 
423     import std.range : walkLength, front, popFront;
424 
425     auto grouped = aes.group;
426     assertEqual(grouped.walkLength, 2);
427     size_t totalLength = grouped.front.walkLength;
428     assertGreaterThan(totalLength, 0);
429     assertLessThan(totalLength, 3);
430     grouped.popFront;
431     assertEqual(totalLength + grouped.front.walkLength, 3);
432 }
433 
434 import std.range : isInputRange;
435 
436 /**
437   DataID is used to refer represent any type as a usable type
438   */
439 struct DataID
440 {
441     /// Create DataID with given value and id
442     this( double value, string id )
443     {
444         import std.typecons : tuple;
445         state = tuple( value, id );
446     }
447 
448     /// Overloading to for the DataID
449     T to(T)() const
450     {
451         import std.conv : to;
452         static if (is(T==double))
453             return state[0];
454         else 
455             return state[1].to!T;
456     }
457 
458     /// Tuple holding the value and id
459     Tuple!(double, string) state; 
460 
461     alias state this;
462 }
463 
464 unittest
465 {
466     import std.conv : to;
467     auto did = DataID( 0.1, "a" );
468     assertEqual( did[0], 0.1 );
469     assertEqual( did.to!double, 0.1 );
470     assertEqual( did.to!string, "a" );
471 }
472 
473 private template aesFields(T)
474 {
475     import std.traits;
476     template isAesField(alias name)
477     {
478         import painlesstraits : isFieldOrProperty;
479         import std.typecons : Tuple;
480         // To be honest, I am not sure why isFieldOrProperty!name does not
481         // suffice (instead of the first two), but that 
482         // results in toHash for Tuple
483         static if ( __traits(compiles, isFieldOrProperty!(
484             __traits(getMember, T, name) ) )
485              && isFieldOrProperty!(__traits(getMember,T,name))
486              && name[0] != "_"[0]
487              && __traits(compiles, ( in T u ) {
488             auto a = __traits(getMember, u, name); 
489             Tuple!(typeof(a),name)(a); } )
490             )
491             enum isAesField = true;
492         else
493             enum isAesField = false;
494     }
495 
496     import std.meta : Filter;
497     enum aesFields = Filter!(isAesField, __traits(allMembers, T));
498 }
499 
500 unittest
501 {
502     struct Point { double x; double y; string label = "Point"; }
503     assertEqual( "x", aesFields!Point[0] );
504     assertEqual( "y", aesFields!Point[1] );
505     assertEqual( "label", aesFields!Point[2] );
506     assertEqual( 3, aesFields!(Point).length );
507 
508     auto pnt2 = Tuple!(double, "x", double, "y", string, "label" )( 1.0, 2.0, "Point" );
509     assertEqual( "x", aesFields!(typeof(pnt2))[0] );
510     assertEqual( "y", aesFields!(typeof(pnt2))[1] );
511     assertEqual( "label", aesFields!(typeof(pnt2))[2] );
512     assertEqual( 3, aesFields!(typeof(pnt2)).length );
513 }
514 
515 package template hasAesField(T, alias name)
516 {
517     enum bool hasAesField = (function() {
518         bool has = false;
519         foreach (name2; aesFields!T)
520         { 
521             if (name == name2)
522                 has = true;
523         }
524         return has;
525     })();
526 }
527 
528 unittest
529 {
530     struct Point { double x; double y; string label = "Point"; }
531     static assert( hasAesField!(Point, "x") );
532     static assert( !hasAesField!(Point, "z") );
533 }
534 
535 /++
536 Merge two types by their members. 
537 
538 If it has similar named members, then it uses the second one.
539 
540 returns a named Tuple (or Aes) with all the members and their values. 
541 +/
542 template merge(T, U)
543 {
544     auto merge(T base, U other)
545     {
546         import ggplotd.meta : ApplyLeft;
547         import std.meta : Filter, AliasSeq, templateNot;
548         alias fieldsU = aesFields!U;
549         alias notHasAesFieldU = ApplyLeft!(templateNot!(hasAesField),U);
550         alias fieldsT = Filter!(notHasAesFieldU, aesFields!T);
551 
552         auto vT = fieldValues!(T, fieldsT)(base);
553         auto vU = fieldValues!(U, fieldsU)(other);
554 
555         return Tuple!(AliasSeq!(
556             typeAndFields!(T,fieldsT),
557             typeAndFields!(U,fieldsU)
558             ))(vT.expand, vU.expand);
559     }
560 }
561 
562 unittest
563 {
564     auto pnt = Tuple!(double, "x", double, "y", string, "label" )( 1.0, 2.0, "Point" );
565     auto merged = DefaultValues.merge( pnt );
566     assertEqual( merged.x, 1.0 );
567     assertEqual( merged.y, 2.0 );
568     assertEqual( merged.colour, "black" );
569     assertEqual( merged.label, "Point" );
570 
571     // Test whether type/ordering is consistent
572     // Given enough benefit we can break this, but we'll have to adapt plotcli to match,
573     // which to be fair is relatively straightforward
574     static assert( is(Tuple!(string, "colour", double, "size", 
575 		double, "angle", double, "alpha", bool, "mask", 
576 		double, "fill", double, "x", double, "y", string, "label") == typeof(merged) ) );
577 }
578 
579 /// 
580 unittest
581 {
582     struct Point { double x; double y; string label = "Point"; }
583     auto pnt = Point( 1.0, 2.0 );
584 
585     auto merged = DefaultValues.merge( pnt );
586     assertEqual( merged.x, 1.0 );
587     assertEqual( merged.y, 2.0 );
588     assertEqual( merged.colour, "black" );
589     assertEqual( merged.label, "Point" );
590 }
591 
592 
593 static import ggplotd.range;
594 /**
595 Deprecated: Moved to ggplotd.range;
596 */
597 deprecated alias mergeRange = ggplotd.range.mergeRange;