1 module ggplotd.colour;
2 
3 import std.range : ElementType;
4 import std.typecons : Tuple;
5 
6 import cairo.cairo : RGB;
7 
8 //import std.experimental.color.conv;
9 //import std.experimental.color.rgb;
10 //import std.experimental.color.hsx;
11 
12 import ggplotd.aes : NumericLabel;
13 
14 version (unittest)
15 {
16     import dunit.toolkit;
17 }
18 
19 /++
20 HCY to RGB
21 
22 H(ue) 0-360, C(hroma) 0-1, Y(Luma) 0-1
23 +/
24 RGB hcyToRGB(double h, double c, double y)
25 {
26     import std.math : abs;
27 
28     auto ha = h / 60;
29     auto x = c * (1 - abs(ha % 2 - 1));
30     Tuple!(double, double, double) rgb1;
31     if (ha == 0)
32         rgb1 = Tuple!(double, double, double)(0, 0, 0);
33     else if (ha < 1)
34         rgb1 = Tuple!(double, double, double)(c, x, 0);
35     else if (ha < 2)
36         rgb1 = Tuple!(double, double, double)(x, c, 0);
37     else if (ha < 3)
38         rgb1 = Tuple!(double, double, double)(0, c, x);
39     else if (ha < 4)
40         rgb1 = Tuple!(double, double, double)(0, x, c);
41     else if (ha < 5)
42         rgb1 = Tuple!(double, double, double)(x, 0, c);
43     else if (ha < 6)
44         rgb1 = Tuple!(double, double, double)(c, 0, x);
45     auto m = y - (.3 * rgb1[0] + .59 * rgb1[1] + .11 * rgb1[2]);
46     return RGB(rgb1[0] + m, rgb1[1] + m, rgb1[2] + m);
47 }
48 
49 /++
50     Returns an associative array with names as key and colours as values
51 
52     Would have been nicer to just define a static AA, but that is currently
53     not possible.
54     +/
55 auto createNamedColours()
56 {
57     RGB[string] nameMap;
58     nameMap["black"] = RGB(0, 0, 0);
59     nameMap["white"] = RGB(1, 1, 1);
60     nameMap["red"] = RGB(1, 0, 0);
61     nameMap["green"] = RGB(0, 1, 0);
62     nameMap["red"] = RGB(0, 0, 1);
63     return nameMap;
64 }
65 
66 /// Converts any type into a double string pair, which is used by colour maps
67 struct ColourID
68 {
69     import std.typecons : Tuple;
70 
71     ///
72     this(T)(in T setId)
73     {
74         import std.math : isNumeric;
75         import std.conv : to;
76 
77         static if (isNumeric!T)
78         {
79             id[0] = setId.to!double;
80         }
81         else
82             id[1] = setId.to!string;
83     }
84 
85     Tuple!(double, string) id; ///
86 
87     alias id this; ///
88 }
89 
90 unittest
91 {
92     import std.math : isNaN;
93     import std.range : empty;
94 
95     auto cID = ColourID("a");
96     assert(isNaN(cID[0]));
97     assertEqual(cID[1], "a");
98     auto numID = ColourID(0);
99     assertEqual(numID[0], 0);
100     assert(numID[1].empty);
101 }
102 
103 import std.range : isInputRange;
104 import std.range : ElementType;
105 
106 ///
107 struct ColourIDRange(T) if (isInputRange!T && is(ElementType!T == ColourID))
108 {
109     ///
110     this(T range)
111     {
112         original = range;
113         namedColours = createNamedColours();
114     }
115 
116     ///
117     @property auto front()
118     {
119         import std.range : front;
120         import std.math : isNaN;
121 
122         if (!isNaN(original.front[0]) || original.front[1] in namedColours)
123             return original.front;
124         else if (original.front[1] !in labelMap)
125         {
126             import std.conv : to;
127 
128             labelMap[original.front[1]] = labelMap.length.to!double;
129         }
130         original.front[0] = labelMap[original.front[1]];
131         return original.front;
132     }
133 
134     ///
135     void popFront()
136     {
137         import std.range : popFront;
138 
139         original.popFront;
140     }
141 
142     ///
143     @property bool empty()
144     {
145         import std.range : empty;
146 
147         return original.empty;
148     }
149 
150     // TODO More elegant way of doing this? Key is that we want to keep
151     // labelMap after our we've iterated over this array.
152     // One possible solution would be to have a fillLabelMap, which will
153     // run till the end of original and fill the LabelMap
154     static double[string] labelMap;
155 
156 private:
157     T original;
158     //E[double] toLabelMap;
159     RGB[string] namedColours;
160 }
161 
162 unittest
163 {
164     import std.math : isNaN;
165 
166     auto ids = [ColourID("black"), ColourID(-1), ColourID("a"), ColourID("b"), ColourID("a")];
167     auto cids = ColourIDRange!(typeof(ids))(ids);
168 
169     assertEqual(cids.front[1], "black");
170     assert(isNaN(cids.front[0]));
171     cids.popFront;
172     assertEqual(cids.front[1], "");
173     assertEqual(cids.front[0], -1);
174     cids.popFront;
175     assertEqual(cids.front[1], "a");
176     assertEqual(cids.front[0], 0);
177     cids.popFront;
178     assertEqual(cids.front[1], "b");
179     assertEqual(cids.front[0], 1);
180     cids.popFront;
181     assertEqual(cids.front[1], "a");
182     assertEqual(cids.front[0], 0);
183 }
184 
185 ///
186 auto gradient(double value, double from, double till)
187 {
188     if (from == till)
189         return hcyToRGB(200, 0.5, 0.5);
190     return hcyToRGB(200, 0.5 + 0.5 * (value - from) / (till - from), (value - from) / (till - from));
191 }
192 
193 private auto safeMax(T)(T a, T b)
194 {
195     import std.math : isNaN;
196     import std.algorithm : max;
197 
198     if (isNaN(b))
199         return a;
200     if (isNaN(a))
201         return b;
202     return max(a, b);
203 }
204 
205 private auto safeMin(T)(T a, T b)
206 {
207     import std.math : isNaN;
208     import std.algorithm : min;
209 
210     if (isNaN(b))
211         return a;
212     if (isNaN(a))
213         return b;
214     return min(a, b);
215 }
216 
217 alias ColourMap = RGB delegate(ColourID tup);
218 
219 ///
220 auto createColourMap(R)(R colourIDs) if (is(ElementType!R == Tuple!(double,
221         string)) || is(ElementType!R == ColourID))
222 {
223     import std.algorithm : filter, map, reduce;
224     import std.math : isNaN;
225     import std.array : array;
226     import std.typecons : Tuple;
227 
228     auto validatedIDs = ColourIDRange!R(colourIDs);
229 
230     auto minmax = validatedIDs.map!((a) => a[0]).reduce!((a, b) => safeMin(a,
231         b), (a, b) => safeMax(a, b));
232 
233     auto namedColours = createNamedColours;
234     //RGB!("rgba", float)
235     return (ColourID tup) {
236         if (tup[1] in namedColours)
237             return namedColours[tup[1]];
238         else if (isNaN(tup[0]))
239             return gradient(validatedIDs.labelMap[tup[1]], minmax[0], minmax[1]);
240         return gradient(tup[0], minmax[0], minmax[1]);
241     };
242 }
243 
244 unittest
245 {
246     import std.typecons : Tuple;
247 
248     assertFalse(createColourMap([ColourID("a"),
249         ColourID("b")])(ColourID("a")) == createColourMap([ColourID("a"), ColourID("b")])(
250         ColourID("b")));
251 
252     assertEqual(createColourMap([ColourID("a"), ColourID("b")])(ColourID("black")),
253         RGB(0, 0, 0));
254 
255     assertEqual(createColourMap([ColourID("black")])(ColourID("black")), RGB(0, 0,
256         0));
257 }