1 module ggplotd.guide; 2 3 version (unittest) 4 { 5 import dunit.toolkit; 6 } 7 8 private struct DiscreteStoreWithOffset 9 { 10 import std.typecons : Tuple, tuple; 11 size_t[string] store; 12 Tuple!(double, double)[] offsets; 13 14 bool put(in DiscreteStoreWithOffset ds) 15 { 16 bool added = false; 17 foreach(el, offset1, offset2; ds.data) 18 { 19 if (this.put(el, offset1)) 20 added = true; 21 if (this.put(el, offset2)) 22 added = true; 23 } 24 return added; 25 } 26 27 bool put(string el, double offset = 0) 28 { 29 import ggplotd.algorithm : safeMin, safeMax; 30 if (el !in store) 31 { 32 store[el] = store.length; 33 offsets ~= tuple(offset, offset); 34 _min = safeMin(store.length - 1 + offset, _min); 35 _max = safeMax(store.length - 1 + offset, _max); 36 return true; 37 } else { 38 auto id = store[el]; 39 offsets[id] = tuple(safeMin(offsets[id][0], offset), 40 safeMax(offsets[id][1], offset)); 41 _min = safeMin(id + offsets[id][0], _min); 42 _max = safeMax(id + offsets[id][1], _max); 43 } 44 return false; 45 } 46 47 double min() const 48 { 49 if (store.length == 0) 50 return 0; 51 return _min; 52 } 53 54 double max() const 55 { 56 if (store.length == 0) 57 return 0; 58 return _max; 59 } 60 61 auto data() const 62 { 63 import std.array : array; 64 import std.algorithm : map, sort; 65 auto kv = store.byKeyValue().array; 66 auto sorted = kv.sort!((a, b) => a.value < b.value); 67 return sorted.map!((a) => tuple(a.key, offsets[a.value][0], offsets[a.value][1])); 68 } 69 70 auto length() const 71 { 72 return offsets.length; 73 } 74 75 double _min; 76 double _max; 77 } 78 79 unittest 80 { 81 DiscreteStoreWithOffset ds; 82 assertEqual(ds.min(), 0); 83 assertEqual(ds.max(), 0); 84 85 ds.put("b", 0.5); 86 assertEqual(ds.min(), 0.5); 87 assertEqual(ds.max(), 0.5); 88 89 ds.put("a", 0.5); 90 assertEqual(ds.min(), 0.5); 91 assertEqual(ds.max(), 1.5); 92 93 ds.put("b", -0.5); 94 assertEqual(ds.min(), -0.5); 95 assertEqual(ds.max(), 1.5); 96 97 ds.put("c", -0.7); 98 assertEqual(ds.min(), -0.5); 99 assertEqual(ds.max(), 1.5); 100 101 DiscreteStoreWithOffset ds2; 102 ds2.put("d", 0.5); 103 ds2.put("b", -1.0); 104 ds.put(ds2); 105 assertEqual(ds.min(), -1.0); 106 assertEqual(ds.max(), 3.5); 107 } 108 109 /// Store values so we can later create guides from them 110 private struct GuideStore(string type = "") 111 { 112 import std.range : isInputRange; 113 /// Put another GuideStore into the store 114 void put(T)(in T gs) 115 if (is(T==GuideStore!(type))) 116 { 117 _store.put(gs._store); 118 119 import ggplotd.algorithm : safeMin, safeMax; 120 _min = safeMin(_min, gs._min); 121 _max = safeMax(_max, gs._max); 122 } 123 124 /// Add a range of values to the store 125 void put(T)(in T range) 126 if (!is(T==string) && isInputRange!T) 127 { 128 foreach(t; range) 129 this.put(t); 130 } 131 132 import std.traits : TemplateOf; 133 /// Add a value of anytype to the store 134 void put(T)(in T value, double offset = 0) 135 if (!is(T==GuideStore!(type)) && 136 (is(T==string) || !isInputRange!T) 137 ) 138 { 139 import std.conv : to; 140 import std.traits : isNumeric; 141 // For now we can just ignore colour I think 142 static if (isNumeric!T) 143 { 144 import ggplotd.algorithm : safeMin, safeMax; 145 _min = safeMin(_min, value.to!double + offset); 146 _max = safeMax(_max, value.to!double + offset); 147 } else { 148 static if (type == "colour") 149 { 150 import ggplotd.colourspace : isColour; 151 static if (!isColour!T) { 152 static if (is(T==string)) { 153 auto col = namedColour(value); 154 if (col.isNull) 155 { 156 _store.put(value, offset); 157 } 158 } else { 159 _store.put(value.to!string, offset); 160 } 161 } 162 } else { 163 _store.put(value.to!string, offset); 164 } 165 } 166 } 167 168 /// Minimum value encountered till now 169 double min() const 170 { 171 import std.math : isNaN; 172 import ggplotd.algorithm : safeMin; 173 if (_store.length > 0 || isNaN(_min)) 174 return safeMin(_store.min, _min); 175 return _min; 176 } 177 178 /// Maximum value encountered till now 179 double max() const 180 { 181 import std.math : isNaN; 182 import ggplotd.algorithm : safeMax; 183 if (_store.length > 0 || isNaN(_max)) 184 return safeMax(_store.max, _max); 185 return _max; 186 } 187 188 /// The discete values in the store 189 @property auto store() const 190 { 191 import std.algorithm : map; 192 return _store.data.map!((a) => a[0]); 193 } 194 195 /// A hash mapping the discrete values to continuous (double) 196 @property auto storeHash() const 197 { 198 import std.conv : to; 199 double[string] hash; 200 foreach(k, v; _store.store) 201 { 202 hash[k] = v.to!double; 203 } 204 return hash; 205 } 206 207 /// True if we encountered discrete values 208 bool hasDiscrete() const 209 { 210 return _store.length > 0; 211 } 212 213 double _min; 214 double _max; 215 216 DiscreteStoreWithOffset _store; // Should really only store uniques 217 218 static if (type == "colour") 219 { 220 import ggplotd.colour : namedColour; 221 } 222 } 223 224 unittest 225 { 226 import std.array : array; 227 import std.math : isNaN; 228 import std.range : walkLength; 229 // Not numeric -> add as string 230 GuideStore!"" gs; 231 gs.put("b"); 232 gs.put("b"); 233 assertEqual(gs.store.walkLength, 1); 234 gs.put("a"); 235 assertEqual(gs.store.walkLength, 2); 236 gs.put("b"); 237 assertEqual(gs.store.walkLength, 2); 238 assertEqual(gs.store.array, ["b", "a"]); 239 assertEqual(gs.storeHash, ["b":0.0, "a":1.0]); 240 assertEqual(gs.min, 0); 241 assertEqual(gs.max, 1); 242 243 // Numeric -> add as min or max (also test int) 244 gs.put(-1); 245 assertEqual(gs.min, -1.0); 246 assertEqual(gs.max, 1.0); 247 gs.put(3.0); 248 assertEqual(gs.min, -1.0); 249 assertEqual(gs.max, 3.0); 250 gs.put(1.5); 251 assertEqual(gs.min, -1.0); 252 assertEqual(gs.max, 3.0); 253 254 import ggplotd.colour: RGBA; 255 GuideStore!"colour" gsc; 256 // Test colour is ignored 257 gsc.put(RGBA(0, 0, 0, 0)); 258 assertEqual(gsc.store.walkLength, 0); 259 // Test named colour is ignored 260 gsc.put("red"); 261 assertEqual(gsc.store.walkLength, 0); 262 assertEqual(gsc.min, 0); 263 assertEqual(gsc.max, 0); 264 gsc.put("b"); 265 assertEqual(gsc.store.walkLength, 1); 266 267 // Colour not ignored for standard gc 268 gs.put(RGBA(0, 0, 0, 0)); 269 assertEqual(gs.store.walkLength, 3); 270 // Test named colour is ignored 271 gs.put("red"); 272 assertEqual(gs.store.walkLength, 4); 273 274 275 GuideStore!"" gs2; 276 gs2.put(2); 277 assertEqual(gs2.min, 2); 278 assertEqual(gs2.max, 2); 279 280 GuideStore!"" gs3; 281 gs3.put(-2); 282 assertEqual(gs3.min, -2); 283 assertEqual(gs3.max, -2); 284 } 285 286 unittest 287 { 288 GuideStore!"" gs; 289 gs.put(["a", "b", "a"]); 290 import std.array : array; 291 import std.range : walkLength; 292 assertEqual(gs.store.walkLength, 2); 293 294 GuideStore!"" gs2; 295 gs2.put(["c", "b", "a"]); 296 gs.put(gs2); 297 assertEqual(gs.store.walkLength, 3); 298 assertEqual(gs.store.array, ["a","b","c"]); 299 gs2.put([10.1,-0.1]); 300 gs.put(gs2); 301 assertEqual(gs.min, -0.1); 302 assertEqual(gs.max, 10.1); 303 304 GuideStore!"" gs3; 305 gs3.put(["a", "b", "a"]); 306 const(GuideStore!"") cst_gs() { 307 GuideStore!"" gs; 308 gs.put(["c", "b", "a"]); 309 return gs; 310 } 311 gs3.put(cst_gs()); 312 assertEqual(gs3.store.walkLength, 3); 313 assertEqual(gs3.store.array, ["a","b","c"]); 314 315 } 316 317 unittest 318 { 319 GuideStore!"" gs; 320 gs.put("a", 0.5); 321 assertEqual(gs.min(), 0.5); 322 assertEqual(gs.max(), 0.5); 323 324 GuideStore!"" gs2; 325 gs2.put("b", 0.7); 326 gs.put(gs2); 327 assertEqual(gs.min(), 0.5); 328 assertEqual(gs.max(), 1.7); 329 330 GuideStore!"" gs3; 331 gs3.put("b", -0.7); 332 gs.put(gs3); 333 import std.math : approxEqual; 334 assert(approxEqual(gs.min(), 0.3)); 335 assert(approxEqual(gs.max(), 1.7)); 336 } 337 338 /// A callable struct that translates any value into a double 339 struct GuideToDoubleFunction 340 { 341 /// Convert the value to double 342 auto convert(T)(in T value) const 343 { 344 import std.conv : to; 345 import std.traits : isNumeric; 346 static if (isNumeric!T) { 347 return doubleConvert(value.to!double); 348 } else { 349 return stringConvert(value.to!string); 350 } 351 } 352 353 /// Call the function with a value 354 auto opCall(T)(in T value) const 355 { 356 return this.convert!T(value); 357 } 358 359 /// Function that governs translation from double to double (continuous to continuous) 360 double delegate(double) doubleConvert; 361 /// Function that governs translation from string to double (discrete to continuous) 362 double delegate(string) stringConvert; 363 } 364 365 /// A callable struct that translates any value into a colour 366 struct GuideToColourFunction 367 { 368 /// Call the function with a value 369 auto opCall(T)(in T value) const 370 { 371 import std.conv : to; 372 import std.traits : isNumeric; 373 static if (isNumeric!T) { 374 return doubleConvert(value.to!double); 375 } else { 376 static if (isColour!T) { 377 import ggplotd.colourspace : RGBA, toColourSpace; 378 return value.toColourSpace!RGBA; 379 } else { 380 static if (is(T==string)) { 381 auto col = namedColour(value); 382 if (!col.isNull) 383 return RGBA(col.r, col.g, col.b, 1); 384 else 385 return stringConvert(value); 386 } else { 387 return stringConvert(value.to!string); 388 } 389 } 390 } 391 } 392 393 auto toDouble(T)(in T value) const 394 { 395 import std.conv : to; 396 import std.traits : isNumeric; 397 if (isNumeric!T) 398 return value.to!double; 399 else 400 return stringToDoubleConvert(value.to!string); 401 } 402 403 /// Function that governs translation from double to colour (continuous to colour) 404 RGBA delegate(double) doubleConvert; 405 /// Function that governs translation from string to colour (discrete to colour) 406 RGBA delegate(string) stringConvert; 407 408 /// Function that governs translation from string to double (discrete to continuous) 409 double delegate(string) stringToDoubleConvert; 410 import ggplotd.colourspace : isColour; 411 import ggplotd.colour : namedColour, RGBA; 412 } 413 414 /// Create an appropiate GuidToDoubleFunction from a GuideStore 415 auto guideFunction(string type)(GuideStore!type gs) 416 if (type != "colour") 417 { 418 GuideToDoubleFunction gf; 419 static if (type == "size") { 420 gf.doubleConvert = (a) { 421 import std.math : isNaN; 422 if (isNaN(a)) 423 return a; 424 assert(a >= gs.min() || a <= gs.max(), "Value falls outside of range"); 425 if (gs.min() < 0.4 || gs.max() > 5.0) // Limit the size to between these values 426 { 427 if (gs.max() == gs.min()) 428 return 1.0; 429 return 0.7 + a*(5.0 - 0.7)/(gs.max() - gs.min()); 430 } 431 return a; 432 }; 433 434 } else { 435 gf.doubleConvert = (a) { 436 import std.math : isNaN; 437 if (isNaN(a)) 438 return a; 439 assert(a >= gs.min() || a <= gs.max(), "Value falls outside of range"); 440 return a; 441 }; 442 443 } 444 immutable storeHash = gs.storeHash; 445 446 gf.stringConvert = (a) { 447 assert(a in storeHash, "Value not in guide"); 448 return gf.doubleConvert(storeHash[a]); 449 }; 450 return gf; 451 } 452 453 unittest 454 { 455 GuideStore!"" gs; 456 gs.put(["b","a"]); 457 auto gf = guideFunction(gs); 458 assertEqual(gf(0.1), 0.1); 459 assertEqual(gf("a"), 1); 460 461 import std.math : isNaN; 462 assert(isNaN(gf(double.init))); 463 } 464 465 unittest 466 { 467 GuideStore!"size" gs; 468 gs.put( [0.5, 4] ); 469 auto gf = guideFunction(gs); 470 assertEqual(gf(0.6), 0.6); 471 472 gs.put( [0.0] ); 473 auto gf2 = guideFunction(gs); 474 assertEqual(gf2(0.0), 0.7); 475 assertEqual(gf2(4.0), 5.0); 476 477 GuideStore!"size" gs3; 478 gs3.put( [0.0] ); 479 auto gf3 = guideFunction(gs3); 480 assertEqual(gf3(0.0), 1.0); 481 } 482 483 import ggplotd.colour : ColourGradientFunction; 484 /// Create an appropiate GuidToColourFunction from a GuideStore 485 auto guideFunction(string type)(GuideStore!type gs, ColourGradientFunction colourFunction) 486 if (type == "colour") 487 { 488 GuideToColourFunction gc; 489 gc.doubleConvert = (a) { 490 import std.math : isNaN; 491 if (isNaN(a)) { 492 import ggplotd.colourspace : RGBA; 493 return RGBA(0,0,0,0); 494 } 495 assert(a >= gs.min() || a <= gs.max(), "Value falls outside of range"); 496 return colourFunction(a, gs.min(), gs.max()); 497 }; 498 499 immutable storeHash = gs.storeHash; 500 501 gc.stringToDoubleConvert = (a) { 502 assert(a in storeHash, "Value not in storeHash"); 503 return storeHash[a]; 504 }; 505 506 gc.stringConvert = (a) { 507 assert(a in storeHash, "Value not in storeHash"); 508 return gc.doubleConvert(gc.stringToDoubleConvert(a)); 509 }; 510 return gc; 511 } 512 513 unittest 514 { 515 import ggplotd.colour : colourGradient, namedColour; 516 import ggplotd.colourspace : HCY, RGBA, toTuple; 517 GuideStore!"colour" gs; 518 gs.put([0.1, 3.0]); 519 auto gf = guideFunction(gs, colourGradient!HCY("blue-red")); 520 assertEqual(gf(0.1).toTuple, namedColour("blue").get().toTuple); 521 assertEqual(gf(3.0).toTuple, namedColour("red").get().toTuple); 522 assertEqual(gf("green").toTuple, namedColour("green").get().toTuple); 523 assertEqual(gf(namedColour("green").get()).toTuple, namedColour("green").get().toTuple); 524 assertEqual(gf(double.init).toTuple, RGBA(0,0,0,0).toTuple); 525 }