The OpenD Programming Language

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 package 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 : isClose;
334     assert(isClose(gs.min(), 0.3));
335     assert(isClose(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     private auto convert(T)(in T value, bool scale = true) const
343     {
344         import std.conv : to;
345         import std.traits : isNumeric;
346         double result;
347         static if (isNumeric!T) {
348             result = doubleConvert(value.to!double);
349         } else {
350             result = stringConvert(value.to!string);
351         }
352         return result;
353     }
354 
355     auto unscaled(T)(in T value) const
356     {
357         return this.convert!T(value, false);
358     }
359 
360     /// Call the function with a value
361     auto opCall(T)(in T value, bool scale = true) const
362     {
363         auto result = unscaled!T(value);
364         if (scaleFunction.isNull || !scale)
365             return result;
366         else
367             return scaleFunction.get()(result);
368     }
369 
370     /// Function that governs translation from double to double (continuous to continuous)
371     double delegate(double) doubleConvert;
372     /// Function that governs translation from string to double (discrete to continuous)
373     double delegate(string) stringConvert;
374 
375     import std.typecons : Nullable;
376     /// Additional scaling of the field (i.e. log10, polar coordinates)
377     Nullable!(double delegate(double)) scaleFunction;
378 }
379 
380 /// A callable struct that translates any value into a colour
381 struct GuideToColourFunction
382 {
383     /// Call the function with a value
384     auto opCall(T)(in T value, bool scale = true) const
385     {
386         import std.conv : to;
387         import std.traits : isNumeric;
388         static if (isNumeric!T) {
389             return doubleConvert(toDouble(value));
390         } else {
391             static if (isColour!T) {
392                 import ggplotd.colourspace : RGBA, toColourSpace;
393                 return value.toColourSpace!RGBA;
394             } else {
395                 static if (is(T==string)) {
396                     auto col = namedColour(value);
397                     if (!col.isNull)
398                         return RGBA(col.get().r, col.get().g, col.get().b, 1);
399                     else
400                         return stringConvert(value);
401                 } else {
402                     return stringConvert(value.to!string);
403                 }
404             }
405         }
406     }
407 
408     auto toDouble(T)(in T value, bool scale = true) const
409     {
410         import std.conv : to;
411         import std.traits : isNumeric;
412         double result = this.unscaled(value);
413         if (scaleFunction.isNull || !scale)
414             return result;
415         else
416             return scaleFunction.get()(result);
417     }
418 
419     auto unscaled(T)(in T value) const
420     {
421         import std.conv : to;
422         import std.traits : isNumeric;
423         double result;
424         static if (isNumeric!T)
425             result = value.to!double;
426         else
427             result = stringToDoubleConvert(value.to!string);
428         return result;
429     }
430 
431     /// Function that governs translation from double to colour (continuous to colour)
432     RGBA delegate(double) doubleConvert;
433     /// Function that governs translation from string to colour (discrete to colour)
434     RGBA delegate(string) stringConvert;
435 
436     /// Function that governs translation from string to double (discrete to continuous)
437     double delegate(string) stringToDoubleConvert;
438     import ggplotd.colourspace : isColour;
439     import ggplotd.colour : namedColour, RGBA;
440 
441     import std.typecons : Nullable;
442     /// Additional scaling of the field (i.e. log10, polar coordinates)
443     Nullable!(double delegate(double)) scaleFunction;
444 }
445 
446 /// Create an appropiate GuidToDoubleFunction from a GuideStore
447 auto guideFunction(string type)(GuideStore!type gs)
448     if (type != "colour")
449 {
450     GuideToDoubleFunction gf;
451     static if (type == "size") {
452         gf.doubleConvert = (a) {
453             import std.math : isNaN;
454             if (isNaN(a))
455                 return a;
456             assert(a >= gs.min() || a <= gs.max(), "Value falls outside of range");
457             if (gs.min() < 0.4 || gs.max() > 5.0) // Limit the size to between these values
458             {
459                 if (gs.max() == gs.min())
460                     return 1.0;
461                 return 0.7 + a*(5.0 - 0.7)/(gs.max() - gs.min());
462             }
463             return a;
464         };
465 
466     } else {
467         gf.doubleConvert = (a) {
468             import std.math : isNaN;
469             if (isNaN(a))
470                 return a;
471             assert(a >= gs.min() || a <= gs.max(), "Value falls outside of range");
472             return a;
473         };
474 
475     }
476     immutable storeHash = gs.storeHash;
477 
478     gf.stringConvert = (a) {
479         assert(a in storeHash, "Value not in guide");
480         return gf.doubleConvert(storeHash[a]);
481     };
482     return gf;
483 }
484 
485 unittest
486 {
487     GuideStore!"" gs;
488     gs.put(["b","a"]);
489     auto gf = guideFunction(gs);
490     assertEqual(gf(0.1), 0.1);
491     assertEqual(gf("a"), 1);
492 
493     import std.math : isNaN;
494     assert(isNaN(gf(double.init)));
495 }
496 
497 unittest
498 {
499     GuideStore!"size" gs;
500     gs.put( [0.5, 4] );
501     auto gf = guideFunction(gs);
502     assertEqual(gf(0.6), 0.6);
503 
504     gs.put( [0.0] );
505     auto gf2 = guideFunction(gs);
506     assertEqual(gf2(0.0), 0.7);
507     assertEqual(gf2(4.0), 5.0);
508 
509     GuideStore!"size" gs3;
510     gs3.put( [0.0] );
511     auto gf3 = guideFunction(gs3);
512     assertEqual(gf3(0.0), 1.0);
513 }
514 
515 import ggplotd.colour : ColourGradientFunction;
516 /// Create an appropiate GuidToColourFunction from a GuideStore
517 auto guideFunction(string type)(GuideStore!type gs, ColourGradientFunction colourFunction)
518     if (type == "colour")
519 {
520     GuideToColourFunction gc;
521     gc.doubleConvert = (a) {
522         import std.math : isNaN;
523         if (isNaN(a)) {
524             import ggplotd.colourspace : RGBA;
525             return RGBA(0,0,0,0);
526         }
527         assert(a >= gs.min() || a <= gs.max(), "Value falls outside of range");
528         return colourFunction(a, gs.min(), gs.max());
529     };
530 
531     immutable storeHash = gs.storeHash;
532 
533     gc.stringToDoubleConvert = (a) {
534         assert(a in storeHash, "Value not in storeHash");
535         return storeHash[a];
536     };
537 
538     gc.stringConvert = (a) {
539         assert(a in storeHash, "Value not in storeHash");
540         return gc.doubleConvert(gc.stringToDoubleConvert(a));
541     };
542     return gc;
543 }
544 
545 unittest
546 {
547     import ggplotd.colour : colourGradient, namedColour;
548     import ggplotd.colourspace : HCY, RGBA, toTuple;
549     GuideStore!"colour" gs;
550     gs.put([0.1, 3.0]);
551     auto gf = guideFunction(gs, colourGradient!HCY("blue-red"));
552     assertEqual(gf(0.1).toTuple, namedColour("blue").get().toTuple);
553     assertEqual(gf(3.0).toTuple, namedColour("red").get().toTuple);
554     assertEqual(gf("green").toTuple, namedColour("green").get().toTuple);
555     assertEqual(gf(namedColour("green").get()).toTuple, namedColour("green").get().toTuple);
556     assertEqual(gf(double.init).toTuple, RGBA(0,0,0,0).toTuple);
557 }