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 /// 558 struct NumericLabel(T) if (isInputRange!T) 559 { 560 import std.range : ElementType; 561 import std.traits : isNumeric; 562 563 alias E = ElementType!T; 564 565 /// 566 this(T range) 567 { 568 original = range; 569 } 570 571 /// 572 @property auto front() 573 { 574 import std.typecons : Tuple; 575 import std.range : front; 576 import std.conv : to; 577 578 static if (isNumeric!E) 579 return Tuple!(double, string)(original.front.to!double, original.front.to!string); 580 else 581 { 582 if (original.front !in fromLabelMap) 583 { 584 fromLabelMap[original.front] = fromLabelMap.length.to!double; 585 //toLabelMap[fromLabelMap[original.front]] 586 // = original.front; 587 } 588 return Tuple!(double, string)(fromLabelMap[original.front], original.front.to!string, 589 ); 590 } 591 } 592 593 /// 594 void popFront() 595 { 596 import std.range : popFront; 597 598 original.popFront; 599 } 600 601 /// 602 @property bool empty() 603 { 604 import std.range : empty; 605 606 return original.empty; 607 } 608 609 /// 610 @property bool numeric() 611 { 612 static if (isNumeric!E) 613 return true; 614 else 615 return false; 616 } 617 618 private: 619 T original; 620 //E[double] toLabelMap; 621 double[E] fromLabelMap; 622 } 623 624 unittest 625 { 626 import std.stdio : writeln; 627 import std.array : array; 628 import std.algorithm : map; 629 630 auto num = NumericLabel!(double[])([0.0, 0.1, 1.0, 0.0]); 631 assertEqual(num.map!((a) => a[0]).array, [0.0, 0.1, 1.0, 0.0]); 632 assertEqual(num.map!((a) => a[1]).array, ["0", "0.1", "1", "0"]); 633 auto strs = NumericLabel!(string[])(["a", "c", "b", "a"]); 634 assertEqual(strs.map!((a) => a[0]).array, [0, 1, 2.0, 0.0]); 635 assertEqual(strs.map!((a) => a[1]).array, ["a", "c", "b", "a"]); 636 } 637 638 unittest 639 { 640 import painlesstraits; 641 642 auto t = Tuple!(double,"x")(1.0); 643 644 static assert(isFieldOrProperty!(t.x)); 645 } 646 647 /++ 648 Merge two types by their members. 649 650 If it has similar named members, then it uses the second one. 651 652 returns a named Tuple (or Aes) with all the members and their values. 653 +/ 654 template merge(T, U) 655 { 656 import std.traits; 657 import painlesstraits; 658 auto generateCode() 659 { 660 import std.string : split; 661 string typing = "Tuple!("; 662 //string typing = T.stringof.split("!")[0] ~ "!("; 663 //string typingU = U.stringof.split("!")[0] ~ "!("; 664 string variables = "("; 665 foreach (name; __traits(allMembers, U)) 666 { 667 static if (__traits(compiles, isFieldOrProperty!( 668 __traits(getMember, U, name))) 669 && __traits(compiles, ( in U u ) { 670 auto a = __traits(getMember, u, name); 671 Tuple!(typeof(a),name)(a); } ) 672 && isFieldOrProperty!(__traits(getMember,U,name)) 673 && name[0] != "_"[0] ) 674 { 675 typing ~= "typeof(other." ~ name ~ "),\"" ~ name ~ "\","; 676 variables ~= "other." ~ name ~ ","; 677 } 678 } 679 680 foreach (name; __traits(allMembers, T)) 681 { 682 static if (__traits(compiles, isFieldOrProperty!( 683 __traits(getMember, T, name))) 684 && __traits(compiles, ( in T u ) { 685 auto a = __traits(getMember, u, name); 686 Tuple!(typeof(a),name)(a); } ) 687 && isFieldOrProperty!(__traits(getMember,T,name)) 688 && name[0] != "_"[0] ) 689 { 690 bool contains = false; 691 foreach (name2; __traits(allMembers, U)) 692 { 693 if (name == name2) 694 contains = true; 695 } 696 if (!contains) 697 { 698 typing ~= "typeof(base." ~ name ~ "),\"" ~ name ~ "\","; 699 variables ~= "base." ~ name ~ ","; 700 } 701 } 702 } 703 return "return " ~ typing[0 .. $ - 1] ~ ")" ~ variables[0 .. $ - 1] ~ ");"; 704 } 705 706 auto merge(T base, U other) 707 { 708 mixin(generateCode()); 709 } 710 } 711 712 /// 713 unittest 714 { 715 import std.range : front; 716 717 auto xs = ["a", "b"]; 718 auto ys = ["c", "d"]; 719 auto labels = ["e", "f"]; 720 auto aes = Aes!(string[], "x", string[], "y", string[], "label")(xs, ys, labels); 721 722 auto nlAes = merge(aes, Aes!(NumericLabel!(string[]), "x", 723 NumericLabel!(string[]), "y")(NumericLabel!(string[])(aes.x), 724 NumericLabel!(string[])(aes.y))); 725 726 assertEqual(nlAes.x.front[0], 0); 727 assertEqual(nlAes.label.front, "e"); 728 } 729 730 unittest 731 { 732 auto pnt = Tuple!(double, "x", double, "y", string, "label" )( 1.0, 2.0, "Point" ); 733 auto merged = DefaultValues.merge( pnt ); 734 assertEqual( merged.x, 1.0 ); 735 assertEqual( merged.y, 2.0 ); 736 assertEqual( merged.colour, "black" ); 737 assertEqual( merged.label, "Point" ); 738 } 739 740 unittest 741 { 742 import std.range : walkLength; 743 auto m = [DefaultValues].mergeRange( Aes!(double[], "x")([1.0])); 744 assertEqual( m.walkLength, 1); 745 } 746 747 /// 748 unittest 749 { 750 struct Point { double x; double y; string label = "Point"; } 751 auto pnt = Point( 1.0, 2.0 ); 752 753 auto merged = DefaultValues.merge( pnt ); 754 assertEqual( merged.x, 1.0 ); 755 assertEqual( merged.y, 2.0 ); 756 assertEqual( merged.colour, "black" ); 757 assertEqual( merged.label, "Point" ); 758 } 759 760 /++ 761 Merge the elements of two ranges. If first is not a range then merge that with each element of the second range. 762 +/ 763 auto mergeRange( R1, R2 )( R1 r1, R2 r2 ) 764 { 765 import std.array : array; 766 import std.range : zip, walkLength, repeat; 767 import std.algorithm : map; 768 static if (isInputRange!R1 && isInputRange!R2) 769 return r1.zip(r2).array.map!((a) => a[0].merge( a[1] ) ); 770 else 771 return zip( r1.repeat(r2.walkLength), 772 r2).array.map!((a) => a[0].merge( a[1] ) ); 773 } 774 775 /// 776 unittest 777 { 778 import std.range : front; 779 780 auto xs = ["a", "b"]; 781 auto ys = ["c", "d"]; 782 auto labels = ["e", "f"]; 783 auto aes = Aes!(string[], "x", string[], "y", string[], "label")(xs, ys, labels); 784 auto nlAes = mergeRange(DefaultValues, aes ); 785 assertEqual(nlAes.front.x, "a"); 786 assertEqual(nlAes.front.label, "e"); 787 assertEqual(nlAes.front.colour, "black"); 788 } 789 790 /// 791 unittest 792 { 793 import std.range : front; 794 795 auto xs = ["a", "b"]; 796 auto ys = ["c", "d"]; 797 auto labels = ["e", "f"]; 798 auto aes = Aes!(string[], "x", string[], "y", string[], "label")(xs, ys, labels); 799 800 auto nlAes = mergeRange(aes, Aes!(NumericLabel!(string[]), "x", 801 NumericLabel!(string[]), "y")(NumericLabel!(string[])(aes.x), 802 NumericLabel!(string[])(aes.y))); 803 804 assertEqual(nlAes.front.x[0], 0); 805 assertEqual(nlAes.front.label, "e"); 806 }