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 auto gradient(C)( double value, C from, C till ) 216 { 217 return hcyToRGB( 218 from[0] + value * (till[0]-from[0]), 219 from[1] + value * (till[1]-from[1]), 220 from[2] + value * (till[2]-from[2]) 221 ); 222 } 223 224 /// 225 auto gradient(double value, double from, double till) 226 { 227 if (from == till) 228 return hcyToRGB(200, 0.5, 0.5); 229 return gradient( (value-from)/(till-from), 230 Tuple!(double,double,double)(200, 0.5, 0), 231 Tuple!(double,double,double)(200, 1, 1) ); 232 } 233 234 private auto safeMax(T)(T a, T b) 235 { 236 import std.math : isNaN; 237 import std.algorithm : max; 238 239 if (isNaN(b)) 240 return a; 241 if (isNaN(a)) 242 return b; 243 return max(a, b); 244 } 245 246 private auto safeMin(T)(T a, T b) 247 { 248 import std.math : isNaN; 249 import std.algorithm : min; 250 251 if (isNaN(b)) 252 return a; 253 if (isNaN(a)) 254 return b; 255 return min(a, b); 256 } 257 258 alias ColourMap = RGBA delegate(ColourID tup); 259 260 /// 261 auto createColourMap(R)(R colourIDs) if (is(ElementType!R == Tuple!(double, 262 string)) || is(ElementType!R == ColourID)) 263 { 264 import std.algorithm : filter, map, reduce; 265 import std.math : isNaN; 266 import std.array : array; 267 import std.typecons : Tuple; 268 269 auto validatedIDs = ColourIDRange!R(colourIDs); 270 271 auto minmax = Tuple!(double, double)(0, 0); 272 if (!validatedIDs.empty) 273 minmax = validatedIDs.map!((a) => a[0]).reduce!((a, b) => safeMin(a, 274 b), (a, b) => safeMax(a, b)); 275 276 auto namedColours = createNamedColours; 277 278 return (ColourID tup) { 279 if (tup[2].red >= 0) 280 return tup[2]; 281 else if (tup[1] in namedColours) 282 return namedColours[tup[1]]; 283 else if (isNaN(tup[0])) 284 return gradient(validatedIDs.labelMap[tup[1]], minmax[0], minmax[1]); 285 return gradient(tup[0], minmax[0], minmax[1]); 286 }; 287 } 288 289 unittest 290 { 291 import std.typecons : Tuple; 292 import std.array : array; 293 import std.range : iota; 294 import std.algorithm : map; 295 296 assertFalse(createColourMap([ColourID("a"), 297 ColourID("b")])(ColourID("a")) == createColourMap([ColourID("a"), ColourID("b")])( 298 ColourID("b"))); 299 300 assertEqual(createColourMap([ColourID("a"), ColourID("b")])(ColourID("black")), 301 RGBA(0, 0, 0, 1)); 302 303 assertEqual(createColourMap([ColourID("black")])(ColourID("black")), RGBA(0, 0, 304 0, 1)); 305 306 auto cM = iota(0.0,8.0,1.0).map!((a) => ColourID(a)). 307 createColourMap(); 308 assert( cM( ColourID(0) ) != cM( ColourID(1) ) ); 309 assertEqual( cM( ColourID(0) ), cM( ColourID(0) ) ); 310 }