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 /++
142     Aes is used to store and access data for plotting
143 
144     Aes is an InputRange, with named Tuples as the ElementType. The names
145     refer to certain fields, such as x, y, colour etc.
146 
147     The fields commonly used are data fields, such as "x" and "y". Which data
148     fields are required depends on the geom function being called. 
149     
150     Other common fields: 
151     $(UL
152         $(LI "label": Text labels (string))
153         $(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))
154         $(LI "size": Gives the relative size of points/lineWidth etc.)
155         $(LI "angle": Angle of printed labels in radians (double))
156         $(LI "alpha": Alpha value of the drawn object (double))
157         $(LI "mask": Mask the area outside the axes. Prevents you from drawing outside of the area (bool))
158         $(LI "fill": Whether to fill the object/holds the alpha value to fill with (double).))
159     +/
160 template Aes(Specs...)
161 {
162     import std.meta : AliasSeq;
163     template parseSpecs(Specs...)
164     {
165         import std.range : isInputRange, ElementType;
166         static if (Specs.length < 2)
167         {
168             alias parseSpecs = AliasSeq!();
169         }
170         else static if (
171              isInputRange!(Specs[0])
172              && is(typeof(Specs[1]) : string)
173         )
174         {
175             alias parseSpecs = AliasSeq!(
176             ElementType!(Specs[0]), Specs[1],
177                 parseSpecs!(Specs[2 .. $]));
178         }
179         else
180         {
181             pragma(msg, Specs);
182             static assert(0,
183                 "Attempted to instantiate Tuple with an " ~ "invalid argument: " ~ Specs[0].stringof);
184         }
185     }
186 
187     template parseTypes(Specs...)
188     {
189         import std.range : isInputRange;
190         static if (Specs.length < 2)
191         {
192             alias parseTypes = AliasSeq!();
193         }
194         else static if (
195              isInputRange!(Specs[0])
196              && is(typeof(Specs[1]) : string)
197         )
198         {
199             alias parseTypes = AliasSeq!(
200                 Specs[0], 
201                 parseTypes!(Specs[2 .. $]));
202         }
203         else
204         {
205             pragma(msg, Specs);
206             static assert(0,
207                 "Attempted to instantiate Tuple with an " ~ "invalid argument: " ~ Specs[0].stringof);
208         }
209     }
210     alias elementsType = parseSpecs!Specs;
211     alias types = parseTypes!Specs;
212 
213     struct Aes
214     {
215         import std.range : Zip;
216         private Zip!(types) aes;
217 
218         this(Args...)(Args args)
219         {
220             import std.range : zip;
221             aes = zip(args);
222         }
223 
224         void popFront()
225         {
226             aes.popFront;
227         }
228 
229         auto @property empty() 
230         {
231             return aes.empty;
232         }
233 
234         auto @property front()
235         {
236             return Tuple!(elementsType)( aes.front.expand );
237         }
238     }
239 }
240 
241 /// Basic Aes usage
242 unittest
243 {
244     auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([0.0, 1],
245         [2, 1.0], ["white", "white2"]);
246 
247     aes.popFront;
248     assertEqual(aes.front.y, 1);
249     assertEqual(aes.front.colour, "white2");
250 
251     auto aes2 = Aes!(double[], "x", double[], "y")([0.0, 1], [2.0, 1]);
252     assertEqual(aes2.front.y, 2);
253 
254     import std.range : repeat;
255 
256     auto xs = repeat(0);
257     auto aes3 = Aes!(typeof(xs), "x", double[], "y")(xs, [2.0, 1]);
258 
259     assertEqual(aes3.front.x, 0);
260     aes3.popFront;
261     aes3.popFront;
262     assertEqual(aes3.empty, true);
263 }
264 
265 
266 import std.typetuple : TypeTuple;
267 private template fieldValues( T, Specs... )
268 {
269     import std.typecons : Tuple, tuple;
270     auto fieldValues( T t )
271     {
272         static if (Specs.length == 0)
273             return tuple();
274         else
275             return tuple( __traits(getMember, t, Specs[0]),
276                 (fieldValues!(typeof(t), Specs[1..$])(t)).expand );
277     }
278 }
279 
280 unittest 
281 {
282     struct Point { double x; double y; string label = "Point"; }
283     auto pnt = Point( 1.0, 2.0 );
284     auto fv = fieldValues!(Point, "x","y","label")(pnt);
285     assertEqual(fv[0], 1.0);
286     assertEqual(fv[1], 2.0);
287     assertEqual(fv[2], "Point");
288     auto fv2 = fieldValues!(Point, "x","label")(pnt);
289     assertEqual(fv2[0], 1.0);
290     assertEqual(fv2[1], "Point");
291 }
292 
293 private template typeAndFields( T, Specs... )
294 {
295     import std.meta : AliasSeq;
296     static if (Specs.length == 0)
297         alias typeAndFields = AliasSeq!();
298     else
299         alias typeAndFields = AliasSeq!( 
300             typeof(__traits(getMember, T, Specs[0])), 
301             Specs[0], typeAndFields!(T, Specs[1..$]) );
302 }
303 
304 unittest 
305 {
306     struct Point { double x; double y; string label = "Point"; }
307     alias fts = typeAndFields!(Point, "x","y","label");
308 
309     auto pnt = Point( 1.0, 2.0 );
310     auto fv = fieldValues!(Point, "x","y","label")(pnt);
311     auto tp = Tuple!( fts )( fv.expand );
312     assertEqual(tp.x, 1.0);
313     assertEqual(tp.y, 2.0);
314     assertEqual(tp.label, "Point");
315  }
316 
317 // Default fields to group by
318 alias DefaultGroupFields = TypeTuple!("alpha","colour","label");
319 
320 /++
321     Groups data by colour label etc.
322 
323     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.
324 +/
325 template group(Specs...)
326 {
327     static if (Specs.length == 0)
328     {
329         alias Specs = DefaultGroupFields;
330     }
331 
332     auto extractKey(T)(T a)
333     {
334         import ggplotd.meta : ApplyLeft;
335         import std.meta : Filter;
336         alias hasFieldT = ApplyLeft!(hasAesField, T);
337         alias fields = Filter!(hasFieldT, Specs);
338         static if (fields.length == 0)
339             return 1;
340         else
341             return fieldValues!(T, fields)(a);
342     } 
343 
344     auto group(AES)(AES aes)
345     {
346         import ggplotd.range : groupBy;
347         return aes.groupBy!((a) => extractKey(a)).values;
348     }
349 }
350 
351 ///
352 unittest
353 {
354     import std.range : walkLength;
355     auto aes = Aes!(double[], "x", string[], "colour", double[], "alpha")
356         ([0.0,1,2,3], ["a","a","b","b"], [0.0,1,0,1]);
357 
358     assertEqual(group!("colour","alpha")(aes).walkLength,4);
359     assertEqual(group!("alpha")(aes).walkLength,2);
360 
361     // Ignores field that does not exist
362     assertEqual(group!("alpha","abcdef")(aes).walkLength,2);
363 
364     // Should return one group holding them all
365     assertEqual(group!("abcdef")(aes)[0].walkLength,4);
366 
367     assertEqual(group(aes).walkLength,4);
368 }
369 
370 ///
371 unittest
372 {
373     auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([1.0,
374         2.0, 1.1], [3.0, 1.5, 1.1], ["a", "b", "a"]);
375 
376     import std.range : walkLength, front, popFront;
377 
378     auto grouped = aes.group;
379     assertEqual(grouped.walkLength, 2);
380     size_t totalLength = grouped.front.walkLength;
381     assertGreaterThan(totalLength, 0);
382     assertLessThan(totalLength, 3);
383     grouped.popFront;
384     assertEqual(totalLength + grouped.front.walkLength, 3);
385 }
386 
387 import std.range : isInputRange;
388 
389 /**
390   DataID is used to refer represent any type as a usable type
391   */
392 struct DataID
393 {
394     /// Create DataID with given value and id
395     this( double value, string id )
396     {
397         import std.typecons : tuple;
398         state = tuple( value, id );
399     }
400 
401     /// Overloading to for the DataID
402     T to(T)() const
403     {
404         import std.conv : to;
405         static if (is(T==double))
406             return state[0];
407         else 
408             return state[1].to!T;
409     }
410 
411     /// Tuple holding the value and id
412     Tuple!(double, string) state; 
413 
414     alias state this;
415 }
416 
417 unittest
418 {
419     import std.conv : to;
420     auto did = DataID( 0.1, "a" );
421     assertEqual( did[0], 0.1 );
422     assertEqual( did.to!double, 0.1 );
423     assertEqual( did.to!string, "a" );
424 }
425 
426 private template aesFields(T)
427 {
428     import std.traits;
429     template isAesField(alias name)
430     {
431         import painlesstraits : isFieldOrProperty;
432         import std.typecons : Tuple;
433         // To be honest, I am not sure why isFieldOrProperty!name does not
434         // suffice (instead of the first two), but that 
435         // results in toHash for Tuple
436         static if ( __traits(compiles, isFieldOrProperty!(
437             __traits(getMember, T, name) ) )
438              && isFieldOrProperty!(__traits(getMember,T,name))
439              && name[0] != "_"[0]
440              && __traits(compiles, ( in T u ) {
441             auto a = __traits(getMember, u, name); 
442             Tuple!(typeof(a),name)(a); } )
443             )
444             enum isAesField = true;
445         else
446             enum isAesField = false;
447     }
448 
449     import std.meta : Filter;
450     enum aesFields = Filter!(isAesField, __traits(allMembers, T));
451 }
452 
453 unittest
454 {
455     struct Point { double x; double y; string label = "Point"; }
456     assertEqual( "x", aesFields!Point[0] );
457     assertEqual( "y", aesFields!Point[1] );
458     assertEqual( "label", aesFields!Point[2] );
459     assertEqual( 3, aesFields!(Point).length );
460 
461     auto pnt2 = Tuple!(double, "x", double, "y", string, "label" )( 1.0, 2.0, "Point" );
462     assertEqual( "x", aesFields!(typeof(pnt2))[0] );
463     assertEqual( "y", aesFields!(typeof(pnt2))[1] );
464     assertEqual( "label", aesFields!(typeof(pnt2))[2] );
465     assertEqual( 3, aesFields!(typeof(pnt2)).length );
466 }
467 
468 private template hasAesField(T, alias name)
469 {
470     enum bool hasAesField = (function() {
471         bool has = false;
472         foreach (name2; aesFields!T)
473         { 
474             if (name == name2)
475                 has = true;
476         }
477         return has;
478     })();
479 }
480 
481 unittest
482 {
483     struct Point { double x; double y; string label = "Point"; }
484     static assert( hasAesField!(Point, "x") );
485     static assert( !hasAesField!(Point, "z") );
486 }
487 
488 /++
489 Merge two types by their members. 
490 
491 If it has similar named members, then it uses the second one.
492 
493 returns a named Tuple (or Aes) with all the members and their values. 
494 +/
495 template merge(T, U)
496 {
497     auto merge(T base, U other)
498     {
499         import ggplotd.meta : ApplyLeft;
500         import std.meta : Filter, AliasSeq, templateNot;
501         alias fieldsU = aesFields!U;
502         alias notHasAesFieldU = ApplyLeft!(templateNot!(hasAesField),U);
503         alias fieldsT = Filter!(notHasAesFieldU, aesFields!T);
504 
505         auto vT = fieldValues!(T, fieldsT)(base);
506         auto vU = fieldValues!(U, fieldsU)(other);
507 
508         return Tuple!(AliasSeq!(
509             typeAndFields!(T,fieldsT),
510             typeAndFields!(U,fieldsU)
511             ))(vT.expand, vU.expand);
512     }
513 }
514 
515 unittest
516 {
517     auto pnt = Tuple!(double, "x", double, "y", string, "label" )( 1.0, 2.0, "Point" );
518     auto merged = DefaultValues.merge( pnt );
519     assertEqual( merged.x, 1.0 );
520     assertEqual( merged.y, 2.0 );
521     assertEqual( merged.colour, "black" );
522     assertEqual( merged.label, "Point" );
523 
524     // Test whether type/ordering is consistent
525     // Given enough benefit we can break this, but we'll have to adapt plotcli to match,
526     // which to be fair is relatively straightforward
527     static assert( is(Tuple!(string, "colour", double, "size", double, "angle", double, "alpha", 
528         bool, "mask", double, "fill", double, "x", double, "y", string, "label") == typeof(merged) ) );
529 }
530 
531 /// 
532 unittest
533 {
534     struct Point { double x; double y; string label = "Point"; }
535     auto pnt = Point( 1.0, 2.0 );
536 
537     auto merged = DefaultValues.merge( pnt );
538     assertEqual( merged.x, 1.0 );
539     assertEqual( merged.y, 2.0 );
540     assertEqual( merged.colour, "black" );
541     assertEqual( merged.label, "Point" );
542 }
543 
544 
545 static import ggplotd.range;
546 /**
547 Deprecated: Moved to ggplotd.range;
548 */
549 deprecated alias mergeRange = ggplotd.range.mergeRange;