1 /** 2 Deal with colour related struct/functions, such as ColourSpaces etc. 3 */ 4 module ggplotd.colour; 5 6 import std.range : ElementType; 7 import std.typecons : Tuple; 8 9 import ggplotd.colourspace : RGBA; 10 11 version (unittest) 12 { 13 import dunit.toolkit; 14 } 15 16 /** 17 Convert a name (string) into a Nullable!colour. 18 19 Internally calls std.experimental.colorFromString and names supported are therefore exactly the same. This inludes all the colors defined by X11, adopted by the W3C, SVG, and other popular libraries. 20 */ 21 auto namedColour(in string name) 22 { 23 import std.experimental.color : colorFromString; 24 import ggplotd.colourspace : toColourSpace, RGB; 25 import std.typecons : Nullable; 26 Nullable!RGB colour; 27 try 28 { 29 colour = colorFromString(name).toColourSpace!RGB; 30 } catch {} 31 return colour; 32 } 33 34 unittest 35 { 36 import std.typecons : tuple; 37 import ggplotd.colourspace : toTuple; 38 auto col = namedColour("red"); 39 assert(!col.isNull); 40 assertEqual(col.toTuple, tuple(1,0,0)); 41 42 auto col2 = namedColour("this colour does not exist you idiot"); 43 assert(col2.isNull); 44 } 45 46 struct ColourGradient(C) 47 { 48 import ggplotd.colourspace : toColourSpace; 49 void put( in double value, in C colour) 50 { 51 import std.range : back, empty; 52 if (!stops.data.empty) 53 assert( value > stops.data.back[0], 54 "Stops must be added in increasing value" ); 55 stops.put( Tuple!(double, C)( value, colour ) ); 56 } 57 58 void put( in double value, string name ) 59 { 60 auto rgb = namedColour(name); 61 assert(!rgb.isNull, "Unknown colour name"); 62 auto colour = toColourSpace!C(rgb.get()); 63 this.put( value, colour ); 64 } 65 66 /** 67 To find the interval within which a value falls 68 69 If value to high or low return respectively the highest two or lowest two 70 */ 71 auto interval( in double value ) const 72 { 73 import std.algorithm : findSplitBefore; 74 import std.range : empty, front, back; 75 assert(stops.data.length > 1, "Need at least two stops"); 76 // Split stops around it. If one empty take two from other and warn value 77 // outside of coverage (debug), else take back and front from splitted 78 auto splitted = stops.data.findSplitBefore!"a[0]>b"([value]); 79 80 if (splitted[0].empty) 81 return stops.data[0..2]; 82 else if (splitted[1].empty) 83 return stops.data[($-2)..$]; 84 return [splitted[0].back, splitted[1].front]; 85 } 86 87 /** 88 Get the colour associated with passed value 89 */ 90 auto colour( in double value ) const 91 { 92 import ggplotd.colourspace : toTuple; 93 // When returning colour by value, try zip(c1, c2).map!( (a,b) => a+v*(b-a)) or something 94 auto inval = interval( value ); 95 auto sc = (value-inval[0][0])/(inval[1][0]-inval[0][0]); 96 auto minC = inval[0][1].toTuple; 97 auto maxC = inval[1][1].toTuple; 98 return ( C( 99 minC[0] + sc*(maxC[0]-minC[0]), 100 minC[1] + sc*(maxC[1]-minC[1]), 101 minC[2] + sc*(maxC[2]-minC[2]) ) ); 102 } 103 104 private: 105 import std.range : Appender; 106 Appender!(Tuple!(double,C)[]) stops; 107 } 108 109 unittest 110 { 111 import ggplotd.colourspace : RGB; 112 import std.range : back, front; 113 114 ColourGradient!RGB cg; 115 116 cg.put( 0, RGB(0,0,0) ); 117 cg.put( 1, "white" ); 118 119 auto ans = cg.interval( 0.1 ); 120 assertEqual( ans.front[0], 0 ); 121 assertEqual( ans.back[0], 1 ); 122 123 cg = ColourGradient!RGB(); 124 125 cg.put( 0, RGB(0,0,0) ); 126 cg.put( 0.2, RGB(0.5,0.6,0.8) ); 127 cg.put( 1, "white" ); 128 ans = cg.interval( 0.1 ); 129 assertEqual( ans.front[0], 0 ); 130 assertEqual( ans.back[0], 0.2 ); 131 132 ans = cg.interval( 1.1 ); 133 assertEqual( ans.front[0], 0.2 ); 134 assertEqual( ans.back[0], 1.0 ); 135 136 auto col = cg.colour( 0.1 ); 137 assertEqual( col, RGB(0.25,0.3,0.4) ); 138 } 139 140 /// 141 alias ColourGradientFunction = RGBA delegate( double value, double from, double till ); 142 143 /** 144 Function returning a colourgradient function based on a specified ColourGradient 145 146 Params: 147 cg = A ColourGradient 148 absolute = Whether the cg is an absolute scale or relative (between 0 and 1) 149 150 Examples: 151 ----------------- 152 auto cg = ColourGradient!HCY(); 153 cg.put( 0, HCY(200, 0.5, 0) ); 154 cg.put( 100, HCY(200, 0.5, 0) ); 155 GGPlotD().put( colourGradient( cg ) ); 156 ----------------- 157 */ 158 ColourGradientFunction colourGradient(T)( in ColourGradient!T cg, 159 bool absolute = false ) 160 { 161 if (absolute) { 162 return ( double value, double from, double till ) 163 { 164 import ggplotd.colourspace : RGBA, toColourSpace; 165 return cg.colour( value ).toColourSpace!RGBA; 166 }; 167 } 168 return ( double value, double from, double till ) 169 { 170 import ggplotd.colourspace : RGBA, toColourSpace; 171 return cg.colour( (value-from)/(till-from) ).toColourSpace!RGBA; 172 }; 173 } 174 175 /** 176 Function returning a named colourgradient. 177 178 Colours can be specified with colour names separated by dashes: 179 "white-red" will result in a colourgradient from white to red. You can specify more than two colours "blue-white-red". "default" will result in the default (blueish) colourgradient. 180 181 Examples: 182 ----------------- 183 GGPlotD().put( colourGradient( "blue-red" ); 184 ----------------- 185 */ 186 ColourGradientFunction colourGradient(T)( string name ) 187 { 188 import std.algorithm : splitter; 189 import std.range : empty, walkLength; 190 if ( !name.empty && name != "default" ) 191 { 192 import ggplotd.colourspace : toColourSpace; 193 auto cg = ColourGradient!T(); 194 auto splitted = name.splitter("-"); 195 immutable dim = splitted.walkLength; 196 if (dim == 1) 197 { 198 auto col = namedColour(splitted.front); 199 assert(!col.isNull, "Unknown named colour"); 200 auto c = col.get().toColourSpace!T; 201 cg.put(0, c ); 202 cg.put(1, c ); 203 } 204 if (dim > 1) 205 { 206 auto value = 0.0; 207 immutable width = 1.0/(dim-1); 208 foreach(sp ; splitted) 209 { 210 auto col = namedColour(sp); 211 assert(!col.isNull, "Unknown named colour"); 212 cg.put(value, col.get().toColourSpace!T); 213 value += width; 214 } 215 } 216 return colourGradient(cg, false); 217 } 218 import std.math : PI; 219 import ggplotd.colourspace : HCY; 220 auto cg = ColourGradient!HCY(); 221 cg.put( 0, HCY(200/360.0, 0.5, 0.1)); 222 cg.put( 1, HCY(200/360.0, 0.5, 0.8)); 223 return colourGradient(cg, false); 224 } 225 226 unittest 227 { 228 import ggplotd.colourspace : HCY; 229 auto cf = colourGradient!HCY( "red-white-blue" ); 230 assertEqual( cf( -1, -1, 2 ).r, 1 ); 231 assertEqual( cf( -1, -1, 2 ).g, 0 ); 232 assertEqual( cf( 2, -1, 2 ).b, 1 ); 233 assertLessThan( cf( 2, -1, 2 ).g, 1e-5 ); 234 assertEqual( cf( 0.5, -1, 2 ).b, 1 ); 235 assertEqual( cf( 0.5, -1, 2 ).g, 1 ); 236 }