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 }