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", 10, 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 size of points in pixels (size_t)) 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 @property bool empty() 267 { 268 if (length == 0) 269 return true; 270 return false; 271 } 272 273 /// 274 size_t length() 275 { 276 import std.algorithm : min; 277 import std.range : walkLength, isInfinite; 278 279 size_t l = size_t.max; 280 foreach (i, type; Types[0 .. $]) 281 { 282 static if (!isInfinite!type) 283 { 284 if (field[i].walkLength < l) 285 l = field[i].walkLength; 286 } 287 } 288 return l; 289 } 290 291 /** 292 * Comparison for equality. 293 */ 294 bool opEquals(R)(R rhs) if (areCompatibleTuples!(typeof(this), R, "==")) 295 { 296 return field[] == rhs.field[]; 297 } 298 299 /// ditto 300 bool opEquals(R)(R rhs) const if (areCompatibleTuples!(typeof(this), R, "==")) 301 { 302 return field[] == rhs.field[]; 303 } 304 305 /** 306 * Comparison for ordering. 307 */ 308 int opCmp(R)(R rhs) if (areCompatibleTuples!(typeof(this), R, "<")) 309 { 310 foreach (i, Unused; Types) 311 { 312 if (field[i] != rhs.field[i]) 313 { 314 return field[i] < rhs.field[i] ? -1 : 1; 315 } 316 } 317 return 0; 318 } 319 320 /// ditto 321 int opCmp(R)(R rhs) const if (areCompatibleTuples!(typeof(this), R, "<")) 322 { 323 foreach (i, Unused; Types) 324 { 325 if (field[i] != rhs.field[i]) 326 { 327 return field[i] < rhs.field[i] ? -1 : 1; 328 } 329 } 330 return 0; 331 } 332 333 /** 334 * Assignment from another tuple. Each element of the source must be 335 * implicitly assignable to the respective element of the target. 336 */ 337 void opAssign(R)(auto ref R rhs) if (areCompatibleTuples!(typeof(this), R, 338 "=")) 339 { 340 import std.algorithm : swap; 341 342 static if (is(R : Tuple!Types) && !__traits(isRef, rhs)) 343 { 344 if (__ctfe) 345 { 346 // Cannot use swap at compile time 347 field[] = rhs.field[]; 348 } 349 else 350 { 351 // Use swap-and-destroy to optimize rvalue assignment 352 swap!(Tuple!Types)(this, rhs); 353 } 354 } 355 else 356 { 357 // Do not swap; opAssign should be called on the fields. 358 field[] = rhs.field[]; 359 } 360 } 361 362 /** 363 * Takes a slice of the tuple. 364 * 365 * Examples: 366 * ---- 367 * Tuple!(int, string, float, double) a; 368 * a[1] = "abc"; 369 * a[2] = 4.5; 370 * auto s = a.slice!(1, 3); 371 * static assert(is(typeof(s) == Tuple!(string, float))); 372 * assert(s[0] == "abc" && s[1] == 4.5); 373 * ---- 374 */ 375 @property ref Tuple!(sliceSpecs!(from, to)) slice(size_t from, size_t to)() @trusted if ( 376 from <= to && to <= Types.length) 377 { 378 return *cast(typeof(return)*)&(field[from]); 379 } 380 381 /// 382 size_t toHash() const nothrow @trusted 383 { 384 size_t h = 0; 385 foreach (i, T; Types) 386 h += typeid(T).getHash(cast(const void*)&field[i]); 387 return h; 388 } 389 390 /** 391 * Converts to string. 392 */ 393 void toString(DG)(scope DG sink) 394 { 395 enum header = typeof(this).stringof ~ "(", footer = ")", separator = ", "; 396 sink(header); 397 foreach (i, Type; Types) 398 { 399 static if (i > 0) 400 { 401 sink(separator); 402 } 403 // TODO: Change this once toString() works for shared objects. 404 static if (is(Type == class) && is(typeof(Type.init) == shared)) 405 { 406 sink(Type.stringof); 407 } 408 else 409 { 410 import std.format : FormatSpec, formatElement; 411 412 FormatSpec!char f; 413 formatElement(sink, field[i], f); 414 } 415 } 416 sink(footer); 417 } 418 419 /// 420 string toString()() 421 { 422 import std.conv : to; 423 424 return this.to!string; 425 } 426 } 427 } 428 429 unittest 430 { 431 auto tup = Aes!(double[], "x", double[], "y", string[], "colour")([0, 1], 432 [2, 1], ["white", "white2"]); 433 auto tup2 = Aes!(double[], "x", double[], "y")([0, 1], [2, 1]); 434 assertEqual(tup.colour, ["white", "white2"]); 435 assertEqual(tup.length, 2); 436 assertEqual(tup2.length, 2); 437 438 tup2.x ~= 0.0; 439 tup2.x ~= 0.0; 440 assertEqual(tup2.length, 2); 441 tup2.y ~= 0.0; 442 assertEqual(tup2.length, 3); 443 } 444 445 /// Basic Aes usage 446 unittest 447 { 448 auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([0, 1], 449 [2, 1], ["white", "white2"]); 450 451 aes.popFront; 452 assertEqual(aes.front.y, 1); 453 assertEqual(aes.front.colour, "white2"); 454 455 auto aes2 = Aes!(double[], "x", double[], "y")([0, 1], [2, 1]); 456 assertEqual(aes2.front.y, 2); 457 458 import std.range : repeat; 459 460 auto xs = repeat(0); 461 auto aes3 = Aes!(typeof(xs), "x", double[], "y")(xs, [2, 1]); 462 463 assertEqual(aes3.front.x, 0); 464 aes3.popFront; 465 aes3.popFront; 466 assertEqual(aes3.empty, true); 467 468 } 469 470 /++ 471 Groups data by colour label etc. 472 473 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. 474 +/ 475 template group(Specs...) 476 { 477 string buildExtractKey() 478 { 479 static if (Specs.length == 0) 480 { 481 import std.typetuple : TypeTuple; 482 alias Specs = TypeTuple!("alpha","colour","label"); 483 } 484 string types = ""; 485 string values = ""; 486 foreach( spec; Specs ) 487 { 488 types ~= "typeof(a." ~ spec ~"),"; 489 values ~= "a." ~ spec ~","; 490 } 491 string str = "auto extractKey(T)(T a) 492 { return Tuple!(" ~ types[0..$-1] ~ ")(" ~ values[0..$-1] ~ "); }"; 493 return str; 494 } 495 496 auto group(AES)(AES aes) 497 { 498 import std.stdio; 499 mixin(buildExtractKey()); 500 501 // Attach all default fields 502 auto merged = DefaultValues.mergeRange( aes ); 503 extractKey( merged.front ); 504 505 typeof(merged.front())[][typeof(extractKey(merged.front))] grouped; 506 507 // Extract keys for aa and store in aa. Return the values of the aa 508 foreach( tup; merged ) 509 { 510 auto key = extractKey(tup); 511 if (key in grouped) 512 grouped[key] ~= tup; 513 else 514 grouped[key] = [tup]; 515 } 516 return grouped.values; 517 } 518 } 519 520 /// 521 unittest 522 { 523 import std.range : walkLength; 524 auto aes = Aes!(double[], "x", string[], "colour", double[], "alpha") 525 ([0,1,2,3], ["a","a","b","b"], [0,1,0,1]); 526 527 assertEqual(group!("colour","alpha")(aes).walkLength,4); 528 assertEqual(group!("alpha")(aes).walkLength,2); 529 530 assertEqual(group(aes).walkLength,4); 531 } 532 533 /// 534 unittest 535 { 536 auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([1.0, 537 2.0, 1.1], [3.0, 1.5, 1.1], ["a", "b", "a"]); 538 539 import std.range : walkLength, front, popFront; 540 541 auto grouped = aes.group; 542 assertEqual(grouped.walkLength, 2); 543 assertEqual(grouped.front.walkLength, 2); 544 grouped.popFront; 545 assertEqual(grouped.front.walkLength, 1); 546 } 547 548 /// 549 import std.range : isInputRange; 550 551 /// 552 struct NumericLabel(T) if (isInputRange!T) 553 { 554 import std.range : ElementType; 555 import std.traits : isNumeric; 556 557 alias E = ElementType!T; 558 559 /// 560 this(T range) 561 { 562 original = range; 563 } 564 565 /// 566 @property auto front() 567 { 568 import std.typecons : Tuple; 569 import std.range : front; 570 import std.conv : to; 571 572 static if (isNumeric!E) 573 return Tuple!(double, string)(original.front.to!double, original.front.to!string); 574 else 575 { 576 if (original.front !in fromLabelMap) 577 { 578 fromLabelMap[original.front] = fromLabelMap.length.to!double; 579 //toLabelMap[fromLabelMap[original.front]] 580 // = original.front; 581 } 582 return Tuple!(double, string)(fromLabelMap[original.front], original.front.to!string, 583 ); 584 } 585 } 586 587 /// 588 void popFront() 589 { 590 import std.range : popFront; 591 592 original.popFront; 593 } 594 595 /// 596 @property bool empty() 597 { 598 import std.range : empty; 599 600 return original.empty; 601 } 602 603 /// 604 @property bool numeric() 605 { 606 static if (isNumeric!E) 607 return true; 608 else 609 return false; 610 } 611 612 private: 613 T original; 614 //E[double] toLabelMap; 615 double[E] fromLabelMap; 616 } 617 618 unittest 619 { 620 import std.stdio : writeln; 621 import std.array : array; 622 import std.algorithm : map; 623 624 auto num = NumericLabel!(double[])([0.0, 0.1, 1.0, 0.0]); 625 assertEqual(num.map!((a) => a[0]).array, [0.0, 0.1, 1.0, 0.0]); 626 assertEqual(num.map!((a) => a[1]).array, ["0", "0.1", "1", "0"]); 627 auto strs = NumericLabel!(string[])(["a", "c", "b", "a"]); 628 assertEqual(strs.map!((a) => a[0]).array, [0, 1, 2.0, 0.0]); 629 assertEqual(strs.map!((a) => a[1]).array, ["a", "c", "b", "a"]); 630 } 631 632 unittest 633 { 634 import painlesstraits; 635 636 auto t = Tuple!(double,"x")(1.0); 637 638 static assert(isFieldOrProperty!(t.x)); 639 } 640 641 /++ 642 Merge two types by their members. 643 644 If it has similar named members, then it uses the second one. 645 646 returns a named Tuple (or Aes) with all the members and their values. 647 +/ 648 template merge(T, U) 649 { 650 import std.traits; 651 import painlesstraits; 652 auto generateCode() 653 { 654 import std.string : split; 655 string typing = "Tuple!("; 656 //string typing = T.stringof.split("!")[0] ~ "!("; 657 //string typingU = U.stringof.split("!")[0] ~ "!("; 658 string variables = "("; 659 foreach (name; __traits(allMembers, U)) 660 { 661 static if (__traits(compiles, isFieldOrProperty!( 662 __traits(getMember, U, name))) 663 && __traits(compiles, ( in U u ) { 664 auto a = __traits(getMember, u, name); 665 Tuple!(typeof(a),name)(a); } ) 666 && isFieldOrProperty!(__traits(getMember,U,name)) 667 && name[0] != "_"[0] ) 668 { 669 typing ~= "typeof(other." ~ name ~ "),\"" ~ name ~ "\","; 670 variables ~= "other." ~ name ~ ","; 671 } 672 } 673 674 foreach (name; __traits(allMembers, T)) 675 { 676 static if (__traits(compiles, isFieldOrProperty!( 677 __traits(getMember, T, name))) 678 && __traits(compiles, ( in T u ) { 679 auto a = __traits(getMember, u, name); 680 Tuple!(typeof(a),name)(a); } ) 681 && isFieldOrProperty!(__traits(getMember,T,name)) 682 && name[0] != "_"[0] ) 683 { 684 bool contains = false; 685 foreach (name2; __traits(allMembers, U)) 686 { 687 if (name == name2) 688 contains = true; 689 } 690 if (!contains) 691 { 692 typing ~= "typeof(base." ~ name ~ "),\"" ~ name ~ "\","; 693 variables ~= "base." ~ name ~ ","; 694 } 695 } 696 } 697 return "return " ~ typing[0 .. $ - 1] ~ ")" ~ variables[0 .. $ - 1] ~ ");"; 698 } 699 700 auto merge(T base, U other) 701 { 702 mixin(generateCode()); 703 } 704 } 705 706 /// 707 unittest 708 { 709 import std.range : front; 710 711 auto xs = ["a", "b"]; 712 auto ys = ["c", "d"]; 713 auto labels = ["e", "f"]; 714 auto aes = Aes!(string[], "x", string[], "y", string[], "label")(xs, ys, labels); 715 716 auto nlAes = merge(aes, Aes!(NumericLabel!(string[]), "x", 717 NumericLabel!(string[]), "y")(NumericLabel!(string[])(aes.x), 718 NumericLabel!(string[])(aes.y))); 719 720 assertEqual(nlAes.x.front[0], 0); 721 assertEqual(nlAes.label.front, "e"); 722 } 723 724 unittest 725 { 726 auto pnt = Tuple!(double, "x", double, "y", string, "label" )( 1.0, 2.0, "Point" ); 727 auto merged = DefaultValues.merge( pnt ); 728 assertEqual( merged.x, 1.0 ); 729 assertEqual( merged.y, 2.0 ); 730 assertEqual( merged.colour, "black" ); 731 assertEqual( merged.label, "Point" ); 732 } 733 734 unittest 735 { 736 import std.range : walkLength; 737 auto m = [DefaultValues].mergeRange( Aes!(double[], "x")([1.0])); 738 assertEqual( m.walkLength, 1); 739 } 740 741 /// 742 unittest 743 { 744 struct Point { double x; double y; string label = "Point"; } 745 auto pnt = Point( 1.0, 2.0 ); 746 747 auto merged = DefaultValues.merge( pnt ); 748 assertEqual( merged.x, 1.0 ); 749 assertEqual( merged.y, 2.0 ); 750 assertEqual( merged.colour, "black" ); 751 assertEqual( merged.label, "Point" ); 752 } 753 754 /++ 755 Merge the elements of two ranges. If first is not a range then merge that with each element of the second range. 756 +/ 757 auto mergeRange( R1, R2 )( R1 r1, R2 r2 ) 758 { 759 import std.array : array; 760 import std.range : zip, walkLength, repeat; 761 import std.algorithm : map; 762 static if (isInputRange!R1 && isInputRange!R2) 763 return r1.zip(r2).array.map!((a) => a[0].merge( a[1] ) ); 764 else 765 return zip( r1.repeat(r2.walkLength), 766 r2).array.map!((a) => a[0].merge( a[1] ) ); 767 } 768 769 /// 770 unittest 771 { 772 import std.range : front; 773 774 auto xs = ["a", "b"]; 775 auto ys = ["c", "d"]; 776 auto labels = ["e", "f"]; 777 auto aes = Aes!(string[], "x", string[], "y", string[], "label")(xs, ys, labels); 778 auto nlAes = mergeRange(DefaultValues, aes ); 779 assertEqual(nlAes.front.x, "a"); 780 assertEqual(nlAes.front.label, "e"); 781 assertEqual(nlAes.front.colour, "black"); 782 } 783 784 /// 785 unittest 786 { 787 import std.range : front; 788 789 auto xs = ["a", "b"]; 790 auto ys = ["c", "d"]; 791 auto labels = ["e", "f"]; 792 auto aes = Aes!(string[], "x", string[], "y", string[], "label")(xs, ys, labels); 793 794 auto nlAes = mergeRange(aes, Aes!(NumericLabel!(string[]), "x", 795 NumericLabel!(string[]), "y")(NumericLabel!(string[])(aes.x), 796 NumericLabel!(string[])(aes.y))); 797 798 assertEqual(nlAes.front.x[0], 0); 799 assertEqual(nlAes.front.label, "e"); 800 }