1 module ggplotd.guide;
2 
3 version (unittest)
4 {
5     import dunit.toolkit;
6 }
7 
8 private struct DiscreteStoreWithOffset
9 {
10     import std.typecons : Tuple, tuple;
11     size_t[string] store;
12     Tuple!(double, double)[] offsets;
13 
14     bool put(in DiscreteStoreWithOffset ds)
15     {
16         bool added = false;
17         foreach(el, offset1, offset2; ds.data) 
18         {
19             if (this.put(el, offset1))
20                 added = true;
21             if (this.put(el, offset2))
22                 added = true;
23         }
24         return added;
25     }
26 
27     bool put(string el, double offset = 0)
28     {
29         import ggplotd.algorithm : safeMin, safeMax;
30         if (el !in store)
31         {
32             store[el] = store.length;
33             offsets ~= tuple(offset, offset);
34             _min = safeMin(store.length - 1 + offset, _min);
35             _max = safeMax(store.length - 1 + offset, _max);
36             return true;
37         } else {
38             auto id = store[el];
39             offsets[id] = tuple(safeMin(offsets[id][0], offset),
40                 safeMax(offsets[id][1], offset));
41             _min = safeMin(id + offsets[id][0], _min);
42             _max = safeMax(id + offsets[id][1], _max);
43         }
44         return false;
45     }
46 
47     double min() const
48     {
49         if (store.length == 0)
50             return 0;
51         return _min;
52     }
53 
54     double max() const
55     {
56         if (store.length == 0)
57             return 0;
58         return _max;
59     }
60 
61     auto data() const
62     {
63         import std.array : array;
64         import std.algorithm : map, sort;
65         auto kv = store.byKeyValue().array;
66         auto sorted = kv.sort!((a, b) => a.value < b.value);
67         return sorted.map!((a) => tuple(a.key, offsets[a.value][0], offsets[a.value][1]));
68     }
69 
70     auto length() const
71     {
72         return offsets.length;
73     }
74 
75     double _min;
76     double _max;
77 }
78 
79 unittest
80 {
81     DiscreteStoreWithOffset ds;
82     assertEqual(ds.min(), 0);
83     assertEqual(ds.max(), 0);
84 
85     ds.put("b", 0.5);
86     assertEqual(ds.min(), 0.5);
87     assertEqual(ds.max(), 0.5);
88 
89     ds.put("a", 0.5);
90     assertEqual(ds.min(), 0.5);
91     assertEqual(ds.max(), 1.5);
92 
93     ds.put("b", -0.5);
94     assertEqual(ds.min(), -0.5);
95     assertEqual(ds.max(), 1.5);
96 
97     ds.put("c", -0.7);
98     assertEqual(ds.min(), -0.5);
99     assertEqual(ds.max(), 1.5);
100 
101     DiscreteStoreWithOffset ds2;
102     ds2.put("d", 0.5);
103     ds2.put("b", -1.0);
104     ds.put(ds2);
105     assertEqual(ds.min(), -1.0);
106     assertEqual(ds.max(), 3.5);
107 }
108 
109 /// Store values so we can later create guides from them
110 private struct GuideStore(string type = "")
111 {
112     import std.range : isInputRange;
113     /// Put another GuideStore into the store
114     void put(T)(in T gs)
115         if (is(T==GuideStore!(type))) 
116     {
117         _store.put(gs._store);
118 
119         import ggplotd.algorithm : safeMin, safeMax;
120         _min = safeMin(_min, gs._min);
121         _max = safeMax(_max, gs._max);
122     }
123 
124     /// Add a range of values to the store
125     void put(T)(in T range)
126         if (!is(T==string) && isInputRange!T)
127     {
128         foreach(t; range)
129             this.put(t);
130     }
131 
132     import std.traits : TemplateOf;
133     /// Add a value of anytype to the store
134     void put(T)(in T value, double offset = 0)
135         if (!is(T==GuideStore!(type)) &&
136             (is(T==string) || !isInputRange!T)
137         )
138     {
139         import std.conv : to;
140         import std.traits : isNumeric;
141         // For now we can just ignore colour I think
142         static if (isNumeric!T)
143         {
144             import ggplotd.algorithm : safeMin, safeMax;
145             _min = safeMin(_min, value.to!double + offset);
146             _max = safeMax(_max, value.to!double + offset);
147         } else {
148             static if (type == "colour")
149             {
150                 import ggplotd.colourspace : isColour;
151                 static if (!isColour!T) {
152                     static if (is(T==string)) {
153                         auto col = namedColour(value);
154                         if (col.isNull) 
155                         {
156                             _store.put(value, offset);
157                         }
158                     } else {
159                         _store.put(value.to!string, offset);
160                     }
161                 }
162             } else {
163                 _store.put(value.to!string, offset);
164             }
165         }
166     }
167 
168     /// Minimum value encountered till now
169     double min() const
170     {
171         import std.math : isNaN;
172         import ggplotd.algorithm : safeMin;
173         if (_store.length > 0 || isNaN(_min))
174             return safeMin(_store.min, _min);
175         return _min;
176     }
177 
178     /// Maximum value encountered till now
179     double max() const
180     {
181         import std.math : isNaN;
182         import ggplotd.algorithm : safeMax;
183         if (_store.length > 0 || isNaN(_max))
184             return safeMax(_store.max, _max);
185         return _max;
186     }
187 
188     /// The discete values in the store
189     @property auto store() const
190     {
191         import std.algorithm : map;
192         return _store.data.map!((a) => a[0]);
193     }
194 
195     /// A hash mapping the discrete values to continuous (double)
196     @property auto storeHash() const
197     {
198         import std.conv : to;
199         double[string] hash;
200         foreach(k, v; _store.store) 
201         {
202             hash[k] = v.to!double;
203         }
204         return hash;
205     }
206 
207     /// True if we encountered discrete values
208     bool hasDiscrete() const
209     {
210         return _store.length > 0;
211     }
212 
213     double _min;
214     double _max;
215 
216     DiscreteStoreWithOffset _store; // Should really only store uniques
217 
218     static if (type == "colour")
219     {
220         import ggplotd.colour : namedColour;
221     }
222 }
223 
224 unittest
225 {
226     import std.array : array;
227     import std.math : isNaN;
228     import std.range : walkLength;
229     // Not numeric -> add as string
230     GuideStore!"" gs;
231     gs.put("b");
232     gs.put("b");
233     assertEqual(gs.store.walkLength, 1);
234     gs.put("a");
235     assertEqual(gs.store.walkLength, 2);
236     gs.put("b");
237     assertEqual(gs.store.walkLength, 2);
238     assertEqual(gs.store.array, ["b", "a"]);
239     assertEqual(gs.storeHash, ["b":0.0, "a":1.0]);
240     assertEqual(gs.min, 0);
241     assertEqual(gs.max, 1);
242 
243     // Numeric -> add as min or max (also test int)
244     gs.put(-1);
245     assertEqual(gs.min, -1.0);
246     assertEqual(gs.max, 1.0);
247     gs.put(3.0);
248     assertEqual(gs.min, -1.0);
249     assertEqual(gs.max, 3.0);
250     gs.put(1.5);
251     assertEqual(gs.min, -1.0);
252     assertEqual(gs.max, 3.0);
253 
254     import ggplotd.colour: RGBA;
255     GuideStore!"colour" gsc;
256     // Test colour is ignored
257     gsc.put(RGBA(0, 0, 0, 0));
258     assertEqual(gsc.store.walkLength, 0);
259     // Test named colour is ignored
260     gsc.put("red");
261     assertEqual(gsc.store.walkLength, 0);
262     assertEqual(gsc.min, 0);
263     assertEqual(gsc.max, 0);
264     gsc.put("b");
265     assertEqual(gsc.store.walkLength, 1);
266 
267     // Colour not ignored for standard gc
268     gs.put(RGBA(0, 0, 0, 0));
269     assertEqual(gs.store.walkLength, 3);
270     // Test named colour is ignored
271     gs.put("red");
272     assertEqual(gs.store.walkLength, 4);
273 
274 
275     GuideStore!"" gs2;
276     gs2.put(2);
277     assertEqual(gs2.min, 2);
278     assertEqual(gs2.max, 2);
279 
280     GuideStore!"" gs3;
281     gs3.put(-2);
282     assertEqual(gs3.min, -2);
283     assertEqual(gs3.max, -2);
284 }
285 
286 unittest
287 {
288     GuideStore!"" gs;
289     gs.put(["a", "b", "a"]);
290     import std.array : array;
291     import std.range : walkLength;
292     assertEqual(gs.store.walkLength, 2);
293 
294     GuideStore!"" gs2;
295     gs2.put(["c", "b", "a"]);
296     gs.put(gs2);
297     assertEqual(gs.store.walkLength, 3);
298     assertEqual(gs.store.array, ["a","b","c"]);
299     gs2.put([10.1,-0.1]);
300     gs.put(gs2);
301     assertEqual(gs.min, -0.1);
302     assertEqual(gs.max, 10.1);
303 
304     GuideStore!"" gs3;
305     gs3.put(["a", "b", "a"]);
306     const(GuideStore!"") cst_gs() {
307         GuideStore!"" gs;
308         gs.put(["c", "b", "a"]);
309         return gs;
310     }
311     gs3.put(cst_gs());
312     assertEqual(gs3.store.walkLength, 3);
313     assertEqual(gs3.store.array, ["a","b","c"]);
314  
315 }
316 
317 unittest
318 {
319     GuideStore!"" gs;
320     gs.put("a", 0.5);
321     assertEqual(gs.min(), 0.5);
322     assertEqual(gs.max(), 0.5);
323 
324     GuideStore!"" gs2;
325     gs2.put("b", 0.7);
326     gs.put(gs2);
327     assertEqual(gs.min(), 0.5);
328     assertEqual(gs.max(), 1.7);
329 
330     GuideStore!"" gs3;
331     gs3.put("b", -0.7);
332     gs.put(gs3);
333     import std.math : approxEqual;
334     assert(approxEqual(gs.min(), 0.3));
335     assert(approxEqual(gs.max(), 1.7));
336 }
337 
338 /// A callable struct that translates any value into a double
339 struct GuideToDoubleFunction
340 {
341     /// Convert the value to double
342     auto convert(T)(in T value) const
343     {
344         import std.conv : to;
345         import std.traits : isNumeric;
346         static if (isNumeric!T) {
347             return doubleConvert(value.to!double);
348         } else {
349             return stringConvert(value.to!string);
350         }
351     }
352 
353     /// Call the function with a value
354     auto opCall(T)(in T value) const
355     {
356         return this.convert!T(value);
357     }
358 
359     /// Function that governs translation from double to double (continuous to continuous)
360     double delegate(double) doubleConvert;
361     /// Function that governs translation from string to double (discrete to continuous)
362     double delegate(string) stringConvert;
363 }
364 
365 /// A callable struct that translates any value into a colour
366 struct GuideToColourFunction
367 {
368     /// Call the function with a value
369     auto opCall(T)(in T value) const
370     {
371         import std.conv : to;
372         import std.traits : isNumeric;
373         static if (isNumeric!T) {
374             return doubleConvert(value.to!double);
375         } else {
376             static if (isColour!T) {
377                 import ggplotd.colourspace : RGBA, toColourSpace;
378                 return value.toColourSpace!RGBA;
379             } else {
380                 static if (is(T==string)) {
381                     auto col = namedColour(value);
382                     if (!col.isNull)
383                         return RGBA(col.r, col.g, col.b, 1);
384                     else
385                         return stringConvert(value);
386                 } else {
387                     return stringConvert(value.to!string);
388                 }
389             }
390         }
391     }
392 
393     auto toDouble(T)(in T value) const
394     {
395         import std.conv : to;
396         import std.traits : isNumeric;
397         if (isNumeric!T)
398             return value.to!double;
399         else
400             return stringToDoubleConvert(value.to!string);
401     }
402 
403     /// Function that governs translation from double to colour (continuous to colour)
404     RGBA delegate(double) doubleConvert;
405     /// Function that governs translation from string to colour (discrete to colour)
406     RGBA delegate(string) stringConvert;
407 
408     /// Function that governs translation from string to double (discrete to continuous)
409     double delegate(string) stringToDoubleConvert;
410     import ggplotd.colourspace : isColour;
411     import ggplotd.colour : namedColour, RGBA;
412 }
413 
414 /// Create an appropiate GuidToDoubleFunction from a GuideStore
415 auto guideFunction(string type)(GuideStore!type gs)
416     if (type != "colour")
417 {
418     GuideToDoubleFunction gf;
419     static if (type == "size") {
420         gf.doubleConvert = (a) {
421             import std.math : isNaN;
422             if (isNaN(a))
423                 return a;
424             assert(a >= gs.min() || a <= gs.max(), "Value falls outside of range");
425             if (gs.min() < 0.4 || gs.max() > 5.0) // Limit the size to between these values
426             {
427                 if (gs.max() == gs.min())
428                     return 1.0;
429                 return 0.7 + a*(5.0 - 0.7)/(gs.max() - gs.min());
430             }
431             return a;
432         };
433 
434     } else {
435         gf.doubleConvert = (a) {
436             import std.math : isNaN;
437             if (isNaN(a))
438                 return a;
439             assert(a >= gs.min() || a <= gs.max(), "Value falls outside of range");
440             return a;
441         };
442 
443     }
444     immutable storeHash = gs.storeHash;
445 
446     gf.stringConvert = (a) {
447         assert(a in storeHash, "Value not in guide");
448         return gf.doubleConvert(storeHash[a]);
449     };
450     return gf;
451 }
452 
453 unittest
454 {
455     GuideStore!"" gs;
456     gs.put(["b","a"]);
457     auto gf = guideFunction(gs);
458     assertEqual(gf(0.1), 0.1);
459     assertEqual(gf("a"), 1);
460 
461     import std.math : isNaN;
462     assert(isNaN(gf(double.init)));
463 }
464 
465 unittest
466 {
467     GuideStore!"size" gs;
468     gs.put( [0.5, 4] );
469     auto gf = guideFunction(gs);
470     assertEqual(gf(0.6), 0.6);
471 
472     gs.put( [0.0] );
473     auto gf2 = guideFunction(gs);
474     assertEqual(gf2(0.0), 0.7);
475     assertEqual(gf2(4.0), 5.0);
476 
477     GuideStore!"size" gs3;
478     gs3.put( [0.0] );
479     auto gf3 = guideFunction(gs3);
480     assertEqual(gf3(0.0), 1.0);
481 }
482 
483 import ggplotd.colour : ColourGradientFunction;
484 /// Create an appropiate GuidToColourFunction from a GuideStore
485 auto guideFunction(string type)(GuideStore!type gs, ColourGradientFunction colourFunction)
486     if (type == "colour")
487 {
488     GuideToColourFunction gc;
489     gc.doubleConvert = (a) {
490         import std.math : isNaN;
491         if (isNaN(a)) {
492             import ggplotd.colourspace : RGBA;
493             return RGBA(0,0,0,0);
494         }
495         assert(a >= gs.min() || a <= gs.max(), "Value falls outside of range");
496         return colourFunction(a, gs.min(), gs.max());
497     };
498 
499     immutable storeHash = gs.storeHash;
500 
501     gc.stringToDoubleConvert = (a) {
502         assert(a in storeHash, "Value not in storeHash");
503         return storeHash[a];
504     };
505 
506     gc.stringConvert = (a) {
507         assert(a in storeHash, "Value not in storeHash");
508         return gc.doubleConvert(gc.stringToDoubleConvert(a));
509     };
510     return gc;
511 }
512 
513 unittest
514 {
515     import ggplotd.colour : colourGradient, namedColour;
516     import ggplotd.colourspace : HCY, RGBA, toTuple;
517     GuideStore!"colour" gs;
518     gs.put([0.1, 3.0]);
519     auto gf = guideFunction(gs, colourGradient!HCY("blue-red"));
520     assertEqual(gf(0.1).toTuple, namedColour("blue").get().toTuple);
521     assertEqual(gf(3.0).toTuple, namedColour("red").get().toTuple);
522     assertEqual(gf("green").toTuple, namedColour("green").get().toTuple);
523     assertEqual(gf(namedColour("green").get()).toTuple, namedColour("green").get().toTuple);
524     assertEqual(gf(double.init).toTuple, RGBA(0,0,0,0).toTuple);
525 }