1 module ggplotd.aes; 2 3 import std.range : front, popFront, empty; 4 5 version (unittest) 6 { 7 import dunit.toolkit; 8 } 9 10 import std.typecons : Tuple, Typedef; 11 12 /** 13 Number of pixels 14 15 Mainly used to differentiate between drawing in plot coordinates or in pixel based coordinates. 16 */ 17 struct Pixel 18 { 19 /// Number of pixels in int 20 this( int val ) { value = val; } 21 22 /// Copy constructor 23 this( Pixel val ) { value = val; } 24 25 26 alias value this; 27 28 /// Number of pixels 29 int value; 30 } 31 32 unittest 33 { 34 static if (is(typeof(Pixel(10))==Pixel)) 35 {} else 36 assert(false); 37 } 38 39 import std.typecons : tuple; 40 /++ 41 Map data fields to "aesthetic" fields understood by the ggplotd geom functions 42 43 The most commonly used aesthetic fields in ggplotd are "x" and "y". Which further data 44 fields are used/required depends on the geom function being called. 45 46 Other common fields: 47 $(UL 48 $(LI "colour": Identifier for the colour. In general data points with different colour ids get different colours. This can be almost any type. You can also specify the colour by name or cairo.Color type if you want to specify an exact colour (any type that isNumeric, cairo.Color.RGB(A), or can be converted to string)) 49 $(LI "size": Gives the relative size of points/lineWidth etc.) 50 $(LI "label": Text labels (string)) 51 $(LI "angle": Angle of printed labels in radians (double)) 52 $(LI "alpha": Alpha value of the drawn object (double)) 53 $(LI "mask": Mask the area outside the axes. Prevents you from drawing outside of the area (bool)) 54 $(LI "fill": Whether to fill the object/holds the alpha value to fill with (double).)) 55 56 In practice aes is an alias for std.typecons.tuple. 57 58 Examples: 59 --------------------------- 60 struct Diamond 61 { 62 string clarity = "SI2"; 63 double carat = 0.23; 64 double price = 326; 65 } 66 67 Diamond diamond; 68 69 auto mapped = aes!("colour", "x", "y")(diamond.clarity, diamond.carat, diamond.price); 70 assert(mapped.colour == "SI2"); 71 assert(mapped.x == 0.23); 72 assert(mapped.y == 326); 73 --------------------------- 74 75 Examples: 76 --------------------------- 77 import std.typecons : Tuple; 78 // aes returns a named tuple 79 assert(aes!("x", "y")(1.0, 2.0) == Tuple!(double, "x", double, "y")(1.0, 2.0)); 80 --------------------------- 81 82 +/ 83 alias aes = tuple; 84 85 unittest 86 { 87 struct Diamond 88 { 89 string clarity = "SI2"; 90 double carat = 0.23; 91 double price = 326; 92 } 93 94 Diamond diamond; 95 96 auto mapped = aes!("colour", "x", "y")(diamond.clarity, diamond.carat, diamond.price); 97 assertEqual(mapped.colour, "SI2"); 98 assertEqual(mapped.x, 0.23); 99 assertEqual(mapped.y, 326); 100 101 102 import std.typecons : Tuple; 103 // aes is a convenient alternative to a named tuple 104 assert(aes!("x", "y")(1.0, 2.0) == Tuple!(double, "x", double, "y")(1.0, 2.0)); 105 } 106 107 /// 108 unittest 109 { 110 auto a = aes!(int, "y", int, "x")(1, 2); 111 assertEqual( a.y, 1 ); 112 assertEqual( a.x, 2 ); 113 114 auto a1 = aes!("y", "x")(1, 2); 115 assertEqual( a1.y, 1 ); 116 assertEqual( a1.x, 2 ); 117 118 auto a2 = aes!("y")(1); 119 assertEqual( a2.y, 1 ); 120 121 122 import std.range : zip; 123 import std.algorithm : map; 124 auto xs = [0,1]; 125 auto ys = [2,3]; 126 auto points = xs.zip(ys).map!((t) => aes!("x", "y")(t[0], t[1])); 127 assertEqual(points.front.x, 0); 128 assertEqual(points.front.y, 2); 129 points.popFront; 130 assertEqual(points.front.x, 1); 131 assertEqual(points.front.y, 3); 132 } 133 134 // TODO Also update default grouping if appropiate 135 /// Default values for most settings 136 static auto DefaultValues = aes!( 137 "label", "colour", "size", 138 "angle", "alpha", "mask", "fill" ) 139 ("", "black", 1.0, 0.0, 1.0, true, 0.0); 140 141 /++ 142 Aes is used to store and access data for plotting 143 144 Aes is an InputRange, with named Tuples as the ElementType. The names 145 refer to certain fields, such as x, y, colour etc. 146 147 The fields commonly used are data fields, such as "x" and "y". Which data 148 fields are required depends on the geom function being called. 149 150 Other common fields: 151 $(UL 152 $(LI "label": Text labels (string)) 153 $(LI "colour": Identifier for the colour. In general data points with different colour ids get different colours. This can be almost any type. You can also specify the colour by name or cairo.Color type if you want to specify an exact colour (any type that isNumeric, cairo.Color.RGB(A), or can be converted to string)) 154 $(LI "size": Gives the relative size of points/lineWidth etc.) 155 $(LI "angle": Angle of printed labels in radians (double)) 156 $(LI "alpha": Alpha value of the drawn object (double)) 157 $(LI "mask": Mask the area outside the axes. Prevents you from drawing outside of the area (bool)) 158 $(LI "fill": Whether to fill the object/holds the alpha value to fill with (double).)) 159 +/ 160 template Aes(Specs...) 161 { 162 import std.meta : AliasSeq; 163 template parseSpecs(Specs...) 164 { 165 import std.range : isInputRange, ElementType; 166 static if (Specs.length < 2) 167 { 168 alias parseSpecs = AliasSeq!(); 169 } 170 else static if ( 171 isInputRange!(Specs[0]) 172 && is(typeof(Specs[1]) : string) 173 ) 174 { 175 alias parseSpecs = AliasSeq!( 176 ElementType!(Specs[0]), Specs[1], 177 parseSpecs!(Specs[2 .. $])); 178 } 179 else 180 { 181 pragma(msg, Specs); 182 static assert(0, 183 "Attempted to instantiate Tuple with an " ~ "invalid argument: " ~ Specs[0].stringof); 184 } 185 } 186 187 template parseTypes(Specs...) 188 { 189 import std.range : isInputRange; 190 static if (Specs.length < 2) 191 { 192 alias parseTypes = AliasSeq!(); 193 } 194 else static if ( 195 isInputRange!(Specs[0]) 196 && is(typeof(Specs[1]) : string) 197 ) 198 { 199 alias parseTypes = AliasSeq!( 200 Specs[0], 201 parseTypes!(Specs[2 .. $])); 202 } 203 else 204 { 205 pragma(msg, Specs); 206 static assert(0, 207 "Attempted to instantiate Tuple with an " ~ "invalid argument: " ~ Specs[0].stringof); 208 } 209 } 210 alias elementsType = parseSpecs!Specs; 211 alias types = parseTypes!Specs; 212 213 struct Aes 214 { 215 import std.range : Zip; 216 private Zip!(types) aes; 217 218 this(Args...)(Args args) 219 { 220 import std.range : zip; 221 aes = zip(args); 222 } 223 224 void popFront() 225 { 226 aes.popFront; 227 } 228 229 auto @property empty() 230 { 231 return aes.empty; 232 } 233 234 auto @property front() 235 { 236 return Tuple!(elementsType)( aes.front.expand ); 237 } 238 } 239 } 240 241 /// Basic Aes usage 242 unittest 243 { 244 auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([0.0, 1], 245 [2, 1.0], ["white", "white2"]); 246 247 aes.popFront; 248 assertEqual(aes.front.y, 1); 249 assertEqual(aes.front.colour, "white2"); 250 251 auto aes2 = Aes!(double[], "x", double[], "y")([0.0, 1], [2.0, 1]); 252 assertEqual(aes2.front.y, 2); 253 254 import std.range : repeat; 255 256 auto xs = repeat(0); 257 auto aes3 = Aes!(typeof(xs), "x", double[], "y")(xs, [2.0, 1]); 258 259 assertEqual(aes3.front.x, 0); 260 aes3.popFront; 261 aes3.popFront; 262 assertEqual(aes3.empty, true); 263 } 264 265 266 import std.typetuple : TypeTuple; 267 private template fieldValues( T, Specs... ) 268 { 269 import std.typecons : Tuple, tuple; 270 auto fieldValues( T t ) 271 { 272 static if (Specs.length == 0) 273 return tuple(); 274 else 275 return tuple( __traits(getMember, t, Specs[0]), 276 (fieldValues!(typeof(t), Specs[1..$])(t)).expand ); 277 } 278 } 279 280 unittest 281 { 282 struct Point { double x; double y; string label = "Point"; } 283 auto pnt = Point( 1.0, 2.0 ); 284 auto fv = fieldValues!(Point, "x","y","label")(pnt); 285 assertEqual(fv[0], 1.0); 286 assertEqual(fv[1], 2.0); 287 assertEqual(fv[2], "Point"); 288 auto fv2 = fieldValues!(Point, "x","label")(pnt); 289 assertEqual(fv2[0], 1.0); 290 assertEqual(fv2[1], "Point"); 291 } 292 293 private template typeAndFields( T, Specs... ) 294 { 295 import std.meta : AliasSeq; 296 static if (Specs.length == 0) 297 alias typeAndFields = AliasSeq!(); 298 else 299 alias typeAndFields = AliasSeq!( 300 typeof(__traits(getMember, T, Specs[0])), 301 Specs[0], typeAndFields!(T, Specs[1..$]) ); 302 } 303 304 unittest 305 { 306 struct Point { double x; double y; string label = "Point"; } 307 alias fts = typeAndFields!(Point, "x","y","label"); 308 309 auto pnt = Point( 1.0, 2.0 ); 310 auto fv = fieldValues!(Point, "x","y","label")(pnt); 311 auto tp = Tuple!( fts )( fv.expand ); 312 assertEqual(tp.x, 1.0); 313 assertEqual(tp.y, 2.0); 314 assertEqual(tp.label, "Point"); 315 } 316 317 // Default fields to group by 318 alias DefaultGroupFields = TypeTuple!("alpha","colour","label"); 319 320 /++ 321 Groups data by colour label etc. 322 323 Will also add DefaultValues for label etc to the data. It is also possible to specify exactly what to group by on as a template parameter. See example. 324 +/ 325 template group(Specs...) 326 { 327 static if (Specs.length == 0) 328 { 329 alias Specs = DefaultGroupFields; 330 } 331 332 auto extractKey(T)(T a) 333 { 334 import ggplotd.meta : ApplyLeft; 335 import std.meta : Filter; 336 alias hasFieldT = ApplyLeft!(hasAesField, T); 337 alias fields = Filter!(hasFieldT, Specs); 338 static if (fields.length == 0) 339 return 1; 340 else 341 return fieldValues!(T, fields)(a); 342 } 343 344 auto group(AES)(AES aes) 345 { 346 import ggplotd.range : groupBy; 347 return aes.groupBy!((a) => extractKey(a)).values; 348 } 349 } 350 351 /// 352 unittest 353 { 354 import std.range : walkLength; 355 auto aes = Aes!(double[], "x", string[], "colour", double[], "alpha") 356 ([0.0,1,2,3], ["a","a","b","b"], [0.0,1,0,1]); 357 358 assertEqual(group!("colour","alpha")(aes).walkLength,4); 359 assertEqual(group!("alpha")(aes).walkLength,2); 360 361 // Ignores field that does not exist 362 assertEqual(group!("alpha","abcdef")(aes).walkLength,2); 363 364 // Should return one group holding them all 365 assertEqual(group!("abcdef")(aes)[0].walkLength,4); 366 367 assertEqual(group(aes).walkLength,4); 368 } 369 370 /// 371 unittest 372 { 373 auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([1.0, 374 2.0, 1.1], [3.0, 1.5, 1.1], ["a", "b", "a"]); 375 376 import std.range : walkLength, front, popFront; 377 378 auto grouped = aes.group; 379 assertEqual(grouped.walkLength, 2); 380 size_t totalLength = grouped.front.walkLength; 381 assertGreaterThan(totalLength, 0); 382 assertLessThan(totalLength, 3); 383 grouped.popFront; 384 assertEqual(totalLength + grouped.front.walkLength, 3); 385 } 386 387 import std.range : isInputRange; 388 389 /** 390 DataID is used to refer represent any type as a usable type 391 */ 392 struct DataID 393 { 394 /// Create DataID with given value and id 395 this( double value, string id ) 396 { 397 import std.typecons : tuple; 398 state = tuple( value, id ); 399 } 400 401 /// Overloading to for the DataID 402 T to(T)() const 403 { 404 import std.conv : to; 405 static if (is(T==double)) 406 return state[0]; 407 else 408 return state[1].to!T; 409 } 410 411 /// Tuple holding the value and id 412 Tuple!(double, string) state; 413 414 alias state this; 415 } 416 417 unittest 418 { 419 import std.conv : to; 420 auto did = DataID( 0.1, "a" ); 421 assertEqual( did[0], 0.1 ); 422 assertEqual( did.to!double, 0.1 ); 423 assertEqual( did.to!string, "a" ); 424 } 425 426 private template aesFields(T) 427 { 428 import std.traits; 429 template isAesField(alias name) 430 { 431 import painlesstraits : isFieldOrProperty; 432 import std.typecons : Tuple; 433 // To be honest, I am not sure why isFieldOrProperty!name does not 434 // suffice (instead of the first two), but that 435 // results in toHash for Tuple 436 static if ( __traits(compiles, isFieldOrProperty!( 437 __traits(getMember, T, name) ) ) 438 && isFieldOrProperty!(__traits(getMember,T,name)) 439 && name[0] != "_"[0] 440 && __traits(compiles, ( in T u ) { 441 auto a = __traits(getMember, u, name); 442 Tuple!(typeof(a),name)(a); } ) 443 ) 444 enum isAesField = true; 445 else 446 enum isAesField = false; 447 } 448 449 import std.meta : Filter; 450 enum aesFields = Filter!(isAesField, __traits(allMembers, T)); 451 } 452 453 unittest 454 { 455 struct Point { double x; double y; string label = "Point"; } 456 assertEqual( "x", aesFields!Point[0] ); 457 assertEqual( "y", aesFields!Point[1] ); 458 assertEqual( "label", aesFields!Point[2] ); 459 assertEqual( 3, aesFields!(Point).length ); 460 461 auto pnt2 = Tuple!(double, "x", double, "y", string, "label" )( 1.0, 2.0, "Point" ); 462 assertEqual( "x", aesFields!(typeof(pnt2))[0] ); 463 assertEqual( "y", aesFields!(typeof(pnt2))[1] ); 464 assertEqual( "label", aesFields!(typeof(pnt2))[2] ); 465 assertEqual( 3, aesFields!(typeof(pnt2)).length ); 466 } 467 468 private template hasAesField(T, alias name) 469 { 470 enum bool hasAesField = (function() { 471 bool has = false; 472 foreach (name2; aesFields!T) 473 { 474 if (name == name2) 475 has = true; 476 } 477 return has; 478 })(); 479 } 480 481 unittest 482 { 483 struct Point { double x; double y; string label = "Point"; } 484 static assert( hasAesField!(Point, "x") ); 485 static assert( !hasAesField!(Point, "z") ); 486 } 487 488 /++ 489 Merge two types by their members. 490 491 If it has similar named members, then it uses the second one. 492 493 returns a named Tuple (or Aes) with all the members and their values. 494 +/ 495 template merge(T, U) 496 { 497 auto merge(T base, U other) 498 { 499 import ggplotd.meta : ApplyLeft; 500 import std.meta : Filter, AliasSeq, templateNot; 501 alias fieldsU = aesFields!U; 502 alias notHasAesFieldU = ApplyLeft!(templateNot!(hasAesField),U); 503 alias fieldsT = Filter!(notHasAesFieldU, aesFields!T); 504 505 auto vT = fieldValues!(T, fieldsT)(base); 506 auto vU = fieldValues!(U, fieldsU)(other); 507 508 return Tuple!(AliasSeq!( 509 typeAndFields!(T,fieldsT), 510 typeAndFields!(U,fieldsU) 511 ))(vT.expand, vU.expand); 512 } 513 } 514 515 unittest 516 { 517 auto pnt = Tuple!(double, "x", double, "y", string, "label" )( 1.0, 2.0, "Point" ); 518 auto merged = DefaultValues.merge( pnt ); 519 assertEqual( merged.x, 1.0 ); 520 assertEqual( merged.y, 2.0 ); 521 assertEqual( merged.colour, "black" ); 522 assertEqual( merged.label, "Point" ); 523 524 // Test whether type/ordering is consistent 525 // Given enough benefit we can break this, but we'll have to adapt plotcli to match, 526 // which to be fair is relatively straightforward 527 static assert( is(Tuple!(string, "colour", double, "size", double, "angle", double, "alpha", 528 bool, "mask", double, "fill", double, "x", double, "y", string, "label") == typeof(merged) ) ); 529 } 530 531 /// 532 unittest 533 { 534 struct Point { double x; double y; string label = "Point"; } 535 auto pnt = Point( 1.0, 2.0 ); 536 537 auto merged = DefaultValues.merge( pnt ); 538 assertEqual( merged.x, 1.0 ); 539 assertEqual( merged.y, 2.0 ); 540 assertEqual( merged.colour, "black" ); 541 assertEqual( merged.label, "Point" ); 542 } 543 544 545 static import ggplotd.range; 546 /** 547 Deprecated: Moved to ggplotd.range; 548 */ 549 deprecated alias mergeRange = ggplotd.range.mergeRange;