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