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 public 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 (Throwable) {}
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 }