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 /// Returns field if it exists, otherwise uses the passed default 142 auto fieldWithDefault(alias field, AES, T)(AES aes, T theDefault) 143 { 144 static if (hasAesField!(AES, field)) 145 return __traits(getMember, aes, field); 146 else 147 return theDefault; 148 } 149 150 unittest 151 { 152 struct Point { double x; double y; string label = "Point"; } 153 auto point = Point(1.0, 2.0); 154 assertEqual(fieldWithDefault!("x")(point, "1"), 1.0); 155 assertEqual(fieldWithDefault!("z")(point, "1"), "1"); 156 } 157 158 /++ 159 Aes is used to store and access data for plotting 160 161 Aes is an InputRange, with named Tuples as the ElementType. The names 162 refer to certain fields, such as x, y, colour etc. 163 164 The fields commonly used are data fields, such as "x" and "y". Which data 165 fields are required depends on the geom function being called. 166 167 Other common fields: 168 $(UL 169 $(LI "label": Text labels (string)) 170 $(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)) 171 $(LI "size": Gives the relative size of points/lineWidth etc.) 172 $(LI "angle": Angle of printed labels in radians (double)) 173 $(LI "alpha": Alpha value of the drawn object (double)) 174 $(LI "mask": Mask the area outside the axes. Prevents you from drawing outside of the area (bool)) 175 $(LI "fill": Whether to fill the object/holds the alpha value to fill with (double).)) 176 +/ 177 template Aes(Specs...) 178 { 179 import std.meta : AliasSeq; 180 template parseSpecs(Specs...) 181 { 182 import std.range : isInputRange, ElementType; 183 static if (Specs.length < 2) 184 { 185 alias parseSpecs = AliasSeq!(); 186 } 187 else static if ( 188 isInputRange!(Specs[0]) 189 && is(typeof(Specs[1]) : string) 190 ) 191 { 192 alias parseSpecs = AliasSeq!( 193 ElementType!(Specs[0]), Specs[1], 194 parseSpecs!(Specs[2 .. $])); 195 } 196 else 197 { 198 pragma(msg, Specs); 199 static assert(0, 200 "Attempted to instantiate Tuple with an " ~ "invalid argument: " ~ Specs[0].stringof); 201 } 202 } 203 204 template parseTypes(Specs...) 205 { 206 import std.range : isInputRange; 207 static if (Specs.length < 2) 208 { 209 alias parseTypes = AliasSeq!(); 210 } 211 else static if ( 212 isInputRange!(Specs[0]) 213 && is(typeof(Specs[1]) : string) 214 ) 215 { 216 alias parseTypes = AliasSeq!( 217 Specs[0], 218 parseTypes!(Specs[2 .. $])); 219 } 220 else 221 { 222 pragma(msg, Specs); 223 static assert(0, 224 "Attempted to instantiate Tuple with an " ~ "invalid argument: " ~ Specs[0].stringof); 225 } 226 } 227 228 // maps a type to its init value 229 private auto init(T)() 230 { 231 return T.init; 232 } 233 234 // ArgsCall taken from https://dlang.org/library/std/meta/alias_seq.html 235 private auto ref ArgCall(alias Func, arg)() 236 { 237 return Func!(arg)(); 238 } 239 240 // Map taken from https://dlang.org/library/std/meta/alias_seq.html 241 private template Map(alias Func, args...) 242 { 243 static if (args.length > 1) 244 { 245 alias Map = AliasSeq!(ArgCall!(Func, args[0]), Map!(Func, args[1 .. $])); 246 } 247 else 248 { 249 alias Map = ArgCall!(Func, args[0]); 250 } 251 } 252 253 alias elementsType = parseSpecs!Specs; 254 alias types = parseTypes!Specs; 255 256 struct Aes 257 { 258 import std.range : zip; 259 260 // from 2.080.0 on zip can return not only Zip, but also ZipShortest. On top of that it is not accessible from 261 // outside. Therefore, use "typeof" the accessible convenience template function "zip" only. 262 private typeof(zip!types(Map!(init, types))) aes; 263 264 // use explicit types as they are known from template argument deduction stage 265 this(types args) 266 { 267 import std.range : zip; 268 aes = zip(args); 269 } 270 271 void popFront() 272 { 273 aes.popFront; 274 } 275 276 auto @property empty() 277 { 278 return aes.empty; 279 } 280 281 auto @property front() 282 { 283 return Tuple!(elementsType)( aes.front.expand ); 284 } 285 } 286 } 287 288 /// Basic Aes usage 289 unittest 290 { 291 auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([0.0, 1], 292 [2, 1.0], ["white", "white2"]); 293 294 aes.popFront; 295 assertEqual(aes.front.y, 1); 296 assertEqual(aes.front.colour, "white2"); 297 298 auto aes2 = Aes!(double[], "x", double[], "y")([0.0, 1], [2.0, 1]); 299 assertEqual(aes2.front.y, 2); 300 301 import std.range : repeat; 302 303 auto xs = repeat(0); 304 auto aes3 = Aes!(typeof(xs), "x", double[], "y")(xs, [2.0, 1]); 305 306 assertEqual(aes3.front.x, 0); 307 aes3.popFront; 308 aes3.popFront; 309 assertEqual(aes3.empty, true); 310 } 311 312 313 import std.typetuple : TypeTuple; 314 private template fieldValues( T, Specs... ) 315 { 316 import std.typecons : Tuple, tuple; 317 auto fieldValues( T t ) 318 { 319 static if (Specs.length == 0) 320 return tuple(); 321 else 322 return tuple( __traits(getMember, t, Specs[0]), 323 (fieldValues!(typeof(t), Specs[1..$])(t)).expand ); 324 } 325 } 326 327 unittest 328 { 329 struct Point { double x; double y; string label = "Point"; } 330 auto pnt = Point( 1.0, 2.0 ); 331 auto fv = fieldValues!(Point, "x","y","label")(pnt); 332 assertEqual(fv[0], 1.0); 333 assertEqual(fv[1], 2.0); 334 assertEqual(fv[2], "Point"); 335 auto fv2 = fieldValues!(Point, "x","label")(pnt); 336 assertEqual(fv2[0], 1.0); 337 assertEqual(fv2[1], "Point"); 338 } 339 340 private template typeAndFields( T, Specs... ) 341 { 342 import std.meta : AliasSeq; 343 static if (Specs.length == 0) 344 alias typeAndFields = AliasSeq!(); 345 else 346 alias typeAndFields = AliasSeq!( 347 typeof(__traits(getMember, T, Specs[0])), 348 Specs[0], typeAndFields!(T, Specs[1..$]) ); 349 } 350 351 unittest 352 { 353 struct Point { double x; double y; string label = "Point"; } 354 alias fts = typeAndFields!(Point, "x","y","label"); 355 356 auto pnt = Point( 1.0, 2.0 ); 357 auto fv = fieldValues!(Point, "x","y","label")(pnt); 358 auto tp = Tuple!( fts )( fv.expand ); 359 assertEqual(tp.x, 1.0); 360 assertEqual(tp.y, 2.0); 361 assertEqual(tp.label, "Point"); 362 } 363 364 // Default fields to group by 365 alias DefaultGroupFields = TypeTuple!("alpha","colour","label"); 366 367 /++ 368 Groups data by colour label etc. 369 370 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. 371 +/ 372 template group(Specs...) 373 { 374 static if (Specs.length == 0) 375 { 376 alias Specs = DefaultGroupFields; 377 } 378 379 auto extractKey(T)(T a) 380 { 381 import ggplotd.meta : ApplyLeft; 382 import std.meta : Filter; 383 alias hasFieldT = ApplyLeft!(hasAesField, T); 384 alias fields = Filter!(hasFieldT, Specs); 385 static if (fields.length == 0) 386 return 1; 387 else 388 return fieldValues!(T, fields)(a); 389 } 390 391 auto group(AES)(AES aes) 392 { 393 import ggplotd.range : groupBy; 394 return aes.groupBy!((a) => extractKey(a)).values; 395 } 396 } 397 398 /// 399 unittest 400 { 401 import std.range : walkLength; 402 auto aes = Aes!(double[], "x", string[], "colour", double[], "alpha") 403 ([0.0,1,2,3], ["a","a","b","b"], [0.0,1,0,1]); 404 405 assertEqual(group!("colour","alpha")(aes).walkLength,4); 406 assertEqual(group!("alpha")(aes).walkLength,2); 407 408 // Ignores field that does not exist 409 assertEqual(group!("alpha","abcdef")(aes).walkLength,2); 410 411 // Should return one group holding them all 412 assertEqual(group!("abcdef")(aes)[0].walkLength,4); 413 414 assertEqual(group(aes).walkLength,4); 415 } 416 417 /// 418 unittest 419 { 420 auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([1.0, 421 2.0, 1.1], [3.0, 1.5, 1.1], ["a", "b", "a"]); 422 423 import std.range : walkLength, front, popFront; 424 425 auto grouped = aes.group; 426 assertEqual(grouped.walkLength, 2); 427 size_t totalLength = grouped.front.walkLength; 428 assertGreaterThan(totalLength, 0); 429 assertLessThan(totalLength, 3); 430 grouped.popFront; 431 assertEqual(totalLength + grouped.front.walkLength, 3); 432 } 433 434 import std.range : isInputRange; 435 436 /** 437 DataID is used to refer represent any type as a usable type 438 */ 439 struct DataID 440 { 441 /// Create DataID with given value and id 442 this( double value, string id ) 443 { 444 import std.typecons : tuple; 445 state = tuple( value, id ); 446 } 447 448 /// Overloading to for the DataID 449 T to(T)() const 450 { 451 import std.conv : to; 452 static if (is(T==double)) 453 return state[0]; 454 else 455 return state[1].to!T; 456 } 457 458 /// Tuple holding the value and id 459 Tuple!(double, string) state; 460 461 alias state this; 462 } 463 464 unittest 465 { 466 import std.conv : to; 467 auto did = DataID( 0.1, "a" ); 468 assertEqual( did[0], 0.1 ); 469 assertEqual( did.to!double, 0.1 ); 470 assertEqual( did.to!string, "a" ); 471 } 472 473 private template aesFields(T) 474 { 475 import std.traits; 476 template isAesField(alias name) 477 { 478 import painlesstraits : isFieldOrProperty; 479 import std.typecons : Tuple; 480 // To be honest, I am not sure why isFieldOrProperty!name does not 481 // suffice (instead of the first two), but that 482 // results in toHash for Tuple 483 static if ( __traits(compiles, isFieldOrProperty!( 484 __traits(getMember, T, name) ) ) 485 && isFieldOrProperty!(__traits(getMember,T,name)) 486 && name[0] != "_"[0] 487 && __traits(compiles, ( in T u ) { 488 auto a = __traits(getMember, u, name); 489 Tuple!(typeof(a),name)(a); } ) 490 ) 491 enum isAesField = true; 492 else 493 enum isAesField = false; 494 } 495 496 import std.meta : Filter; 497 enum aesFields = Filter!(isAesField, __traits(allMembers, T)); 498 } 499 500 unittest 501 { 502 struct Point { double x; double y; string label = "Point"; } 503 assertEqual( "x", aesFields!Point[0] ); 504 assertEqual( "y", aesFields!Point[1] ); 505 assertEqual( "label", aesFields!Point[2] ); 506 assertEqual( 3, aesFields!(Point).length ); 507 508 auto pnt2 = Tuple!(double, "x", double, "y", string, "label" )( 1.0, 2.0, "Point" ); 509 assertEqual( "x", aesFields!(typeof(pnt2))[0] ); 510 assertEqual( "y", aesFields!(typeof(pnt2))[1] ); 511 assertEqual( "label", aesFields!(typeof(pnt2))[2] ); 512 assertEqual( 3, aesFields!(typeof(pnt2)).length ); 513 } 514 515 package template hasAesField(T, alias name) 516 { 517 enum bool hasAesField = (function() { 518 bool has = false; 519 foreach (name2; aesFields!T) 520 { 521 if (name == name2) 522 has = true; 523 } 524 return has; 525 })(); 526 } 527 528 unittest 529 { 530 struct Point { double x; double y; string label = "Point"; } 531 static assert( hasAesField!(Point, "x") ); 532 static assert( !hasAesField!(Point, "z") ); 533 } 534 535 /++ 536 Merge two types by their members. 537 538 If it has similar named members, then it uses the second one. 539 540 returns a named Tuple (or Aes) with all the members and their values. 541 +/ 542 template merge(T, U) 543 { 544 auto merge(T base, U other) 545 { 546 import ggplotd.meta : ApplyLeft; 547 import std.meta : Filter, AliasSeq, templateNot; 548 alias fieldsU = aesFields!U; 549 alias notHasAesFieldU = ApplyLeft!(templateNot!(hasAesField),U); 550 alias fieldsT = Filter!(notHasAesFieldU, aesFields!T); 551 552 auto vT = fieldValues!(T, fieldsT)(base); 553 auto vU = fieldValues!(U, fieldsU)(other); 554 555 return Tuple!(AliasSeq!( 556 typeAndFields!(T,fieldsT), 557 typeAndFields!(U,fieldsU) 558 ))(vT.expand, vU.expand); 559 } 560 } 561 562 unittest 563 { 564 auto pnt = Tuple!(double, "x", double, "y", string, "label" )( 1.0, 2.0, "Point" ); 565 auto merged = DefaultValues.merge( pnt ); 566 assertEqual( merged.x, 1.0 ); 567 assertEqual( merged.y, 2.0 ); 568 assertEqual( merged.colour, "black" ); 569 assertEqual( merged.label, "Point" ); 570 571 // Test whether type/ordering is consistent 572 // Given enough benefit we can break this, but we'll have to adapt plotcli to match, 573 // which to be fair is relatively straightforward 574 static assert( is(Tuple!(string, "colour", double, "size", 575 double, "angle", double, "alpha", bool, "mask", 576 double, "fill", double, "x", double, "y", string, "label") == typeof(merged) ) ); 577 } 578 579 /// 580 unittest 581 { 582 struct Point { double x; double y; string label = "Point"; } 583 auto pnt = Point( 1.0, 2.0 ); 584 585 auto merged = DefaultValues.merge( pnt ); 586 assertEqual( merged.x, 1.0 ); 587 assertEqual( merged.y, 2.0 ); 588 assertEqual( merged.colour, "black" ); 589 assertEqual( merged.label, "Point" ); 590 } 591 592 593 static import ggplotd.range; 594 /** 595 Deprecated: Moved to ggplotd.range; 596 */ 597 deprecated alias mergeRange = ggplotd.range.mergeRange;