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; 11 12 // TODO Also update default grouping if appropiate 13 14 /// 15 static auto DefaultValues = Tuple!( 16 string, "label", string, "colour", double, "size", 17 double, "angle", double, "alpha", bool, "mask", double, "fill" ) 18 ("", "black", 1.0, 0, 1, true, 0.0); 19 20 /++ 21 Aes is used to store and access data for plotting 22 23 Aes is an InputRange, with named Tuples as elements. The names 24 refer to certain fields, such as x, y, colour etc. If certain fields 25 are not provided then it provides a default value (see DefaultValues). 26 27 The fields commonly used are data fields, such as "x" and "y". Which data 28 fields are required depends on the geom* function. By default the named 29 Tuple holds the fields: 30 $(UL 31 $(LI "label": Text labels (string)) 32 $(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)) 33 $(LI "size": Gives the relative size of points/lineWidth etc. 34 $(LI "angle": Angle of printed labels in radians (double)) 35 $(LI "alpha": Alpha value of the drawn object (double)) 36 $(LI "mask": Mask the area outside the axes. Prevents you from drawing outside of the area (bool)) 37 $(LI "fill": Whether to fill the object/holds the alpha value to fill with (double).)) 38 +/ 39 template Aes(Specs...) 40 { 41 import std.traits : Identity; 42 import std.typecons : isTuple; 43 import std.typetuple : staticMap, TypeTuple; 44 import std.range : isInputRange; 45 46 // Parse (type,name) pairs (FieldSpecs) out of the specified 47 // arguments. Some fields would have name, others not. 48 template parseSpecs(Specs...) 49 { 50 static if (Specs.length == 0) 51 { 52 alias parseSpecs = TypeTuple!(); 53 } 54 else static if (is(Specs[0]) && isInputRange!(Specs[0])) 55 { 56 static if (is(typeof(Specs[1]) : string)) 57 { 58 alias parseSpecs = TypeTuple!(FieldSpec!(Specs[0 .. 2]), parseSpecs!(Specs[2 .. $])); 59 } 60 else 61 { 62 alias parseSpecs = TypeTuple!(FieldSpec!(Specs[0]), parseSpecs!(Specs[1 .. $])); 63 } 64 } 65 else 66 { 67 static assert(0, 68 "Attempted to instantiate Tuple with an " ~ "invalid argument: " ~ Specs[0].stringof); 69 } 70 } 71 72 template FieldSpec(T, string s = "") 73 { 74 alias Type = T; 75 alias name = s; 76 } 77 78 alias fieldSpecs = parseSpecs!Specs; 79 80 // Used with staticMap. 81 alias extractType(alias spec) = spec.Type; 82 alias extractName(alias spec) = spec.name; 83 84 // Generates named fields as follows: 85 // alias name_0 = Identity!(field[0]); 86 // alias name_1 = Identity!(field[1]); 87 // : 88 // NOTE: field[k] is an expression (which yields a symbol of a 89 // variable) and can't be aliased directly. 90 string injectNamedFields() 91 { 92 string decl = ""; 93 94 foreach (i, name; staticMap!(extractName, fieldSpecs)) 95 { 96 import std.format : format; 97 98 decl ~= format("alias _%s = Identity!(field[%s]);", i, i); 99 if (name.length != 0) 100 { 101 decl ~= format("alias %s = _%s;", name, i); 102 } 103 } 104 return decl; 105 } 106 107 alias fieldNames = staticMap!(extractName, fieldSpecs); 108 109 string injectFront() 110 { 111 import std.format : format; 112 113 string decl = "auto front() { import std.range : ElementType;"; 114 decl ~= "import std.typecons : Tuple; import std.range : front;"; 115 116 string tupleType = "Tuple!("; 117 string values = "("; 118 119 foreach (i, name; fieldNames) 120 { 121 122 tupleType ~= format(q{typeof(%s.front),}, name); 123 tupleType ~= "q{" ~ name ~ "},"; 124 values ~= format("this.%s.front,", name); 125 } 126 127 decl ~= "return " ~ tupleType[0 .. $ - 1] ~ ")" ~ values[0 .. $ - 1] ~ "); }"; 128 //string decl2 = format("auto front() { import std.stdio; \"%s\".writeln; return 0.0; }", decl); 129 return decl; 130 } 131 132 // Returns Specs for a subtuple this[from .. to] preserving field 133 // names if any. 134 alias sliceSpecs(size_t from, size_t to) = staticMap!(expandSpec, fieldSpecs[from .. to]); 135 136 template expandSpec(alias spec) 137 { 138 static if (spec.name.length == 0) 139 { 140 alias expandSpec = TypeTuple!(spec.Type); 141 } 142 else 143 { 144 alias expandSpec = TypeTuple!(spec.Type, spec.name); 145 } 146 } 147 148 enum areCompatibleTuples(Tup1, Tup2, string op) = isTuple!Tup2 && is(typeof({ 149 Tup1 tup1 = void; 150 Tup2 tup2 = void; 151 static assert(tup1.field.length == tup2.field.length); 152 foreach (i, _; 153 Tup1.Types) 154 { 155 auto lhs = typeof(tup1.field[i]).init; 156 auto rhs = typeof(tup2.field[i]).init; 157 static if (op == "=") 158 lhs = rhs; 159 else 160 auto result = mixin("lhs " ~ op ~ " rhs"); 161 } 162 })); 163 164 enum areBuildCompatibleTuples(Tup1, Tup2) = isTuple!Tup2 && is(typeof({ 165 static assert(Tup1.Types.length == Tup2.Types.length); 166 foreach (i, _; 167 Tup1.Types) 168 static assert(isBuildable!(Tup1.Types[i], Tup2.Types[i])); 169 })); 170 171 /+ Returns $(D true) iff a $(D T) can be initialized from a $(D U). +/ 172 enum isBuildable(T, U) = is(typeof({ U u = U.init; T t = u; })); 173 /+ Helper for partial instanciation +/ 174 template isBuildableFrom(U) 175 { 176 enum isBuildableFrom(T) = isBuildable!(T, U); 177 } 178 struct Aes 179 { 180 /** 181 * The type of the tuple's components. 182 */ 183 alias Types = staticMap!(extractType, fieldSpecs); 184 185 /** 186 * The names of the tuple's components. Unnamed fields have empty names. 187 * 188 * Examples: 189 * ---- 190 * alias Fields = Tuple!(int, "id", string, float); 191 * static assert(Fields.fieldNames == TypeTuple!("id", "", "")); 192 * ---- 193 */ 194 alias fieldNames = staticMap!(extractName, fieldSpecs); 195 196 /** 197 * Use $(D t.expand) for a tuple $(D t) to expand it into its 198 * components. The result of $(D expand) acts as if the tuple components 199 * were listed as a list of values. (Ordinarily, a $(D Tuple) acts as a 200 * single value.) 201 * 202 * Examples: 203 * ---- 204 * auto t = tuple(1, " hello ", 2.3); 205 * writeln(t); // Tuple!(int, string, double)(1, " hello ", 2.3) 206 * writeln(t.expand); // 1 hello 2.3 207 * ---- 208 */ 209 Types expand; 210 mixin(injectNamedFields()); 211 212 // backwards compatibility 213 alias field = expand; 214 215 /** 216 * Constructor taking one value for each field. 217 */ 218 static if (Types.length > 0) 219 { 220 this(Types values) 221 { 222 field[] = values[]; 223 } 224 } 225 226 /** 227 * Constructor taking a compatible array. 228 * 229 * Examples: 230 * ---- 231 * int[2] ints; 232 * Tuple!(int, int) t = ints; 233 * ---- 234 */ 235 /+this(U, size_t n)(U[n] values) if (n == Types.length 236 && allSatisfy!(isBuildableFrom!U, Types)) 237 { 238 foreach (i, _; Types) 239 { 240 field[i] = values[i]; 241 } 242 }+/ 243 244 /** 245 * Constructor taking a compatible tuple. 246 */ 247 this(U)(U another) if (areBuildCompatibleTuples!(typeof(this), U)) 248 { 249 field[] = another.field[]; 250 } 251 252 mixin(injectFront()); 253 254 /// 255 void popFront() 256 { 257 import std.range : popFront; 258 259 foreach (i, _; Types[0 .. $]) 260 { 261 field[i].popFront(); 262 } 263 } 264 265 /// 266 auto save() const 267 { 268 return this; 269 } 270 271 /// 272 @property bool empty() 273 { 274 if (length == 0) 275 return true; 276 return false; 277 } 278 279 /// 280 size_t length() 281 { 282 import std.algorithm : min; 283 import std.range : walkLength, isInfinite; 284 285 size_t l = size_t.max; 286 foreach (i, type; Types[0 .. $]) 287 { 288 static if (!isInfinite!type) 289 { 290 if (field[i].walkLength < l) 291 l = field[i].walkLength; 292 } 293 } 294 return l; 295 } 296 297 /** 298 * Comparison for equality. 299 */ 300 bool opEquals(R)(R rhs) if (areCompatibleTuples!(typeof(this), R, "==")) 301 { 302 return field[] == rhs.field[]; 303 } 304 305 /// ditto 306 bool opEquals(R)(R rhs) const if (areCompatibleTuples!(typeof(this), R, "==")) 307 { 308 return field[] == rhs.field[]; 309 } 310 311 /** 312 * Comparison for ordering. 313 */ 314 int opCmp(R)(R rhs) if (areCompatibleTuples!(typeof(this), R, "<")) 315 { 316 foreach (i, Unused; Types) 317 { 318 if (field[i] != rhs.field[i]) 319 { 320 return field[i] < rhs.field[i] ? -1 : 1; 321 } 322 } 323 return 0; 324 } 325 326 /// ditto 327 int opCmp(R)(R rhs) const if (areCompatibleTuples!(typeof(this), R, "<")) 328 { 329 foreach (i, Unused; Types) 330 { 331 if (field[i] != rhs.field[i]) 332 { 333 return field[i] < rhs.field[i] ? -1 : 1; 334 } 335 } 336 return 0; 337 } 338 339 /** 340 * Assignment from another tuple. Each element of the source must be 341 * implicitly assignable to the respective element of the target. 342 */ 343 void opAssign(R)(auto ref R rhs) if (areCompatibleTuples!(typeof(this), R, 344 "=")) 345 { 346 import std.algorithm : swap; 347 348 static if (is(R : Tuple!Types) && !__traits(isRef, rhs)) 349 { 350 if (__ctfe) 351 { 352 // Cannot use swap at compile time 353 field[] = rhs.field[]; 354 } 355 else 356 { 357 // Use swap-and-destroy to optimize rvalue assignment 358 swap!(Tuple!Types)(this, rhs); 359 } 360 } 361 else 362 { 363 // Do not swap; opAssign should be called on the fields. 364 field[] = rhs.field[]; 365 } 366 } 367 368 /** 369 * Takes a slice of the tuple. 370 * 371 * Examples: 372 * ---- 373 * Tuple!(int, string, float, double) a; 374 * a[1] = "abc"; 375 * a[2] = 4.5; 376 * auto s = a.slice!(1, 3); 377 * static assert(is(typeof(s) == Tuple!(string, float))); 378 * assert(s[0] == "abc" && s[1] == 4.5); 379 * ---- 380 */ 381 @property ref Tuple!(sliceSpecs!(from, to)) slice(size_t from, size_t to)() @trusted if ( 382 from <= to && to <= Types.length) 383 { 384 return *cast(typeof(return)*)&(field[from]); 385 } 386 387 /// 388 size_t toHash() const nothrow @trusted 389 { 390 size_t h = 0; 391 foreach (i, T; Types) 392 h += typeid(T).getHash(cast(const void*)&field[i]); 393 return h; 394 } 395 396 /** 397 * Converts to string. 398 */ 399 void toString(DG)(scope DG sink) 400 { 401 enum header = typeof(this).stringof ~ "(", footer = ")", separator = ", "; 402 sink(header); 403 foreach (i, Type; Types) 404 { 405 static if (i > 0) 406 { 407 sink(separator); 408 } 409 // TODO: Change this once toString() works for shared objects. 410 static if (is(Type == class) && is(typeof(Type.init) == shared)) 411 { 412 sink(Type.stringof); 413 } 414 else 415 { 416 import std.format : FormatSpec, formatElement; 417 418 FormatSpec!char f; 419 formatElement(sink, field[i], f); 420 } 421 } 422 sink(footer); 423 } 424 425 /// 426 string toString()() 427 { 428 import std.conv : to; 429 430 return this.to!string; 431 } 432 } 433 } 434 435 unittest 436 { 437 auto tup = Aes!(double[], "x", double[], "y", string[], "colour")([0, 1], 438 [2, 1], ["white", "white2"]); 439 auto tup2 = Aes!(double[], "x", double[], "y")([0, 1], [2, 1]); 440 assertEqual(tup.colour, ["white", "white2"]); 441 assertEqual(tup.length, 2); 442 assertEqual(tup2.length, 2); 443 444 tup2.x ~= 0.0; 445 tup2.x ~= 0.0; 446 assertEqual(tup2.length, 2); 447 tup2.y ~= 0.0; 448 assertEqual(tup2.length, 3); 449 } 450 451 /// Basic Aes usage 452 unittest 453 { 454 auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([0, 1], 455 [2, 1], ["white", "white2"]); 456 457 aes.popFront; 458 assertEqual(aes.front.y, 1); 459 assertEqual(aes.front.colour, "white2"); 460 461 auto aes2 = Aes!(double[], "x", double[], "y")([0, 1], [2, 1]); 462 assertEqual(aes2.front.y, 2); 463 464 import std.range : repeat; 465 466 auto xs = repeat(0); 467 auto aes3 = Aes!(typeof(xs), "x", double[], "y")(xs, [2, 1]); 468 469 assertEqual(aes3.front.x, 0); 470 aes3.popFront; 471 aes3.popFront; 472 assertEqual(aes3.empty, true); 473 474 } 475 476 /++ 477 Groups data by colour label etc. 478 479 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. 480 +/ 481 template group(Specs...) 482 { 483 string buildExtractKey() 484 { 485 static if (Specs.length == 0) 486 { 487 import std.typetuple : TypeTuple; 488 alias Specs = TypeTuple!("alpha","colour","label"); 489 } 490 string types = ""; 491 string values = ""; 492 foreach( spec; Specs ) 493 { 494 types ~= "typeof(a." ~ spec ~"),"; 495 values ~= "a." ~ spec ~","; 496 } 497 string str = "auto extractKey(T)(T a) 498 { return Tuple!(" ~ types[0..$-1] ~ ")(" ~ values[0..$-1] ~ "); }"; 499 return str; 500 } 501 502 auto group(AES)(AES aes) 503 { 504 import std.stdio; 505 mixin(buildExtractKey()); 506 507 // Attach all default fields 508 auto merged = DefaultValues.mergeRange( aes ); 509 extractKey( merged.front ); 510 511 typeof(merged.front())[][typeof(extractKey(merged.front))] grouped; 512 513 // Extract keys for aa and store in aa. Return the values of the aa 514 foreach( tup; merged ) 515 { 516 auto key = extractKey(tup); 517 if (key in grouped) 518 grouped[key] ~= tup; 519 else 520 grouped[key] = [tup]; 521 } 522 return grouped.values; 523 } 524 } 525 526 /// 527 unittest 528 { 529 import std.range : walkLength; 530 auto aes = Aes!(double[], "x", string[], "colour", double[], "alpha") 531 ([0,1,2,3], ["a","a","b","b"], [0,1,0,1]); 532 533 assertEqual(group!("colour","alpha")(aes).walkLength,4); 534 assertEqual(group!("alpha")(aes).walkLength,2); 535 536 assertEqual(group(aes).walkLength,4); 537 } 538 539 /// 540 unittest 541 { 542 auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([1.0, 543 2.0, 1.1], [3.0, 1.5, 1.1], ["a", "b", "a"]); 544 545 import std.range : walkLength, front, popFront; 546 547 auto grouped = aes.group; 548 assertEqual(grouped.walkLength, 2); 549 assertEqual(grouped.front.walkLength, 2); 550 grouped.popFront; 551 assertEqual(grouped.front.walkLength, 1); 552 } 553 554 /// 555 import std.range : isInputRange; 556 557 alias DataID = Tuple!(double, string); 558 559 /// 560 struct NumericLabel(T) if (isInputRange!T) 561 { 562 import std.range : ElementType; 563 import std.traits : isNumeric; 564 565 alias E = ElementType!T; 566 567 /// 568 this(T range) 569 { 570 original = range; 571 } 572 573 /// 574 @property auto front() 575 { 576 import std.typecons : Tuple; 577 import std.range : front; 578 import std.conv : to; 579 580 static if (isNumeric!E) 581 return DataID(original.front.to!double, original.front.to!string); 582 else 583 { 584 if (original.front !in fromLabelMap) 585 { 586 fromLabelMap[original.front] = fromLabelMap.length.to!double; 587 //toLabelMap[fromLabelMap[original.front]] 588 // = original.front; 589 } 590 return DataID(fromLabelMap[original.front], original.front.to!string, 591 ); 592 } 593 } 594 595 /// 596 void popFront() 597 { 598 import std.range : popFront; 599 600 original.popFront; 601 } 602 603 /// 604 @property bool empty() 605 { 606 import std.range : empty; 607 608 return original.empty; 609 } 610 611 /// 612 @property bool numeric() 613 { 614 static if (isNumeric!E) 615 return true; 616 else 617 return false; 618 } 619 620 private: 621 T original; 622 //E[double] toLabelMap; 623 double[E] fromLabelMap; 624 } 625 626 unittest 627 { 628 import std.stdio : writeln; 629 import std.array : array; 630 import std.algorithm : map; 631 632 auto num = NumericLabel!(double[])([0.0, 0.1, 1.0, 0.0]); 633 assertEqual(num.map!((a) => a[0]).array, [0.0, 0.1, 1.0, 0.0]); 634 assertEqual(num.map!((a) => a[1]).array, ["0", "0.1", "1", "0"]); 635 auto strs = NumericLabel!(string[])(["a", "c", "b", "a"]); 636 assertEqual(strs.map!((a) => a[0]).array, [0, 1, 2.0, 0.0]); 637 assertEqual(strs.map!((a) => a[1]).array, ["a", "c", "b", "a"]); 638 } 639 640 unittest 641 { 642 import painlesstraits; 643 644 auto t = Tuple!(double,"x")(1.0); 645 646 static assert(isFieldOrProperty!(t.x)); 647 } 648 649 /++ 650 Merge two types by their members. 651 652 If it has similar named members, then it uses the second one. 653 654 returns a named Tuple (or Aes) with all the members and their values. 655 +/ 656 template merge(T, U) 657 { 658 import std.traits; 659 import painlesstraits; 660 auto generateCode() 661 { 662 import std.string : split; 663 string typing = "Tuple!("; 664 //string typing = T.stringof.split("!")[0] ~ "!("; 665 //string typingU = U.stringof.split("!")[0] ~ "!("; 666 string variables = "("; 667 foreach (name; __traits(allMembers, U)) 668 { 669 static if (__traits(compiles, isFieldOrProperty!( 670 __traits(getMember, U, name))) 671 && __traits(compiles, ( in U u ) { 672 auto a = __traits(getMember, u, name); 673 Tuple!(typeof(a),name)(a); } ) 674 && isFieldOrProperty!(__traits(getMember,U,name)) 675 && name[0] != "_"[0] ) 676 { 677 typing ~= "typeof(other." ~ name ~ "),\"" ~ name ~ "\","; 678 variables ~= "other." ~ name ~ ","; 679 } 680 } 681 682 foreach (name; __traits(allMembers, T)) 683 { 684 static if (__traits(compiles, isFieldOrProperty!( 685 __traits(getMember, T, name))) 686 && __traits(compiles, ( in T u ) { 687 auto a = __traits(getMember, u, name); 688 Tuple!(typeof(a),name)(a); } ) 689 && isFieldOrProperty!(__traits(getMember,T,name)) 690 && name[0] != "_"[0] ) 691 { 692 bool contains = false; 693 foreach (name2; __traits(allMembers, U)) 694 { 695 if (name == name2) 696 contains = true; 697 } 698 if (!contains) 699 { 700 typing ~= "typeof(base." ~ name ~ "),\"" ~ name ~ "\","; 701 variables ~= "base." ~ name ~ ","; 702 } 703 } 704 } 705 return "return " ~ typing[0 .. $ - 1] ~ ")" ~ variables[0 .. $ - 1] ~ ");"; 706 } 707 708 auto merge(T base, U other) 709 { 710 mixin(generateCode()); 711 } 712 } 713 714 /// 715 unittest 716 { 717 import std.range : front; 718 719 auto xs = ["a", "b"]; 720 auto ys = ["c", "d"]; 721 auto labels = ["e", "f"]; 722 auto aes = Aes!(string[], "x", string[], "y", string[], "label")(xs, ys, labels); 723 724 auto nlAes = merge(aes, Aes!(NumericLabel!(string[]), "x", 725 NumericLabel!(string[]), "y")(NumericLabel!(string[])(aes.x), 726 NumericLabel!(string[])(aes.y))); 727 728 assertEqual(nlAes.x.front[0], 0); 729 assertEqual(nlAes.label.front, "e"); 730 } 731 732 unittest 733 { 734 auto pnt = Tuple!(double, "x", double, "y", string, "label" )( 1.0, 2.0, "Point" ); 735 auto merged = DefaultValues.merge( pnt ); 736 assertEqual( merged.x, 1.0 ); 737 assertEqual( merged.y, 2.0 ); 738 assertEqual( merged.colour, "black" ); 739 assertEqual( merged.label, "Point" ); 740 } 741 742 unittest 743 { 744 import std.range : walkLength; 745 auto m = [DefaultValues].mergeRange( Aes!(double[], "x")([1.0])); 746 assertEqual( m.walkLength, 1); 747 } 748 749 /// 750 unittest 751 { 752 struct Point { double x; double y; string label = "Point"; } 753 auto pnt = Point( 1.0, 2.0 ); 754 755 auto merged = DefaultValues.merge( pnt ); 756 assertEqual( merged.x, 1.0 ); 757 assertEqual( merged.y, 2.0 ); 758 assertEqual( merged.colour, "black" ); 759 assertEqual( merged.label, "Point" ); 760 } 761 762 /++ 763 Merge the elements of two ranges. If first is not a range then merge that with each element of the second range. 764 +/ 765 auto mergeRange( R1, R2 )( R1 r1, R2 r2 ) 766 { 767 import std.array : array; 768 import std.range : zip, walkLength, repeat; 769 import std.algorithm : map; 770 static if (isInputRange!R1 && isInputRange!R2) 771 return r1.zip(r2).array.map!((a) => a[0].merge( a[1] ) ); 772 else 773 return zip( r1.repeat(r2.walkLength), 774 r2).array.map!((a) => a[0].merge( a[1] ) ); 775 } 776 777 /// 778 unittest 779 { 780 import std.range : front; 781 782 auto xs = ["a", "b"]; 783 auto ys = ["c", "d"]; 784 auto labels = ["e", "f"]; 785 auto aes = Aes!(string[], "x", string[], "y", string[], "label")(xs, ys, labels); 786 auto nlAes = mergeRange(DefaultValues, aes ); 787 assertEqual(nlAes.front.x, "a"); 788 assertEqual(nlAes.front.label, "e"); 789 assertEqual(nlAes.front.colour, "black"); 790 } 791 792 /// 793 unittest 794 { 795 import std.range : front; 796 797 auto xs = ["a", "b"]; 798 auto ys = ["c", "d"]; 799 auto labels = ["e", "f"]; 800 auto aes = Aes!(string[], "x", string[], "y", string[], "label")(xs, ys, labels); 801 802 auto nlAes = mergeRange(aes, Aes!(NumericLabel!(string[]), "x", 803 NumericLabel!(string[]), "y")(NumericLabel!(string[])(aes.x), 804 NumericLabel!(string[])(aes.y))); 805 806 assertEqual(nlAes.front.x[0], 0); 807 assertEqual(nlAes.front.label, "e"); 808 }