1 /**This file contains all of the plots types in Plot2kill, which can be 2 * drawn on screen using plot2kill.figure.Figure. 3 * 4 * Copyright (C) 2010-2012 David Simcha 5 * 6 * License: 7 * 8 * Boost Software License - Version 1.0 - August 17th, 2003 9 * 10 * Permission is hereby granted, free of charge, to any person or organization 11 * obtaining a copy of the software and accompanying documentation covered by 12 * this license (the "Software") to use, reproduce, display, distribute, 13 * execute, and transmit the Software, and to prepare derivative works of the 14 * Software, and to permit third-parties to whom the Software is furnished to 15 * do so, all subject to the following: 16 * 17 * The copyright notices in the Software and this entire statement, including 18 * the above license grant, this restriction and the following disclaimer, 19 * must be included in all copies of the Software, in whole or in part, and 20 * all derivative works of the Software, unless such copies or derivative 21 * works are solely in the form of machine-executable object code generated by 22 * a source language processor. 23 * 24 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 27 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 28 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 29 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 30 * DEALINGS IN THE SOFTWARE. 31 */ 32 33 module plot2kill.plot; 34 35 import plot2kill.figure, plot2kill.util, plot2kill.hierarchical; 36 import std.random, std.typetuple; 37 38 version(dfl) { 39 public import plot2kill.dflwrapper; 40 } else { 41 public import plot2kill.gtkwrapper; 42 } 43 44 /**Abstract base class for all types of plot objects.*/ 45 abstract class Plot { 46 protected: 47 // Top of the plot. 48 double upperLim = -double.infinity; 49 50 // Bottom of the plot. 51 double lowerLim = double.infinity; 52 53 // Leftmost limit of the plot. 54 double leftLim = double.infinity; 55 56 // Rightmost limit of the plot. 57 double rightLim = -double.infinity; 58 59 string _legendText; 60 61 // Hook in case the plot needs to reset its limits after seeing 62 // the figure it's about to be drawn to. 63 void resetLimits(Figure figure) {} 64 65 package: 66 PlotSize measureLegend(Font legendFont, FigureBase fig) { 67 if(!legendText().length) return PlotSize(0, 0); 68 auto ret = fig.measureText(legendText(), legendFont); 69 ret.width += legendSymbolSize + legendSymbolTextSpace; 70 ret.height = max(ret.height, legendSymbolSize); 71 return PlotSize(ret.width, ret.height); 72 } 73 74 public: 75 // This should be package but for some reason package functions can't 76 // be abstract. 77 abstract void drawLegendSymbol(FigureBase fig, PlotRect where); 78 79 /* Draw the plot on Figure using the rectangular area described by the 80 * integer parameters. 81 */ 82 abstract void drawPlot(Figure, double, double, double, double); 83 84 /** 85 Returns true if this plot type supports legends, otherwise false. 86 */ 87 bool hasLegend() { 88 return true; 89 } 90 91 /**Convenience method that instantiates a Figure object with this plot. 92 * Useful for creating single-plot figures w/o a lot of boilerplate. 93 * 94 * Examples: 95 * --- 96 * auto hist = Histogram([1,2,3,4,5], 3).toFigure; 97 * hist.showAsMain(); 98 * --- 99 */ 100 Figure toFigure() { 101 return new Figure(this); 102 } 103 104 /**Instantiates a Figure object with this plot and also places the default 105 * axes and tick labeling and title for the plot type, if any, on the 106 * Figure. If a plot type has no default labeling, simply forwards to 107 * toFigure(). 108 */ 109 Figure toLabeledFigure() { 110 return toFigure; 111 } 112 113 /**The leftmost point on the plot.*/ 114 double leftMost() { 115 return leftLim; 116 } 117 118 /**The rightmost point on the plot.*/ 119 double rightMost() { 120 return rightLim; 121 } 122 123 /**The topmost point on the plot.*/ 124 double topMost() { 125 return upperLim; 126 } 127 128 /**The bottommost point on the plot.*/ 129 double bottomMost() { 130 return lowerLim; 131 } 132 133 /// 134 string legendText()() { 135 return _legendText; 136 } 137 138 /// 139 This legendText(this This)(string newText) { 140 _legendText = newText; 141 return cast(This) this; 142 } 143 } 144 145 /**A basic bar plot.*/ 146 class BarPlot : Plot { 147 private: 148 double[] _centers; 149 double[] _heights; 150 double[] _lowerErrors; 151 double[] _upperErrors; 152 BarPlot _stackOn; 153 bool _outlineBar = true; 154 155 // Return the bottom of the bar at a given index. This is zero unless 156 // we have a stacked plot. 157 double bottom(size_t index) pure nothrow { 158 return (_stackOn is null) ? 0 : 159 _stackOn.bottom(index) + _stackOn._heights[index]; 160 } 161 162 void fixBounds() { 163 // We should never have both error bars and stacking. The API doesn't 164 // allow this, but double-check here. 165 enforce(_stackOn is null || 166 (_lowerErrors.length == 0 && upperErrors.length == 0)); 167 168 this.leftLim = reduce!min(double.infinity, this.centers) - width / 2; 169 this.rightLim = reduce!max(-double.infinity, this.centers) + width / 2; 170 lowerLim = double.infinity; 171 upperLim = -double.infinity; 172 173 foreach(i, height; _heights) { 174 if(lowerErrors.length > 0) { 175 lowerLim = min(height - lowerErrors[i], lowerLim); 176 } else { 177 lowerLim = min(height + bottom(i), lowerLim); 178 } 179 180 if(upperErrors.length > 0) { 181 upperLim = max(height + upperErrors[i], upperLim); 182 } else { 183 upperLim = max(height + bottom(i), upperLim); 184 } 185 } 186 187 // Don't blend error bars into axes. Handle the case where a boundary 188 // is 0 as a special case, since it would look worse to have the axis 189 // not be exactly zero if all bars are positive or all are negative than 190 // to blend an error bar into an axis. 191 if(upperErrors.length || lowerErrors.length) { 192 immutable pad = 0.01 * (upperLim - lowerLim); 193 if(upperErrors.length && upperLim != 0) { 194 upperLim += pad; 195 } 196 if(lowerErrors.length && lowerLim != 0) { 197 lowerLim -= pad; 198 } 199 } 200 201 if(lowerLim > 0) { 202 lowerLim = 0; 203 } 204 205 if(upperLim < 0) { 206 upperLim = 0; 207 } 208 } 209 210 211 this(double[] centers, double[] heights, double width) { 212 this._centers = centers; 213 this._heights = heights; 214 this.width = width; 215 _barColor = getColor(0, 0, 255); 216 fixBounds(); 217 } 218 219 protected: 220 override void drawPlot( 221 Figure form, 222 double leftMargin, 223 double topMargin, 224 double plotWidth, 225 double plotHeight 226 ) { 227 228 mixin(toPixels); 229 mixin(drawErrorMixin); 230 231 immutable multiplier = plotHeight / (this.upperLim - this.lowerLim); 232 auto brush = form.getBrush(_barColor); 233 scope(exit) doneWith(brush); 234 235 auto blackPen = form.getPen(getColor(0, 0, 0)); 236 scope(exit) doneWith(blackPen); 237 238 foreach(i, center; centers) { 239 immutable zeroPoint = toPixelsY(bottom(i)); 240 immutable height = heights[i]; 241 immutable left = center - width / 2; 242 immutable right = center + width / 2; 243 immutable leftPixels = toPixelsX(left); 244 immutable rightPixels = toPixelsX(right); 245 immutable widthPixels = rightPixels - leftPixels; 246 247 immutable heightPixels = roundTo!int(abs(height) * multiplier); 248 249 immutable startAt = (height > 0) ? 250 zeroPoint - heightPixels : 251 zeroPoint; 252 form.fillClippedRectangle(brush, leftPixels, 253 startAt, widthPixels, heightPixels); 254 255 if(_outlineBar) { 256 form.drawClippedRectangle(blackPen, leftPixels, 257 startAt, widthPixels, heightPixels); 258 } 259 260 // Do error bars. 261 if(lowerErrors.length) { 262 drawErrorBar(blackPen, 263 center, height, height - lowerErrors[i], width / 2); 264 } 265 if(upperErrors.length) { 266 drawErrorBar(blackPen, 267 center, height, height + upperErrors[i], width / 2); 268 } 269 } 270 271 if(lowerLim < 0) { 272 immutable zeroPoint = toPixelsY(0); 273 auto pen = form.getPen(getColor(0, 0, 0), 2); 274 // Draw line across figure at the zero point. 275 form.drawClippedLine(pen, 276 PlotPoint(toPixelsX(leftLim), zeroPoint), 277 PlotPoint(toPixelsX(rightLim), zeroPoint) 278 ); 279 } 280 } 281 282 override void drawLegendSymbol(FigureBase fig, PlotRect where) { 283 drawFillLegend(_barColor, fig, where); 284 } 285 286 Color _barColor; 287 288 public: 289 /**Controls the color of the bar. Defaults to blue.*/ 290 final Color barColor()() { 291 return _barColor; 292 } 293 294 /// Setter 295 final This barColor(this This)(Color newColor) { 296 _barColor = newColor; 297 return cast(This) this; 298 } 299 300 /**Controls whether each bar is outlined in a black rectange.*/ 301 final bool outlineBar()() { 302 return _outlineBar; 303 } 304 305 final This outlineBar(this This)(bool shouldOutline) { 306 _outlineBar = shouldOutline; 307 return cast(This) this; 308 } 309 310 /// 311 immutable double width; 312 313 /**Create a BarPlot. centers and heights must be input ranges with elements 314 * implicitly convertible to double. width determines the width of each 315 * bar relative to the X-axis scale and must be greater than 0. 316 */ 317 static BarPlot opCall(R1, R2)(R1 centers, R2 heights, double width) 318 if(isInputRange!R1 && is(ElementType!R1 : double) && 319 isInputRange!R2 && is(ElementType!R2 : double)) { 320 321 auto c = toDoubleArray(centers); 322 auto h = toDoubleArray(heights); 323 324 enforce(c.length == h.length, 325 "Centers and heights must be same length for bar plot."); 326 327 enforce(width > 0, "Width must be >0 for bar plot."); 328 auto ret = new typeof(return)(c, h, width); 329 return ret; 330 } 331 332 /** 333 Create a BarPlot with error bars. lowerErrors and upperErrors 334 must be input ranges with elements implicitly convertible to double for 335 error bars to be shown. Any other value, such as null or 0, will result 336 in no error bars being shown. Therefore, to only show, for example, 337 upper erros, simply pass in null or 0 for the lower errors. 338 339 To draw symmetric error bars, simply pass in the same range for 340 lowerErrors and upperErrors. However, note that if you do this, 341 the range will need to be a forward range, not an input range. 342 */ 343 static BarPlot opCall(R1, R2, R3, R4) 344 (R1 centers, R2 heights, double width, R3 lowerErrors, R4 upperErrors) 345 if(isInputRange!R1 && is(ElementType!R1 : double) && 346 isInputRange!R2 && is(ElementType!R2 : double)) { 347 348 auto ret = opCall(centers, heights, width); 349 static if(isForwardRange!R3) { 350 ret._lowerErrors = toDoubleArray(lowerErrors.save); 351 } else static if(isInputRange!R3) { 352 ret._lowerErrors = toDoubleArray(lowerErrors); 353 } 354 355 static if(isForwardRange!R4) { 356 ret._upperErrors = toDoubleArray(upperErrors.save); 357 } else static if(isInputRange!R4) { 358 ret._upperErrors = toDoubleArray(upperErrors); 359 } 360 361 enforce(ret.upperErrors.length == 0 || ret.upperErrors.length == 362 ret.centers.length, "Length of upperErrors must equal number of bars."); 363 enforce(ret.lowerErrors.length == 0 || ret.lowerErrors.length == 364 ret.centers.length, "Length of lowerErrors must equal number of bars."); 365 366 ret.fixBounds(); 367 return ret; 368 } 369 370 /** 371 Create a BarPlot that is to be stacked on top of this one. For a 372 convenience function for creating such a plot, see stackedBar(). 373 374 Warning: Creating a cycle (two BarPlots mutually on top of each other) 375 will cause infinite loops, stack overflows and otherwise bad 376 things. 377 */ 378 BarPlot stack(R)(R heights) { 379 auto ret = new typeof(return) 380 (_centers, toDoubleArray(heights), width); 381 ret._stackOn = this; 382 ret.fixBounds(); 383 return ret; 384 } 385 386 /**Scale this object by a factor of scaleFactor in the X direction. 387 * This is useful for getting it onto the same scale as another plot. 388 */ 389 void scaleCenters(double scaleFactor) { 390 enforce(isFinite(scaleFactor), "Can't scale by infinity or NaN."); 391 _centers[] *= scaleFactor; 392 fixBounds(); 393 } 394 395 /**Scale this object by a factor of scaleFactor in the Y direction. 396 * This is useful for getting it onto the same scale as another plot.*/ 397 void scaleHeights(double scaleFactor) { 398 enforce(isFinite(scaleFactor), "Can't scale by infinity or NaN."); 399 _heights[] *= scaleFactor; 400 fixBounds(); 401 } 402 403 /**Shift this graph by shiftBy units in the X direction. 404 * This is useful for getting it onto the same scale as another plot. 405 */ 406 void shiftCenters(double shiftBy) { 407 enforce(isFinite(shiftBy), "Can't shift by infinity or NaN."); 408 _centers[] += shiftBy; 409 fixBounds(); 410 } 411 412 /**Get the centers of the bars.*/ 413 final const(double)[] centers() { 414 return _centers; 415 } 416 417 /**Get the heights of the bars.*/ 418 final const(double)[] heights() { 419 return _heights; 420 } 421 422 /// 423 final const(double)[] lowerErrors() { 424 return _lowerErrors; 425 } 426 427 /// 428 final const(double)[] upperErrors() { 429 return _upperErrors; 430 } 431 432 /**The default labeling includes each center receiving its own x tick 433 * label if there are <= 10 bars on the graph. 434 */ 435 override Figure toLabeledFigure() { 436 auto ret = toFigure; 437 if(centers.length <= 10) { 438 ret.xTickLabels(centers); 439 } 440 441 return ret; 442 } 443 } 444 445 private template isRoR(T) { 446 enum isRoR = isInputRange!T && isInputRange!(ElementType!T); 447 } 448 449 /** 450 Create an array of bar plots that, when inserted into a Figure, will effectively 451 become a grouped bar plot. 452 453 Parameters: 454 455 centers: 456 An input range of the overall center of each group of bars. 457 458 data: 459 A range of ranges, with one range for each bar color. This range should have 460 one number for each group. 461 462 width: 463 The combined width of all of the bars for each group. 464 465 legendText: 466 An array of strings, one for each bar color, or null if no legend is desired. 467 468 colors: 469 An array of colors, one for each bar. If none is provided, the default 470 colors are used. These are, in order, blue, red, green, black, orange, 471 and purple. If more colors are needed, they are generated using a random 472 number generator with a deterministic seed. 473 474 Examples: 475 --- 476 477 // Make a plot with three groups: One for "In Meeting", one for "On Phone", 478 // and one for "Coding". The plot will also have two bar colors: One for 479 // "Without Caffeine" and one for "With Caffeine". 480 auto withoutCaffeine = [8, 6, 3]; 481 auto withCaffeine = [5, 3, 1]; 482 auto sleepinessPlot = groupedBar( 483 iota(3), [withoutCaffeine, withCaffeine], 0.6, 484 ["W/o Caffeine", "W/ Caffeine"], 485 [getColor(64, 64, 255), getColor(255, 64, 64)] 486 ); 487 auto sleepinessFig = Figure(sleepinessPlot) 488 .title("Sleepiness Survey") 489 .yLabel("Sleepiness Rating") 490 .xLabel("Activity") 491 .legendLocation(LegendLocation.right) 492 .horizontalGrid(true) 493 .xTickLabels( 494 iota(3), 495 ["In Meeting", "On Phone", "Coding"] 496 ); 497 --- 498 */ 499 BarPlot[] groupedBar(R1, R2)( 500 R1 centers, 501 R2 data, 502 double width, 503 string[] legendText = null, 504 Color[] colors = null 505 ) 506 if(isInputRange!R1 && isRoR!R2) { 507 return groupedBar(centers, data, (double[][]).init, (double[][]).init, 508 width, legendText, colors); 509 } 510 511 /** 512 Create a grouped bar plot with error bars. lowerErrors and upperErrors 513 must either have the same dimensions as data or be empty. 514 */ 515 BarPlot[] groupedBar(R1, R2, R3, R4)( 516 R1 centers, 517 R2 data, 518 R3 lowerErrors, 519 R4 upperErrors, 520 double width, 521 string[] legendText = null, 522 Color[] colors = null 523 ) 524 if(isInputRange!R1 && isRoR!R2 && isRoR!R3 && isRoR!R4) { 525 return groupStackImpl(CompoundBar.group, centers, data, lowerErrors, 526 upperErrors, width, legendText, colors); 527 } 528 529 /** 530 Create a stacked bar plot. The usage mechanics are identical to those of the 531 no error bar overload of groupedBar(). 532 533 Examples: 534 --- 535 // Stack coffee and tea consumption on top of each other. 536 Figure( 537 stackedBar(iota(3), [[5, 3, 1], [1, 2, 3]], 0.6, 538 ["Coffee", "Tea"] 539 ) 540 ).legendLocation(LegendLocation.right) 541 .title("Caffeine Consumption") 542 .xLabel("Time of Day") 543 .xTickLabels(iota(3), ["Morning", "Afternoon", "Evening"]) 544 .yLabel("Beverages") 545 .showAsMain(); 546 --- 547 */ 548 BarPlot[] stackedBar(R1, R2)( 549 R1 centers, 550 R2 data, 551 double width, 552 string[] legendText = null, 553 Color[] colors = null 554 ) 555 if(isInputRange!R1 && isRoR!R2) { 556 return groupStackImpl(CompoundBar.stack, centers, data, (double[][]).init, 557 (double[][]).init, width, legendText, colors); 558 } 559 560 private enum CompoundBar { 561 group, 562 stack 563 } 564 565 private BarPlot[] groupStackImpl(R1, R2, R3, R4)( 566 CompoundBar whatToDo, 567 R1 centers, 568 R2 data, 569 R3 lowerErrors, 570 R4 upperErrors, 571 double width, 572 string[] legendText = null, 573 Color[] colors = null 574 ) { 575 if(whatToDo == CompoundBar.stack) { 576 enforce(lowerErrors.length == 0 && upperErrors.length == 0); 577 } 578 579 auto centerArr = toDoubleArray(centers); 580 auto dataArr = array(map!toDoubleArray(data)); 581 auto lerrArr = array(map!toDoubleArray(lowerErrors)); 582 auto uerrArr = array(map!toDoubleArray(upperErrors)); 583 584 foreach(elem; tuple(lerrArr, uerrArr, legendText, colors)) { 585 enforce(elem.empty || elem.length == dataArr.length, 586 "Range length mismatch in groupedBar."); 587 } 588 589 if(!colors.length) { 590 colors = [ 591 getColor(0, 0, 255), getColor(255, 0, 0), getColor(0, 255, 0), 592 getColor(0, 0, 0), getColor(255, 128, 0), getColor(255, 0, 255) 593 ]; 594 595 // If we need more colors, generate them "randomly" but from a 596 // deterministic seed. 597 auto gen = Random(31415); 598 while(colors.length < dataArr.length) { 599 colors ~= getColor( 600 uniform!"[]"(cast(ubyte) 0, cast(ubyte) 255, gen), 601 uniform!"[]"(cast(ubyte) 0, cast(ubyte) 255, gen), 602 uniform!"[]"(cast(ubyte) 0, cast(ubyte) 255, gen) 603 ); 604 } 605 } 606 607 BarPlot[] ret; 608 foreach(int groupIndex, group; dataArr) { 609 BarPlot plot; 610 611 enforce(group.length == centerArr.length, 612 "Each group's length must be equal to centers.length for " 613 ~ "grouped and stacked bar plots." 614 ); 615 616 if(whatToDo == CompoundBar.group) { 617 immutable groupWidth = width / dataArr.length; 618 619 // Shift groupCenters over from overall centers. 620 auto groupCenters = centerArr.dup; 621 immutable offset = (groupIndex + 0.5) * groupWidth; 622 groupCenters[] += offset - width / 2; 623 624 if(lowerErrors.length && upperErrors.length) { 625 plot = BarPlot(NoCopy(groupCenters), NoCopy(group), groupWidth, 626 NoCopy(lerrArr[groupIndex]), NoCopy(uerrArr[groupIndex]) 627 ); 628 } else { 629 plot = BarPlot(NoCopy(groupCenters), NoCopy(group), groupWidth); 630 } 631 } else if(whatToDo == CompoundBar.stack) { 632 if(groupIndex == 0) { 633 plot = BarPlot(NoCopy(centerArr), NoCopy(group), width); 634 } else { 635 plot = ret[$ - 1].stack(NoCopy(group)); 636 } 637 } else { 638 assert(0); 639 } 640 641 plot.barColor(colors[groupIndex]); 642 643 if(legendText.length) { 644 plot.legendText(legendText[groupIndex]); 645 } 646 647 ret ~= plot; 648 } 649 650 return ret; 651 } 652 653 /**Determine behavior for elements outside of a fixed-border histogram's bounds. 654 */ 655 enum OutOfBounds { 656 /** Throw throws an exception.*/ 657 throwException, 658 659 /**Ignore simply skips the number.*/ 660 ignore, 661 662 // For backwards compatibility, will eventually be removed. 663 Throw = throwException, 664 Ignore=ignore 665 } 666 667 /**Controls whether a histogram plots counts or probability.*/ 668 enum HistType { 669 /// The Y-axis should be counts. 670 counts, 671 672 /// The Y-axis should be probabilities. 673 probability, 674 675 // For backwards compatibility, will eventually be removed. 676 Probability = probability, 677 Counts = counts 678 } 679 680 /**A class for plotting regular (equal-width) histograms.*/ 681 class Histogram : Plot { 682 683 private double binWidth; 684 685 private uint[] binCounts; 686 private uint nElem; 687 688 private HistType countsOrProbs; 689 690 private this() { 691 _barColor = getColor(0, 0, 255); 692 } 693 694 private bool isCumulative = false; 695 696 protected override void drawPlot( 697 Figure form, 698 double leftMargin, 699 double topMargin, 700 double plotWidth, 701 double plotHeight 702 ) { 703 uint[] binCounts = this.binCounts; 704 if(isCumulative) { 705 binCounts = binCounts.dup; 706 foreach(i; 1..binCounts.length) { 707 binCounts[i] += binCounts[i - 1]; 708 } 709 } 710 711 immutable maxCount = reduce!max(0U, binCounts); 712 immutable multiplier = plotHeight / cast(double) maxCount; 713 714 immutable binWidth = cast(double) plotWidth / nBin; 715 immutable bottom = plotHeight + topMargin; 716 717 auto blackPen = form.getPen(getColor(0, 0, 0), 1); 718 scope(exit) doneWith(blackPen); 719 720 auto brush = form.getBrush(_barColor); 721 scope(exit) doneWith(brush); 722 723 double horizPos = leftMargin; 724 double lastPosPixels = leftMargin; 725 foreach(i, count; binCounts) { 726 // Most of the complexity of this loop body is for making the bin 727 // boundaries accurate in the context of having to round to 728 // pixels. 729 immutable barHeight = multiplier * count; 730 immutable horizPixels = min(roundTo!int(horizPos), lastPosPixels); 731 immutable stopAt = horizPos + binWidth; 732 immutable thisBinWidth = max(1.0, stopAt - horizPixels); 733 734 form.fillClippedRectangle(brush, lastPosPixels, 735 bottom - barHeight, thisBinWidth, barHeight); 736 form.drawClippedRectangle(blackPen, lastPosPixels, 737 bottom - barHeight, thisBinWidth, barHeight); 738 horizPos += binWidth; 739 lastPosPixels = horizPixels + thisBinWidth; 740 } 741 } 742 743 protected override void drawLegendSymbol(FigureBase fig, PlotRect where) { 744 drawFillLegend(_barColor, fig, where); 745 } 746 747 private void fixBounds() { 748 if(isCumulative) { 749 if(countsOrProbs == HistType.Probability) { 750 upperLim = 1; 751 } else { 752 upperLim = nElem; 753 } 754 } else{ 755 if(countsOrProbs == HistType.Probability) { 756 upperLim = reduce!max(0U, binCounts) / cast(double) nElem; 757 } else { 758 upperLim = reduce!max(0U, binCounts); 759 } 760 } 761 } 762 763 private OutOfBounds outOfBoundsBehavior = OutOfBounds.Throw; 764 765 private Color _barColor; 766 767 /**Controls the color of the bar. Defaults to blue.*/ 768 final Color barColor()() { 769 return _barColor; 770 } 771 772 /// Setter 773 final This barColor(this This)(Color newColor) { 774 _barColor = newColor; 775 return cast(This) this; 776 } 777 778 779 /// The number of bins this histogram contains. 780 final uint nBin() const pure nothrow { 781 return cast(uint) binCounts.length; 782 } 783 784 /**Factory method to instantiate this class. nums must be a forward range 785 * with elements implicitly convertible to double. nBin specifies how many 786 * bins the histogram should contain. 787 */ 788 static Histogram opCall(R)(R nums, uint nBin) 789 if(isForwardRange!R && is(ElementType!R : double)) { 790 double leftLim = double.infinity, rightLim = -double.infinity; 791 foreach(num; nums.save) { 792 leftLim = min(leftLim, num); 793 rightLim = max(rightLim, num); 794 } 795 796 return Histogram(nums, nBin, leftLim, rightLim); 797 } 798 799 /**Factory method to instantiate this class with predetermined limits. 800 * This allows nums to be an input range instead of a forward range, since 801 * no pass is necessary to compute the limits. 802 * 803 * This function both obeys and permanently sets whatever bounds behavior 804 * is specified (throwing or ignoring on out of bounds numbers). The 805 * default behavior is to throw. Rationale: Errors should only pass 806 * silently if explicitly silenced. 807 */ 808 static Histogram opCall(R)( 809 R nums, 810 uint nBin, 811 double leftLim, 812 double rightLim, 813 OutOfBounds outOfBoundsBehavior = OutOfBounds.throwException 814 ) if(isInputRange!R && is(ElementType!R : double)) { 815 auto ret = Histogram(nBin, leftLim, rightLim, outOfBoundsBehavior); 816 817 foreach(num; nums) { 818 ret.put(num); 819 } 820 821 return ret; 822 } 823 824 /**Create an empty histogram with pre-specified bounds, which will be 825 * filled with data using the put method. 826 * 827 * Note: The only reason this is a template is because of bugs in 828 * overloading non-templated functions agsinst templated functions. 829 */ 830 static Histogram opCall(I)( 831 I nBin, 832 double leftLim, 833 double rightLim, 834 OutOfBounds outOfBoundsBehavior = OutOfBounds.Throw 835 ) if(isIntegral!I) { 836 auto ret = new Histogram; 837 ret.outOfBoundsBehavior = outOfBoundsBehavior; 838 839 enforce(rightLim > leftLim, 840 "Cannot create a histogram w/ upper lim <= lower lim."); 841 enforce(nBin > 1, "Cannot create a histogram w/ <2 bins."); 842 843 immutable binWidth = (rightLim - leftLim) / nBin; 844 ret.binWidth = binWidth; 845 846 ret.leftLim = leftLim; 847 ret.rightLim = rightLim; 848 ret.binCounts.length = to!uint(nBin); 849 ret.lowerLim = 0; 850 ret.upperLim = 0; 851 852 return ret; 853 } 854 855 /**Add a number to the histogram on the fly.*/ 856 This put(this This)(double num) { 857 if(outOfBoundsBehavior == OutOfBounds.Throw) { 858 enforce(num >= leftLim && num <= rightLim, text( 859 "Number out of bounds for histogram. Got: ", num, 860 ", expected between ", leftLim, " and ", rightLim, ".")); 861 } else { 862 if(!(num >= leftLim && num <= rightLim)) { 863 return cast(This) this; 864 } 865 } 866 867 uint bin; 868 bin = to!uint((num - leftLim) / binWidth); 869 if(bin == nBin) { // Edge case. 870 bin--; 871 } 872 873 binCounts[bin]++; 874 nElem++; 875 876 if(countsOrProbs == HistType.Counts) { 877 if(isCumulative) { 878 upperLim = nElem; 879 } else if(binCounts[bin] > upperLim) { 880 upperLim = binCounts[bin]; 881 } 882 } else if(!isCumulative) { 883 immutable binProb = binCounts[bin] / cast(double) nElem; 884 if(binProb > upperLim) { 885 upperLim = binProb; 886 } 887 } 888 889 return cast(This) this; 890 } 891 892 /**Add the contents of another Histogram to this one. The boundaries and 893 * numbers of bins must be the same. This histogram's settings are 894 * retained. 895 */ 896 This put(this This)(const Histogram rhs) { 897 if(rhs is null) { 898 return cast(This) this; 899 } 900 901 enforce(rhs.leftLim == this.leftLim && rhs.rightLim == this.rightLim, 902 "Boundaries must be the same to combine histograms."); 903 binCounts[] += rhs.binCounts[]; 904 nElem += rhs.nElem; 905 906 fixBounds(); 907 return cast(This) this; 908 } 909 910 /**Assumes the LineGraph input is a plot of a PDF that this histogram is 911 * supposed to approximate, and scales the Y axis of the LineGraph 912 * accordingly so that both appear on the same scale. 913 * 914 * If this Histogram is cumulative, assumes that the input LineGraph is 915 * a CDF instead. 916 */ 917 This scaleDistributionFunction(this This)(LineGraph g) { 918 if(isCumulative) { 919 if(countsOrProbs == HistType.Counts) { 920 g.scaleY(nElem); 921 } 922 // Don't need to do anything if this is a probability histogram. 923 } else { 924 double scaleFactor = (rightLim - leftLim) / nBin; 925 if(countsOrProbs == HistType.Counts) { 926 scaleFactor *= nElem; 927 } 928 929 g.scaleY(scaleFactor); 930 } 931 932 return cast(This) this; 933 } 934 935 /**Determine whether this object throws or ignores if it receives a number 936 * outside its bounds via put. 937 */ 938 This boundsBehavior(this This)(OutOfBounds behavior) { 939 outOfBoundsBehavior = behavior; 940 return cast(This) this; 941 } 942 943 /**Set whether this histogram displays counts or probabilities.*/ 944 This histType(this This)(HistType newType) { 945 countsOrProbs = newType; 946 fixBounds(); 947 return cast(This) this; 948 } 949 950 /**Determines whether this histogram is cumulative.*/ 951 This cumulative(this This)(bool newVal) { 952 isCumulative = newVal; 953 fixBounds(); 954 return cast(This) this; 955 } 956 } 957 958 /**Creates a histogram with equal frequency binning instead of equal width 959 * binning. The scale of a FrequencyHistogram is the probability density scale. 960 * 961 * Note that equal frequency binning doesn't work with discrete 962 * distributions where probability density may be infinite at a point. 963 * Therefore, if a probability density is calculated to be infinite, this 964 * class will throw. 965 * 966 * 967 */ 968 class FrequencyHistogram : Plot { 969 private double[] binWidths; 970 private double elemsPerBin; 971 972 private Color _barColor; 973 974 this() { 975 _barColor = getColor(0, 0, 255); 976 } 977 978 protected override void drawPlot( 979 Figure form, 980 double leftMargin, 981 double topMargin, 982 double plotWidth, 983 double plotHeight 984 ) { 985 986 mixin(toPixels); 987 988 immutable multiplier = plotHeight / (this.upperLim - this.lowerLim); 989 immutable zeroPoint = toPixelsY(0); 990 auto brush = form.getBrush(_barColor); 991 scope(exit) doneWith(brush); 992 auto pen = form.getPen(_barColor, 1); 993 scope(exit) doneWith(pen); 994 995 auto blackPen = form.getPen(getColor(0, 0, 0)); 996 scope(exit) doneWith(blackPen); 997 998 double xStart = leftLim; 999 immutable totalWidth = rightLim - leftLim; 1000 foreach(width; binWidths) { 1001 scope(exit) xStart += width; 1002 immutable height = 1.0 / (binWidths.length * width); 1003 immutable leftPixels = toPixelsX(xStart); 1004 immutable rightPixels = toPixelsX(xStart + width); 1005 immutable widthPixels = rightPixels - leftPixels; 1006 immutable heightPixels = height * multiplier; 1007 1008 immutable startAt = zeroPoint - heightPixels; 1009 1010 form.fillClippedRectangle(brush, leftPixels, 1011 startAt, widthPixels, heightPixels); 1012 1013 form.drawClippedRectangle(pen, leftPixels, 1014 startAt, widthPixels, heightPixels); 1015 1016 // Don't outline rectangle because for equal-frequency 1017 // histograms this is more distracting than readable. 1018 } 1019 } 1020 1021 override void drawLegendSymbol(FigureBase fig, PlotRect where) { 1022 drawFillLegend(_barColor, fig, where); 1023 } 1024 1025 /**Controls the color of the bar. Defaults to blue.*/ 1026 final Color barColor()() { 1027 return _barColor; 1028 } 1029 1030 /// Setter 1031 final This barColor(this This)(Color newColor) { 1032 _barColor = newColor; 1033 return cast(This) this; 1034 } 1035 1036 /**Create a FrequencyHistogram. R must be an input range with elements 1037 * implicitly convertible to double. nBin must be > 0 and <= nums.length. 1038 * 1039 * Throws: Exception if the density for any bin is infinite. 1040 */ 1041 static FrequencyHistogram opCall(R)(R nums, uint nBin) { 1042 auto numArr = toDoubleArray(nums); 1043 enforce(numArr.length >= nBin, 1044 "Can't create an equal-frequency histogram w/ < nBin elements."); 1045 1046 sort(numArr); 1047 auto binWidths = new double[nBin]; 1048 1049 auto ret = new typeof(return); 1050 ret.leftLim = numArr[0]; 1051 ret.rightLim = numArr[$ - 1]; 1052 ret.lowerLim = 0; 1053 1054 // Use linear interpolation to deal w/ non-integer elements/bin. 1055 immutable elemsPerBin = numArr.length / cast(double) nBin; 1056 double lastBinStop = numArr[0]; 1057 foreach(i; 0..nBin) { 1058 immutable floatIndex = elemsPerBin * (i + 1); 1059 immutable indexFract = floatIndex - floor(floatIndex); 1060 size_t lowerIndex = to!size_t(floatIndex); 1061 size_t upperIndex = to!size_t(ceil(floatIndex)); 1062 1063 if(upperIndex >= numArr.length) { 1064 upperIndex = numArr.length - 1; 1065 } 1066 if(lowerIndex >= numArr.length) { 1067 lowerIndex = numArr.length - 1; 1068 } 1069 1070 immutable diff = numArr[upperIndex] - numArr[lowerIndex]; 1071 immutable curBinStop = numArr[lowerIndex] + diff * indexFract; 1072 1073 binWidths[i] = curBinStop - lastBinStop; 1074 enforce(binWidths[i] > 0, 1075 "Can't create an equal-frequency histogram when some bin " ~ 1076 "widths are 0." 1077 ); 1078 lastBinStop = curBinStop; 1079 } 1080 1081 ret.binWidths = binWidths; 1082 ret.elemsPerBin = elemsPerBin; 1083 immutable totalWidth = ret.rightLim - ret.leftLim; 1084 ret.upperLim = 1.0 / (binWidths.length * reduce!min(binWidths)); 1085 return ret; 1086 } 1087 1088 override Figure toLabeledFigure() { 1089 auto ret = toFigure; 1090 ret.yLabel = "Probability Density"; 1091 return ret; 1092 } 1093 } 1094 1095 /**Creates a histogram in which every unique value gets its own bin. 1096 * Useful for histograms where the distribution is known to be discrete over 1097 * a small set of values. 1098 * 1099 * Hint: Since this class inherits from BarPlot, BarPlot.centers will provide 1100 * a list of the unique values found. This can be used to label the X 1101 * axis. 1102 */ 1103 class UniqueHistogram : BarPlot { 1104 1105 /**The total count of this histogram.*/ 1106 immutable uint nElem; 1107 1108 private HistType countsOrProbs = HistType.counts; 1109 1110 private this(double[] x, double[] y, double width, uint nElem) { 1111 this.nElem = nElem; 1112 super(x, y, width); 1113 } 1114 1115 /**Create a UniqueHistogram. R must be an input range with elements 1116 * implicitly convertible to double. The width of each bin will be 1117 * widthFactor times the minimum distance between unique values. widthFactor 1118 * must be > 0 and <= 1. 1119 */ 1120 static UniqueHistogram opCall(R)(R nums, double widthFactor = 0.8) 1121 if(isInputRange!R && isNumeric!(ElementType!R)) { 1122 enforce(widthFactor > 0 && widthFactor <= 1, 1123 "widthFactor must be > 0 and <= 1."); 1124 1125 alias ElementType!R E; 1126 1127 uint nElem; 1128 uint[double] counts; 1129 foreach(num; nums) { 1130 auto ptr = num in counts; 1131 if(ptr is null) { 1132 counts[num] = 1; 1133 } else { 1134 (*ptr)++; 1135 } 1136 nElem++; 1137 } 1138 1139 auto possibleValues = counts.keys; 1140 sort(possibleValues); 1141 double minDiff = double.infinity; 1142 foreach(i; 1..possibleValues.length) { 1143 minDiff = min(minDiff, possibleValues[i] - possibleValues[i - 1]); 1144 } 1145 1146 auto heights = new double[possibleValues.length]; 1147 foreach(i, val; possibleValues) { 1148 heights[i] = counts[val]; 1149 } 1150 1151 return new typeof(this) 1152 (possibleValues, heights, minDiff * widthFactor, nElem); 1153 } 1154 1155 /**Set whether this histogram displays counts or probabilities.*/ 1156 void histType(HistType newType) { 1157 if(newType == countsOrProbs) { 1158 return; 1159 } else if(newType == HistType.Counts) { 1160 scaleHeights(nElem); 1161 } else if(newType == HistType.Probability) { 1162 scaleHeights(1.0 / nElem); 1163 } else { 1164 assert(0); 1165 } 1166 1167 countsOrProbs = newType; 1168 } 1169 } 1170 1171 /**Class for drawing a heat map.*/ 1172 class HeatMap : Plot { 1173 private double[][] values; 1174 private double minVal; 1175 private double maxVal; 1176 private uint _nRows; 1177 private uint _nCols; 1178 private Color[] _colors; 1179 1180 enum string deprecatedMsg = "hotColor() and coldColor() are schedules " ~ 1181 "for deprecation. Use colors() instead."; 1182 1183 private this() { 1184 // Set default colors. 1185 _colors = [ 1186 getColor(255, 255, 255), 1187 getColor(0, 0, 0) 1188 ]; 1189 } 1190 1191 private Color getCellColor(double val) 1192 in { 1193 assert(_colors.length > 1); 1194 assert(val >= minVal); 1195 assert(val <= maxVal); 1196 } body { 1197 immutable diff = maxVal - minVal; 1198 immutable stride = diff / (_colors.length - 1.0); 1199 val -= minVal; 1200 val /= stride; 1201 1202 immutable index1 = to!size_t(floor(val)); 1203 immutable index2 = min(_colors.length - 1, to!size_t(ceil(val))); 1204 auto hotColor = _colors[index2]; 1205 auto coldColor = _colors[index1]; 1206 1207 immutable fract = val - floor(val); 1208 immutable compl = 1.0 - fract; 1209 1210 // Bug 4445: roundTo!ubyte(255.0) throws. 1211 immutable red = cast(ubyte) roundTo!uint( 1212 coldColor.r * compl + hotColor.r * fract); 1213 immutable green = cast(ubyte) roundTo!uint( 1214 coldColor.g * compl + hotColor.g * fract); 1215 immutable blue = cast(ubyte) roundTo!uint( 1216 coldColor.b * compl + hotColor.b * fract); 1217 1218 return getColor(red, green, blue); 1219 } 1220 1221 protected void heatMapDefaultBounds() { 1222 enforceRectangular(); 1223 1224 _nRows = cast(uint) values.length; 1225 if(values.length > 0) { 1226 _nCols = cast(uint) values[0].length; 1227 } 1228 1229 leftLim = 0.5; 1230 rightLim = nCols + 0.5; 1231 lowerLim = 0.5; 1232 upperLim = nRows + 0.5; 1233 } 1234 1235 protected void setMinMax() { 1236 minVal = double.infinity; 1237 maxVal = -double.infinity; 1238 1239 foreach(row; values) foreach(val; row) { 1240 minVal = min(minVal, val); 1241 maxVal = max(maxVal, val); 1242 } 1243 } 1244 1245 protected override void drawLegendSymbol(FigureBase fig, PlotRect where) { 1246 enforce(0, "Heat maps don't have legends."); 1247 } 1248 1249 protected void enforceRectangular() { 1250 foreach(row; values) { 1251 enforce(row.length == values[0].length, 1252 "HeatMap matrices must be rectangular."); 1253 } 1254 } 1255 1256 protected override void drawPlot( 1257 Figure form, 1258 double leftMargin, 1259 double topMargin, 1260 double plotWidth, 1261 double plotHeight 1262 ) { 1263 immutable cellWidth = (rightLim - leftLim) / nCols; 1264 immutable cellHeight = (upperLim - lowerLim) / nRows; 1265 1266 if(nRows < 1 || nCols < 1) { 1267 return; 1268 } 1269 1270 mixin(toPixels); 1271 double lastRowStop = toPixelsY(upperLim); 1272 foreach(row; 0..nRows) { 1273 immutable rowStop = toPixelsY(upperLim - cellHeight * (row + 1)); 1274 scope(exit) lastRowStop = rowStop; 1275 1276 double lastColStop = toPixelsX(leftLim); 1277 foreach(col; 0..nCols) { 1278 immutable colStop = toPixelsX(cellWidth * (col + 1) + leftLim); 1279 scope(exit) lastColStop = colStop; 1280 1281 auto color = getCellColor(values[row][col]); 1282 auto rect = PlotRect(lastColStop, lastRowStop, 1283 colStop - lastColStop, rowStop - lastRowStop); 1284 1285 auto brush = form.getBrush(color); 1286 scope(exit) doneWith(brush); 1287 auto pen = form.getPen(color, 1); 1288 scope(exit) doneWith(pen); 1289 1290 form.fillClippedRectangle(brush, rect); 1291 form.drawClippedRectangle(pen, rect); 1292 } 1293 } 1294 } 1295 1296 private Color _coldColor; 1297 private Color _hotColor; 1298 1299 override bool hasLegend() { 1300 return false; 1301 } 1302 1303 final Color coldColor()() { 1304 pragma(msg, deprecatedMsg); 1305 return _colors[0]; 1306 } 1307 1308 final This coldColor(this This)(Color newColor) { 1309 pragma(msg, deprecatedMsg); 1310 _colors[0] = newColor; 1311 return cast(This) this; 1312 } 1313 1314 final Color hotColor()() { 1315 pragma(msg, deprecatedMsg); 1316 return _colors[1]; 1317 } 1318 1319 final This hotColor(this This)(Color newColor) { 1320 pragma(msg, deprecatedMsg); 1321 _colors[1] = newColor; 1322 return cast(This) this; 1323 } 1324 1325 /// 1326 final Color[] colors()() { 1327 return _colors; 1328 } 1329 1330 /** 1331 Set the colors to be used for this HeatMap as an array from coldest 1332 (smallest value) to warmest (largest value). newColors.length must 1333 be >= 2. 1334 */ 1335 final This colors(this This)(Color[] newColors) { 1336 enforce(newColors.length >= 2, 1337 "Need at least 2 colors for a HeatMap."); 1338 this._colors = newColors; 1339 return cast(This) this; 1340 } 1341 1342 /**Create a heat map from a matrix represented as a range of ranges. 1343 * The matrix must be rectangular. The elements of the ranges must 1344 * be implicitly convertible to double. 1345 */ 1346 static HeatMap opCall(R)(R data) 1347 if(isInputRange!R && isInputRange!(ElementType!R) && 1348 is(ElementType!(ElementType!(R)) : double)) { 1349 auto ret = new typeof(this); 1350 foreach(row; data) { 1351 ret.values ~= toDoubleArray(row); 1352 } 1353 1354 ret.heatMapDefaultBounds(); 1355 ret.setMinMax(); 1356 return ret; 1357 } 1358 1359 /**Create a heat map w/o copying data.*/ 1360 static HeatMap noCopy(double[][] mat) { 1361 auto ret = new typeof(this); 1362 ret.values = mat; 1363 ret.heatMapDefaultBounds(); 1364 ret.setMinMax(); 1365 return ret; 1366 } 1367 1368 /// 1369 final uint nRows() { 1370 return _nRows; 1371 } 1372 1373 /// 1374 final uint nCols() { 1375 return _nCols; 1376 } 1377 1378 /// Throws an exception. Heat maps aren't allowed to have legends. 1379 override This legendText(this This)(string ignored) { 1380 enforce(0, "Heat maps can't have legend text."); 1381 } 1382 } 1383 1384 /** 1385 Convenience function that creates a hierarchically clustered heat map and, 1386 if provided, rearranges your row and column names according to the clustering. 1387 1388 The distance and linkage aliases control the distance and linkage functions 1389 for hierarchical clustering. See plot2kill.hierarchical.hierarchicalCluster() 1390 for details. 1391 */ 1392 HeatMap hierarchicalHeatMap(alias distance = euclidean, alias linkage = mean, R) 1393 (R data, string[] rowNames = null, string[] colNames = null) 1394 if(isInputRange!R && isInputRange!(ElementType!R) && 1395 is(ElementType!(ElementType!(R)) : double)) { 1396 1397 auto matrix = array(map!(toDoubleArray)(data)); 1398 enforce(matrix.length > 0 && matrix[0].length > 0, 1399 "Cannot produce a heat map w/ zero elements."); 1400 1401 foreach(i; 0..matrix.length) { 1402 enforce(matrix[i].length == matrix[0].length, 1403 "data must be rectangular for hierarchicalHeatMap."); 1404 } 1405 1406 auto rowClusters = hierarchicalCluster!(distance, linkage) 1407 (matrix, ClusterBy.rows, rowNames); 1408 1409 auto rowPermApp = appender!(size_t[])(); 1410 size_t nameIndex = 0; 1411 foreach(c; *rowClusters) { 1412 rowPermApp.put(c.index); 1413 if(rowNames.length) rowNames[nameIndex++] = c.name; 1414 } 1415 rowClusters = null; 1416 1417 auto colClusters = hierarchicalCluster!(distance, linkage) 1418 (matrix, ClusterBy.columns, colNames); 1419 1420 auto colPermApp = appender!(size_t[])(); 1421 nameIndex = 0; 1422 foreach(c; *colClusters) { 1423 colPermApp.put(c.index); 1424 if(colNames.length) colNames[nameIndex++] = c.name; 1425 } 1426 colClusters = null; 1427 1428 static T[] byPerm(T)(T[] input, const size_t[] perm) { 1429 assert(perm.length == input.length); 1430 1431 auto ret = new T[input.length]; 1432 foreach(i, p; perm) { 1433 ret[i] = input[p]; 1434 } 1435 1436 return ret; 1437 } 1438 1439 assert(rowPermApp.data.length == matrix.length, text( 1440 rowPermApp.data.length, ' ', matrix.length)); 1441 matrix = byPerm(matrix, rowPermApp.data); 1442 1443 // This fixes some weirdness caused by different conventions between HeatMap 1444 // and most other plots. 1445 reverse(matrix); 1446 1447 foreach(ref row; matrix) { 1448 row = byPerm(row, colPermApp.data); 1449 } 1450 1451 return HeatMap.noCopy(matrix); 1452 } 1453 1454 /**Creates a heat map representing the density of a 2-d probability 1455 * distribution. This is useful when you want to visualize a joint 1456 * probability distribution but the sample size is so large that a 1457 * scatter plot would have an overwhelming number of points. 1458 */ 1459 class HeatScatter : HeatMap { 1460 private double cellWidth; 1461 private double cellHeight; 1462 1463 private OutOfBounds outOfBoundsBehavior = OutOfBounds.Throw; 1464 1465 /**Create a HeatScatter. x, y must be forward ranges with elements 1466 * implicitly convertible to double, and must have the same lengths. 1467 */ 1468 static 1469 HeatScatter opCall(R1, R2)(R1 x, R2 y, uint nRows = 10, uint nCols = 10) 1470 if(isForwardRange!R1 && isForwardRange!R2 && 1471 is(ElementType!R1 : double) && is(ElementType!R2 : double)) { 1472 double xMin = double.infinity; 1473 double xMax = -double.infinity; 1474 uint xLen; 1475 foreach(num; x.save) { 1476 xLen++; 1477 xMin = min(num, xMin); 1478 xMax = max(num, xMax); 1479 } 1480 1481 double yMin = double.infinity; 1482 double yMax = -double.infinity; 1483 uint yLen; 1484 foreach(num; y.save) { 1485 yLen++; 1486 yMin = min(num, yMin); 1487 yMax = max(num, yMax); 1488 } 1489 1490 enforce(xLen == yLen, 1491 "Can't make HeatScatter when x.length != y.length."); 1492 return opCall(x, y, nRows, nCols, xMin, xMax, yMin, yMax); 1493 } 1494 1495 /**Create a HeatScatter with pre-specified bounds. x, y must be forward 1496 * ranges with elements implicitly convertible to double, and must have the 1497 * same lengths. 1498 */ 1499 static 1500 HeatScatter opCall(R1, R2)(R1 x, R2 y, uint nRows, uint nCols, 1501 double xMin, double xMax, double yMin, double yMax, 1502 OutOfBounds boundsBehavior = OutOfBounds.Throw) 1503 if(isForwardRange!R1 && isForwardRange!R2 && 1504 is(ElementType!R1 : double) && is(ElementType!R2 : double)) { 1505 1506 auto ret = opCall(nRows, nCols, xMin, xMax, yMin, yMax, boundsBehavior); 1507 1508 double maxVal = 0; 1509 while(!x.empty && !y.empty) { 1510 scope(exit) { 1511 x.popFront(); 1512 y.popFront(); 1513 } 1514 1515 ret.put(x.front(), y.front()); 1516 } 1517 1518 enforce(x.empty && y.empty, 1519 "Can't make HeatScatter when x.length != y.length."); 1520 return ret; 1521 } 1522 1523 /**Create a blank HeatScatter to fill in using the put() method. 1524 * 1525 * Note: This is a template only because templates can't be overloaded 1526 * against regular functions. 1527 */ 1528 static 1529 HeatScatter opCall(I)(I nRows, uint nCols, 1530 double xMin, double xMax, double yMin, double yMax, 1531 OutOfBounds boundsBehavior = OutOfBounds.Throw) { 1532 enforce(nRows > 0 && nCols > 0, 1533 "Can't make a heat scatter with 0 rows or columns."); 1534 1535 enforce(xMax > xMin, "xMax must be > xMin."); 1536 enforce(yMax > yMin, "yMax must be > yMin."); 1537 1538 auto grid = new double[][](nRows, nCols); 1539 foreach(row; grid) foreach(ref elem; row) { 1540 elem = 0; 1541 } 1542 1543 auto ret = new typeof(return); 1544 ret.boundsBehavior = boundsBehavior; 1545 immutable cellWidth = (xMax - xMin) / nCols; 1546 immutable cellHeight = (yMax - yMin) / nRows; 1547 ret.cellWidth = cellWidth; 1548 ret.cellHeight = cellHeight; 1549 ret.values = grid; 1550 ret._nRows = nRows; 1551 ret._nCols = nCols; 1552 ret.minVal = 0; 1553 ret.maxVal = 0; 1554 ret.leftLim = xMin; 1555 ret.rightLim = xMax; 1556 ret.lowerLim = yMin; 1557 ret.upperLim = yMax; 1558 return ret; 1559 } 1560 1561 /**Add an element to the plot.*/ 1562 This put(this This)(double x, double y) { 1563 bool inBounds() { 1564 return (x >= leftLim && x <= rightLim && y >= lowerLim && 1565 y <= upperLim); 1566 } 1567 1568 if(outOfBoundsBehavior == OutOfBounds.Throw) { 1569 enforce(inBounds(), "Point out of bounds in HeatScatter."); 1570 } else if(!inBounds()) { 1571 return cast(This) this; 1572 } 1573 1574 uint xCoord = to!uint((x - leftLim) / cellWidth); 1575 if(xCoord == nCols) { 1576 xCoord--; 1577 } 1578 1579 uint yCoord = nRows - to!uint((y - lowerLim) / cellHeight) - 1; 1580 if(yCoord == uint.max) { 1581 yCoord = 0; 1582 } 1583 1584 values[yCoord][xCoord]++; 1585 maxVal = max(maxVal, values[yCoord][xCoord]); 1586 1587 return cast(This) this; 1588 } 1589 1590 /**Add another HeatScatter's data to this. The boundaries and row and 1591 * column counts must be the same. The settings from this HeatScatter are 1592 * preserved. 1593 */ 1594 This put(this This)(const HeatScatter rhs) { 1595 if(rhs is null) { 1596 return cast(This) this; 1597 } 1598 enforce(this.leftLim == rhs.leftLim && this.rightLim == rhs.rightLim && 1599 this.upperLim == rhs.upperLim && this.lowerLim == rhs.lowerLim 1600 && this._nRows == rhs._nRows && this._nCols == rhs._nCols, 1601 "Cannot combine two HeatScatters w/ different bounds or row/column #s."); 1602 1603 foreach(row; 0.._nRows) foreach(col; 0.._nCols) { 1604 this.values[row][col] += rhs.values[row][col]; 1605 this.maxVal = max(this.values[row][col], this.maxVal); 1606 } 1607 1608 return cast(This) this; 1609 } 1610 1611 /**Determine whether this object throws or ignores if it receives a number 1612 * outside its bounds via put. 1613 */ 1614 void boundsBehavior(OutOfBounds behavior) { 1615 outOfBoundsBehavior = behavior; 1616 } 1617 } 1618 1619 /**Class for drawing a scatter plot.*/ 1620 class ScatterPlot : Plot { 1621 private double[] x; 1622 private double[] y; 1623 1624 protected override void drawLegendSymbol(FigureBase fig, PlotRect where) { 1625 drawTextLegend(_pointSymbol, _pointColor, fig, where); 1626 } 1627 1628 protected override void drawPlot( 1629 Figure form, 1630 double leftMargin, 1631 double topMargin, 1632 double plotWidth, 1633 double plotHeight 1634 ) { 1635 enforce(x.length == y.length); // Should have already been checked. 1636 1637 if(x.length < 1) { 1638 return; 1639 } 1640 1641 mixin(toPixels); 1642 1643 auto font = getFont(plot2kill.util.defaultFont, 1644 _pointSize + Figure.fontSizeAdjust); 1645 scope(exit) doneWith(font); 1646 1647 auto pointDrawer = ScatterCharDrawer 1648 (_pointSymbol, font, _pointColor, form); 1649 1650 pointDrawer.initialize; 1651 scope(exit) pointDrawer.restore(); 1652 1653 foreach(i; 0..x.length) { 1654 immutable curX = toPixelsX(x[i]); 1655 immutable curY = toPixelsY(y[i]); 1656 pointDrawer.draw(PlotPoint(curX, curY)); 1657 } 1658 } 1659 1660 private Color _pointColor; 1661 private char _pointSymbol = 'x'; 1662 private int _pointSize = 10; 1663 1664 /**The color of each point on the plot.*/ 1665 final Color pointColor()() { 1666 return _pointColor; 1667 } 1668 1669 /// Setter. 1670 final This pointColor(this This)(Color newColor) { 1671 _pointColor = newColor; 1672 return cast(This) this; 1673 } 1674 1675 /**The symbol that should be used on the plot. x and o work pretty well. 1676 * The default is x. 1677 */ 1678 final char pointSymbol()() { 1679 return _pointSymbol; 1680 } 1681 1682 /// Setter 1683 final This pointSymbol(this This)(char newSymbol) { 1684 _pointSymbol = newSymbol; 1685 return cast(This) this; 1686 } 1687 1688 /// The size of a point. (Default 10). 1689 final int pointSize()() { 1690 return _pointSize; 1691 } 1692 1693 /// Setter 1694 final This pointSize(this This)(int newSize) { 1695 this._pointSize = newSize; 1696 return cast(This) this; 1697 } 1698 1699 1700 this() { 1701 _pointColor = getColor(0, 0, 0); 1702 } 1703 1704 1705 /**Factory method for creating a ScatterPlot. x and y must both be 1706 * input ranges of the same length, with elements implicitly convertible 1707 * to doubles. Note that they are copied inside the factory, so changes 1708 * to the original ranges after calling this factory will not affect the 1709 * plot. 1710 */ 1711 static ScatterPlot opCall(R1, R2)(R1 x, R2 y) 1712 if(isInputRange!R1 && is(ElementType!R1 : double) && 1713 isInputRange!R2 && is(ElementType!R2 : double)) { 1714 1715 auto ret = new ScatterPlot; 1716 constructXYGraph(x, y, ret); 1717 1718 fixXYGraphBounds(ret); 1719 addFudgeFactors(ret); 1720 return ret; 1721 } 1722 1723 /**Convenience factory that produces a ScatterPlot with a default X 1724 * axis numbered 1, 2, ..., N where N is the number of points. Mostly 1725 * useful for quick and dirty plots. 1726 */ 1727 static ScatterPlot opCall(R)(R y) 1728 if(isInputRange!R && is(ElementType!R : double)) { 1729 auto ret = new ScatterPlot; 1730 ret.y = toDoubleArray(y); 1731 ret.x = new double[ret.y.length]; 1732 1733 foreach(i, ref elem; ret.x) { 1734 elem = i + 1; 1735 } 1736 1737 fixXYGraphBounds(ret); 1738 addFudgeFactors(ret); 1739 return ret; 1740 } 1741 } 1742 1743 /**Class for drawing a line graph, i.e. a set of points connected by lines.*/ 1744 class LineGraph : Plot { 1745 private double[] x; 1746 private double[] y; 1747 private double[] lowerErrors; 1748 private double[] upperErrors; 1749 1750 1751 private enum defaultErrorWidth = 0.05; 1752 private double _errorWidth = defaultErrorWidth; 1753 1754 private char _pointSymbol = ' '; 1755 private Color _pointColor; 1756 private int _pointSize = 20; 1757 1758 protected void fixBounds() { 1759 this.leftLim = reduce!min(double.infinity, this.x); 1760 this.rightLim = reduce!max(-double.infinity, this.x); 1761 1762 this.upperLim = -double.infinity; 1763 this.lowerLim = double.infinity; 1764 foreach(i, yCoord; this.y) { 1765 if(this.lowerErrors.length) { 1766 this.lowerLim = min(this.lowerLim, yCoord - this.lowerErrors[i]); 1767 } else { 1768 this.lowerLim = min(this.lowerLim, yCoord); 1769 } 1770 if(this.upperErrors.length) { 1771 this.upperLim = max(this.upperLim, yCoord + this.upperErrors[i]); 1772 } else { 1773 this.upperLim = max(this.upperLim, yCoord); 1774 } 1775 } 1776 1777 if(this.lowerErrors.length || this.upperErrors.length) { 1778 // Dont' cut off error bars. 1779 immutable yPad = (this.upperLim - this.lowerLim) * 0.01; 1780 immutable xPad = (this.rightLim - this.leftLim) * _errorWidth; 1781 this.leftLim -= xPad; 1782 this.rightLim += xPad; 1783 this.upperLim += yPad; 1784 this.lowerLim -= yPad; 1785 } 1786 1787 if(_pointSymbol != ' ') { 1788 addFudgeFactors(this); 1789 } 1790 } 1791 1792 protected override void drawLegendSymbol(FigureBase fig, PlotRect where) { 1793 auto pen = fig.getPen(_lineColor, _lineWidth); 1794 scope(exit) doneWith(pen); 1795 drawLineLegend(pen, fig, where); 1796 1797 if(_pointSymbol != ' ') { 1798 drawTextLegend(_pointSymbol, _pointColor, fig, where); 1799 } 1800 } 1801 1802 protected override void drawPlot( 1803 Figure form, 1804 double leftMargin, 1805 double topMargin, 1806 double plotWidth, 1807 double plotHeight 1808 ) { 1809 enforce(x.length == y.length); // Should have already been checked. 1810 1811 if(x.length < 2) { 1812 return; 1813 } 1814 1815 mixin(toPixels); 1816 mixin(drawErrorMixin); 1817 1818 immutable absErrorWidth = _errorWidth * (rightLim - leftLim); 1819 auto errorPen = form.getPen(getColor(0, 0, 0)); 1820 scope(exit) doneWith(errorPen); 1821 1822 double lastX = toPixelsX(x[0]); 1823 double lastY = toPixelsY(y[0]); 1824 1825 void doErrors(size_t index) { 1826 if(lowerErrors.length) { 1827 drawErrorBar( 1828 errorPen, x[index], y[index], 1829 y[index] - lowerErrors[index], absErrorWidth 1830 ); 1831 } 1832 if(upperErrors.length) { 1833 drawErrorBar( 1834 errorPen, x[index], y[index], 1835 y[index] + upperErrors[index], absErrorWidth 1836 ); 1837 } 1838 } 1839 1840 auto pen = form.getPen(_lineColor, _lineWidth); 1841 scope(exit) doneWith(pen); 1842 1843 foreach(i; 1..x.length) { 1844 immutable curX = toPixelsX(x[i]); 1845 immutable curY = toPixelsY(y[i]); 1846 1847 form.drawClippedLine(pen, PlotPoint(lastX, lastY), PlotPoint(curX, curY)); 1848 lastX = curX; 1849 lastY = curY; 1850 } 1851 1852 foreach(i; 0..x.length) { 1853 doErrors(i); 1854 } 1855 1856 if(_pointSymbol == ' ') return; 1857 1858 auto font = getFont(plot2kill.util.defaultFont, 1859 _pointSize + Figure.fontSizeAdjust); 1860 scope(exit) doneWith(font); 1861 1862 auto pointDrawer = ScatterCharDrawer 1863 (_pointSymbol, font, _pointColor, form); 1864 1865 pointDrawer.initialize; 1866 scope(exit) pointDrawer.restore(); 1867 1868 foreach(i; 0..x.length) { 1869 immutable curX = toPixelsX(x[i]); 1870 immutable curY = toPixelsY(y[i]); 1871 pointDrawer.draw(PlotPoint(curX, curY)); 1872 } 1873 } 1874 1875 private Color _lineColor; 1876 private uint _lineWidth = 1; 1877 1878 /**The color of the line. The default is black.*/ 1879 final Color lineColor()() { 1880 return _lineColor; 1881 } 1882 1883 /// Setter 1884 final This lineColor(this This)(Color newColor) { 1885 _lineColor = newColor; 1886 return cast(This) this; 1887 } 1888 1889 /**The width of the line. The default is 1.*/ 1890 final uint lineWidth()() { 1891 return _lineWidth; 1892 } 1893 1894 /// Setter 1895 final This lineWidth(this This)(uint newWidth) { 1896 _lineWidth = newWidth; 1897 return cast(This) this; 1898 } 1899 1900 /**Error bar width, relative to the total width of the plot. Must be 1901 * between 0 and 1. If it's out of bounds, it will be set to the default 1902 * of 0.05. If no error bars are to be drawn, this option is ignored. 1903 */ 1904 final double errorWidth()() { 1905 return _errorWidth; 1906 } 1907 1908 /// Setter 1909 final This errorWidth(this This)(double newWidth) { 1910 if(!(newWidth >= 0 && newWidth <= 1)) { 1911 _errorWidth = defaultErrorWidth; 1912 } else { 1913 _errorWidth = newWidth; 1914 } 1915 1916 fixBounds(); 1917 return cast(This) this; 1918 } 1919 1920 /** 1921 The symbol that should be used to denote a data point. Setting this 1922 to ' ' (a space) turns off point symbols and plots the line only. 1923 By default the point symbol is a space, so no point symbol is plotted.. 1924 */ 1925 final char pointSymbol()() { 1926 return _pointSymbol; 1927 } 1928 1929 /// Setter 1930 final This pointSymbol(this This)(char newSymbol) { 1931 _pointSymbol = newSymbol; 1932 fixBounds(); 1933 return cast(This) this; 1934 } 1935 1936 /**The color of each point on the plot.*/ 1937 final Color pointColor()() { 1938 return _pointColor; 1939 } 1940 1941 /// Setter. 1942 final This pointColor(this This)(Color newColor) { 1943 _pointColor = newColor; 1944 return cast(This) this; 1945 } 1946 1947 /// The size of a point. (Default 20). 1948 final int pointSize()() { 1949 return _pointSize; 1950 } 1951 1952 /// Setter 1953 final This pointSize(this This)(int newSize) { 1954 this._pointSize = newSize; 1955 fixBounds(); 1956 return cast(This) this; 1957 } 1958 1959 private this() { 1960 _lineColor = getColor(0, 0, 0); 1961 _pointColor = getColor(0, 0, 0); 1962 } 1963 1964 /**Factory method for creating a LineGraph. x and y must both be 1965 * input ranges of the same length, with elements implicitly convertible 1966 * to doubles. Note that they are copied inside the factory, so changes 1967 * to the original ranges after calling this factory will not affect the 1968 * plot. 1969 */ 1970 static LineGraph opCall(R1, R2)(R1 x, R2 y) 1971 if(isInputRange!R1 && is(ElementType!R1 : double) && 1972 isInputRange!R2 && is(ElementType!R2 : double)) { 1973 1974 auto ret = new LineGraph; 1975 constructXYGraph(x, y, ret); 1976 ret.fixBounds(); 1977 return ret; 1978 } 1979 1980 /**Convenience factory method that produces a LineGraph with a default 1981 * X axis numbered 1, 2, ..., N, where N is the number of data points, 1982 * and no error bars. This is mostly useful for quick and dirty plots. 1983 */ 1984 static LineGraph opCall(R)(R y) 1985 if(isInputRange!R && is(ElementType!R : double)) { 1986 auto ret = new LineGraph; 1987 ret.y = toDoubleArray(y); 1988 ret.x = new double[ret.y.length]; 1989 1990 foreach(i, ref elem; ret.x) { 1991 elem = i + 1; 1992 } 1993 ret.fixBounds(); 1994 return ret; 1995 } 1996 1997 /**Create a LineGraph with error bars. lowerErrors and upperErrors 1998 * must be input ranges with elements implicitly convertible to double for 1999 * error bars to be shown. Any other value, such as null or 0, will result 2000 * in no error bars being shown. Therefore, to only show, for example, 2001 * upper erros, simply pass in null or 0 for the lower errors. 2002 * 2003 * To draw symmetric error bars, simply pass in the same range for 2004 * lowerErrors and upperErrors. However, note that if you do this, 2005 * the range will need to be a forward range, not an input range. 2006 */ 2007 static LineGraph opCall(R1, R2, R3, R4) 2008 (R1 x, R2 y, R3 lowerErrors, R4 upperErrors) { 2009 auto ret = new typeof(return); 2010 ret.x = toDoubleArray(x); 2011 ret.y = toDoubleArray(y); 2012 2013 enforce(ret.x.length == ret.y.length, 2014 "x, y must have same length for line/scatter graph."); 2015 2016 static if(isForwardRange!R3) { 2017 ret.lowerErrors = toDoubleArray(lowerErrors.save); 2018 } else static if(isInputRange!R3) { 2019 ret.lowerErrors = toDoubleArray(lowerErrors); 2020 } 2021 2022 static if(isForwardRange!R4) { 2023 ret.upperErrors = toDoubleArray(upperErrors.save); 2024 } else static if(isInputRange!R4) { 2025 ret.upperErrors = toDoubleArray(upperErrors); 2026 } 2027 2028 enforce(ret.upperErrors.length == 0 || ret.upperErrors.length == 2029 ret.x.length, "Length of upperErrors must equal number of points."); 2030 enforce(ret.lowerErrors.length == 0 || ret.lowerErrors.length == 2031 ret.x.length, "Length of lowerErrors must equal number of points."); 2032 2033 if(ret.lowerErrors.length == 0 && ret.upperErrors.length == 0) { 2034 sort!"a[0] < b[0]"(zip(ret.x, ret.y)); 2035 } else if(ret.lowerErrors.length == 0) { 2036 sort!"a[0] < b[0]"(zip(ret.x, ret.y, ret.upperErrors)); 2037 } else if(ret.upperErrors.length == 0) { 2038 sort!"a[0] < b[0]"(zip(ret.x, ret.y, ret.lowerErrors)); 2039 } else { 2040 sort!"a[0] < b[0]"( 2041 zip(ret.x, ret.y, ret.lowerErrors, ret.upperErrors)); 2042 } 2043 2044 ret.fixBounds(); 2045 return ret; 2046 } 2047 2048 /**Scale this object by a factor of scaleFactor in the X direction. 2049 * This is useful for getting it onto the same scale as another plot. 2050 */ 2051 void scaleX(double scaleFactor) { 2052 enforce(isFinite(scaleFactor), "Can't scale by infinity or NaN."); 2053 x[] *= scaleFactor; 2054 fixBounds(); 2055 } 2056 2057 /**Scale this object by a factor of scaleFactor in the Y direction. 2058 * This is useful for getting it onto the same scale as another plot.*/ 2059 void scaleY(double scaleFactor) { 2060 enforce(isFinite(scaleFactor), "Can't scale by infinity or NaN."); 2061 y[] *= scaleFactor; 2062 fixBounds(); 2063 } 2064 2065 /**Shift this graph by shiftBy units in the X direction. 2066 * This is useful for getting it onto the same scale as another plot. 2067 */ 2068 void shiftX(double shiftBy) { 2069 enforce(isFinite(shiftBy), "Can't shift by infinity or NaN."); 2070 x[] += shiftBy; 2071 fixBounds(); 2072 } 2073 2074 /**Shift this graph by shiftBy units in the Y direction. 2075 * This is useful for getting it onto the same scale as another plot. 2076 */ 2077 void shiftY(double shiftBy) { 2078 enforce(isFinite(shiftBy), "Can't shift by infinity or NaN."); 2079 y[] += shiftBy; 2080 fixBounds(); 2081 } 2082 } 2083 2084 /** 2085 This class creates a best-fit line, and is useful for adding to scatter 2086 plot figures. The default line color is red instead of black and the default 2087 width is two instead of one to make the best-fit line stand out against the 2088 default scatter plot settings. 2089 */ 2090 class LinearFit : LineGraph { 2091 private double _alpha, _beta, _cor; 2092 2093 final @property const pure nothrow @safe { 2094 /** 2095 The intercept term of the fit line. 2096 */ 2097 double alpha() { return _alpha; } 2098 2099 /** 2100 The slope of the fit line. 2101 */ 2102 double beta() { return _beta; } 2103 2104 /** 2105 The correlation between the predictions made by the regression and 2106 the actual values. 2107 */ 2108 double cor() { return _cor; } 2109 2110 /** 2111 The R^2 value of the fit line. 2112 */ 2113 double r2() { return _cor ^^ 2; } 2114 } 2115 2116 private this() { 2117 super(); 2118 _lineColor = getColor(255, 0, 0); 2119 _lineWidth = 2; 2120 } 2121 2122 /** 2123 Compute the regression coefficients and create a best-fit line, where 2124 x is the independent variable and y is the dependent variable. 2125 lowerLim and upperLim control how far the best-fit line is extended in 2126 each direction along the X-axis. By default the line is extended between 2127 the minimum and maximum values of x. 2128 */ 2129 static LinearFit opCall(R1, R2) 2130 (R1 x, R2 y, double lower = double.nan, double upper = double.nan) 2131 if(isInputRange!R1 && is(ElementType!R1 : double) && 2132 isInputRange!R2 && is(ElementType!R2 : double) 2133 ) { 2134 2135 static if(isForwardRange!R1) { 2136 alias x xx; 2137 } else { 2138 auto xx = toDoubleArray(x); 2139 } 2140 2141 static if(isForwardRange!R2) { 2142 alias y yy; 2143 } else { 2144 auto yy = toDoubleArray(y); 2145 } 2146 2147 import std.numeric; 2148 2149 // This is somewhat inefficient, etc. because all the info has 2150 // to fit neatly on a scatter plot, so we can't be dealing with 2151 // millions of points, and because I don't want to add a dependency 2152 // to do it more properly. 2153 immutable xLen = walkLength(xx); 2154 immutable yLen = walkLength(yy); 2155 enforce(xLen == yLen, "x, y must be same length for LinearFit."); 2156 enforce(xLen > 1, "Need at least two elements for LinearFit."); 2157 2158 immutable xMean = reduce!"a + b"(0.0, xx.save) / xLen; 2159 immutable yMean = reduce!"a + b"(0.0, yy.save) / yLen; 2160 2161 double xDotX = 0, xDotY = 0, yDotY = 0; 2162 foreach(xElem, yElem; lockstep(xx.save, yy.save)) { 2163 immutable xCenter = xElem - xMean; 2164 immutable yCenter = yElem - yMean; 2165 xDotX += xCenter ^^ 2; 2166 yDotY += yCenter ^^ 2; 2167 xDotY += xCenter * yCenter; 2168 } 2169 2170 auto ret = new LinearFit(); 2171 ret._beta = xDotY / xDotX; 2172 ret._alpha = yMean - ret._beta * xMean; 2173 2174 ret._cor = xDotY / sqrt(xDotX) / sqrt(yDotY); 2175 2176 if(isNaN(lower) || isNaN(upper)) { 2177 auto minMaxX = reduce!(min, max)(xx.save); 2178 lower = minMaxX[0]; 2179 upper = minMaxX[1]; 2180 } 2181 2182 ret.x = [lower, upper]; 2183 ret.y.length = 2; 2184 ret.y[] = ret.x[] * ret._beta + ret.alpha; 2185 ret.fixBounds(); 2186 2187 return ret; 2188 } 2189 } 2190 2191 unittest { 2192 auto x = [8, 6, 7, 5, 3, 0, 9]; 2193 auto y = [3, 1, 4, 1, 5, 9, 2]; 2194 2195 // Make sure regression values are right. 2196 auto fit = LinearFit(x, y); 2197 assert(approxEqual(fit.cor, -0.7567996)); 2198 assert(approxEqual(fit.beta, -0.6881)); 2199 assert(approxEqual(fit.alpha, 7.3069)); 2200 assert(approxEqual(fit.r2, 0.5727)); 2201 } 2202 2203 private void constructXYGraph(T, R1, R2)(R1 x, R2 y, T ret) { 2204 ret.x = toDoubleArray(x); 2205 ret.y = toDoubleArray(y); 2206 2207 enforce(ret.x.length == ret.y.length, 2208 "x, y must have same length for line/scatter graph."); 2209 2210 sort!"a[0] < b[0]"(zip(ret.x, ret.y)); 2211 fixXYGraphBounds(ret); 2212 } 2213 2214 private void fixXYGraphBounds(G)(G graph) { 2215 graph.lowerLim = reduce!min(double.infinity, graph.y); 2216 graph.upperLim = reduce!max(-double.infinity, graph.y); 2217 graph.leftLim = reduce!min(double.infinity, graph.x); 2218 graph.rightLim = reduce!max(-double.infinity, graph.x); 2219 2220 /* Weird things happen when a graph has zero width or height. Add fudge 2221 * factors to correct this. 2222 */ 2223 with(graph) { 2224 if(lowerLim == upperLim) { 2225 lowerLim = nextafter(lowerLim, -double.infinity); 2226 upperLim = nextafter(upperLim, double.infinity); 2227 } 2228 2229 if(leftLim == rightLim) { 2230 leftLim = nextafter(leftLim, -double.infinity); 2231 rightLim = nextafter(rightLim, double.infinity); 2232 } 2233 } 2234 } 2235 2236 /**Plot a callable object on a range of values.*/ 2237 class ContinuousFunction : LineGraph { 2238 private enum string setupClass = q{ 2239 auto ret = new ContinuousFunction; 2240 2241 enforce(upperLim > lowerLim, 2242 "Can't do a function plot if upperLim <= lowerLim."); 2243 enforce(nEvals >= 2, "Can't do function plot with < 2 evals."); 2244 2245 immutable diff = upperLim - lowerLim; 2246 immutable width = diff / (nEvals - 1); 2247 auto x = new double[nEvals]; 2248 auto y = new double[nEvals]; 2249 2250 foreach(i; 0..nEvals) { 2251 x[i] = i * width + lowerLim; 2252 y[i] = callable(x[i]); 2253 } 2254 2255 ret.x = x; 2256 ret.y = y; 2257 fixXYGraphBounds(ret); 2258 return ret; 2259 }; 2260 2261 /**Create a ContinuousFunction. C is any callable type mapping a floating 2262 * point number to a number. lowerLim is the lower limit of the plot. 2263 * upperLim is the upper limit of the plot. nEvals is the number of 2264 * evalutations to perform. The default is 1000. More evaluations means 2265 * more accuracy but more computational intensity. 2266 */ 2267 static ContinuousFunction opCall(C)( 2268 scope C callable, 2269 double lowerLim, 2270 double upperLim, 2271 uint nEvals = 1000 2272 ) if(is(typeof(C.init(2.0)) : double)) { 2273 // For some reason doing return opCall!callable doesn't work. 2274 // Using mixin instead. 2275 mixin(setupClass); 2276 } 2277 2278 /**Create a ContinuousFunction using a template alias parameter instead of 2279 * a callable object. 2280 * 2281 * Note: This function is given an name instead of opCall because DMD 2282 * thinks you're trying to instantiate a class if you do 2283 * ContinuousFunction!fun(...). 2284 */ 2285 static ContinuousFunction fromAlias(alias callable)( 2286 double lowerLim, 2287 double upperLim, 2288 uint nEvals = 1000 2289 ) if(is(typeof(callable(2.0)) : double)) { 2290 mixin(setupClass); 2291 } 2292 2293 } 2294 2295 /**Plot a callable object on a range of values.*/ 2296 class DiscreteFunction : BarPlot { 2297 2298 private this(double[] x, double[] y) { 2299 super(x, y, 1); 2300 } 2301 2302 /**Create a DiscreteFunction. C is any callable type mapping an integer 2303 * to a number. lowerLim is the lower limit of the plot. 2304 * upperLim is the upper limit of the plot. 2305 */ 2306 static DiscreteFunction opCall(C)( 2307 scope C callable, 2308 int lowerLim, 2309 int upperLim, 2310 ) if(is(typeof(C.init(2)) : double)) { 2311 enforce(upperLim > lowerLim, 2312 "Can't do a function plot if upperLim <= lowerLim."); 2313 2314 static if(is(typeof(C.opCall))) { 2315 alias ParameterTypeTuple!(C.opCall)[0] I; 2316 } else { 2317 alias ParameterTypeTuple!(C)[0] I; 2318 } 2319 2320 I input = to!I(lowerLim); 2321 double[] x = new double[upperLim - lowerLim + 1]; 2322 double[] y = new double[upperLim - lowerLim + 1]; 2323 2324 foreach(ulIndex; 0..x.length) { 2325 immutable index = cast(int) ulIndex; 2326 x[index] = lowerLim + index; 2327 y[index] = callable(input); 2328 input++; 2329 } 2330 2331 auto ret = new typeof(return)(x, y); 2332 ret.fixBounds(); 2333 return ret; 2334 } 2335 2336 /**Create a DiscreteFunction using a template alias parameter instead of 2337 * a callable object. 2338 * 2339 * Note: This function is given an name instead of opCall because DMD 2340 * thinks you're trying to instantiate a class if you do 2341 * ContinuousFunction!fun(...). 2342 */ 2343 static DiscreteFunction fromAlias(alias callable)( 2344 int lowerLim, 2345 int upperLim, 2346 ) if(is(typeof(callable(2)) : double)) { 2347 enforce(upperLim > lowerLim, 2348 "Can't do a function plot if upperLim <= lowerLim."); 2349 2350 int input = lowerLim; 2351 double[] x = new double[upperLim - lowerLim + 1]; 2352 double[] y = new double[upperLim - lowerLim + 1]; 2353 2354 foreach(int index; 0..x.length) { 2355 x[index] = lowerLim + index; 2356 y[index] = callable(input); 2357 input++; 2358 } 2359 2360 auto ret = new typeof(return)(x, y); 2361 ret.fixBounds(); 2362 return ret; 2363 } 2364 } 2365 2366 // Above was relatively basic plots. Below is more specialized statistical 2367 // plots. 2368 2369 /**Plots a ROC curve, or a curve with sensitivity on the Y-axis 2370 * and 1 - specificity on the X-axis. This is a useful metric for 2371 * determining how well a test statistic discriminates between two classes. 2372 * The following assumptions are made in this implementation: 2373 * 2374 * 1. For some cutoff value c and test statistic T, your decision rule is of 2375 * the form "Class A if T larger than c, Class B if T smaller than c". 2376 * 2377 * 2. In the case of ties, i.e. if class A and class B both have an identical 2378 * value, linear interpolation is used. This is because changing the 2379 * value of c infinitesimally will change both sensitivity and specificity 2380 * in these cases. 2381 */ 2382 class RocCurve : LineGraph { 2383 private: 2384 double _auroc; 2385 2386 this() { 2387 super(); 2388 } 2389 2390 public: 2391 /**Create a RocCurve. classATs are the test statistics that are 2392 * "supposed" to be bigger, classBTs are the test statistics that are 2393 * "supposed to be smaller. Both R1 and R2 must be input ranges with 2394 * elements implicitly convertible to double. 2395 */ 2396 static RocCurve opCall(R1, R2)(R1 classATs, R2 classBTs) 2397 if(isNumeric!(ElementType!R1) && isNumeric!(ElementType!R2)) { 2398 // Shamelessly cut-and-pasted and modified from dstats. 2399 2400 auto classA = array(classATs); 2401 auto classB = array(classBTs); 2402 sort(classA); 2403 sort(classB); 2404 2405 // Start cutoff at -infinity, such that we get everything in class A, i.e. 2406 // perfect specificity, zero sensitivity. We arbitrarily define class B 2407 // as our "positive" class. 2408 double tp = 0, tn = classA.length, fp = 0, fn = classB.length; 2409 double[2] lastPoint = 0; 2410 2411 Unqual!(CommonType!(ElementType!R1, ElementType!R2)) currentVal; 2412 2413 ElementType!R1 popA() { 2414 tn--; 2415 fp++; 2416 auto ret = classA.front(); 2417 classA.popFront(); 2418 return ret; 2419 } 2420 2421 ElementType!R2 popB() { 2422 fn--; 2423 tp++; 2424 auto ret = classB.front(); 2425 classB.popFront(); 2426 return ret; 2427 } 2428 2429 double area = 0; 2430 double[] x = [0.0]; 2431 double[] y = [0.0]; 2432 while(!classA.empty && !classB.empty) { 2433 if(classA.front() < classB.front()) { 2434 currentVal = popA(); 2435 } else { 2436 currentVal = popB(); 2437 } 2438 2439 // Handle ties. 2440 while(!classA.empty && classA.front() == currentVal) { 2441 popA(); 2442 } 2443 2444 while(!classB.empty && classB.front() == currentVal) { 2445 popB(); 2446 } 2447 2448 double[2] curPoint; 2449 curPoint[0] = 1.0 - tn / (fp + tn); 2450 curPoint[1] = tp / (tp + fn); 2451 2452 x ~= curPoint[0]; 2453 y ~= curPoint[1]; 2454 2455 immutable xDist = curPoint[0] - lastPoint[0]; 2456 area += xDist * lastPoint[1]; // Rectangular part. 2457 area += xDist * 0.5 * (curPoint[1] - lastPoint[1]); // Triangular part. 2458 lastPoint[] = curPoint[]; 2459 } 2460 2461 if(classA.length > 0 && classB.length == 0) { 2462 // Then we already have a sensitivity of 1, move straight to the right 2463 // to the point (1, 1). 2464 2465 immutable xDist = 1 - lastPoint[0]; 2466 area += xDist * lastPoint[1]; // Rectangular part. 2467 area += xDist * 0.5 * (1 - lastPoint[1]); // Triangular part. 2468 } 2469 2470 x ~= 1; 2471 y ~= 1; 2472 2473 auto ret = new typeof(return)(); 2474 ret._auroc = area; 2475 ret.x = x; 2476 ret.y = y; 2477 fixXYGraphBounds(ret); 2478 return ret; 2479 } 2480 2481 /**Returns the area under the ROC curve.*/ 2482 final double auroc() const pure nothrow { 2483 return _auroc; 2484 } 2485 2486 /**Default title is the area under the curve. Default x label is 2487 * "1 - Specificity". Default y label is "Sensitivity". 2488 */ 2489 override Figure toLabeledFigure() { 2490 auto ret = toFigure; 2491 ret.title = "AUROC = " ~ to!string(auroc); 2492 ret.xLabel = "1 - Specificity"; 2493 ret.yLabel = "Sensitivity"; 2494 return ret; 2495 } 2496 } 2497 2498 unittest { 2499 // Values worked out by hand on paper. If you don't believe me, work 2500 // them out yourself. 2501 auto foo = RocCurve([4,5,6], [1,2,3]); 2502 assert(foo.auroc == 1); 2503 2504 foo = RocCurve([8,6,7,5,3,0,9], [3,6,2,4,3,6]); 2505 assert(approxEqual(foo.auroc, 0.6904762)); 2506 2507 foo = RocCurve([2,7,1,8,2,8,1,8], [3,1,4,1,5,9,2,6]); 2508 assert(approxEqual(foo.auroc, 0.546875)); 2509 } 2510 2511 /**Plots the quantiles of a set of data on the Y axis against the theoretical 2512 * qualtiles or the quantiles of another set of data on the X axis. 2513 */ 2514 class QQPlot : ScatterPlot { 2515 private this() { 2516 super(); 2517 _lineColor = getColor(255, 0, 0); 2518 } 2519 2520 protected override void drawPlot( 2521 Figure form, 2522 double leftMargin, 2523 double topMargin, 2524 double plotWidth, 2525 double plotHeight 2526 ) { 2527 super.drawPlot( 2528 form, leftMargin, topMargin, plotWidth, plotHeight 2529 ); 2530 2531 // Draw line that indicates identical distributions. 2532 if(max(lowerLim, leftLim) < min(upperLim, rightLim)) { 2533 mixin(toPixels); 2534 2535 auto pen = form.getPen(_lineColor, _lineWidth); 2536 scope(exit) doneWith(pen); 2537 2538 immutable lowerX = toPixelsX(max(lowerLim, leftLim)); 2539 immutable lowerY = toPixelsY(max(lowerLim, leftLim)); 2540 immutable upperX = toPixelsX(min(upperLim, rightLim)); 2541 immutable upperY = toPixelsY(min(upperLim, rightLim)); 2542 2543 form.drawClippedLine( 2544 pen, 2545 PlotPoint(lowerX, lowerY), 2546 PlotPoint(upperX, upperY) 2547 ); 2548 } 2549 } 2550 2551 private Color _lineColor; 2552 2553 /**The color of the y = x line that indicates identical distributions. 2554 * The default is red. 2555 */ 2556 final Color lineColor()() { 2557 return _lineColor; 2558 } 2559 2560 /// Setter 2561 final This lineColor(this This)(Color newColor) { 2562 _lineColor = newColor; 2563 return cast(This) this; 2564 } 2565 2566 private double _lineWidth = 2; 2567 2568 /**The width of the line that indicates identical distributions. 2569 * The default is 2. 2570 */ 2571 final double lineWidth()() { 2572 return _lineWidth; 2573 } 2574 2575 /// Setter. 2576 final This lineWidth(this This)(double newWidth) { 2577 _lineWidth = newWidth; 2578 return cast(This) this; 2579 } 2580 2581 /**Create a QQPlot. dataRange must be an input range with elements 2582 * implicitly convertible to doubles. quantileFunction must be a 2583 * callable (function pointer, delegate, functor, etc.) mapping any 2584 * number between 0 and 1 to its quantile. 2585 * 2586 * Examples: 2587 * --- 2588 * auto norms = randArray!rNorm(100, 0, 1); 2589 * auto theoretical = paramFunctor!invNormalCDF(0, 1); 2590 * auto fig = new Figure( 2591 * QQPlot(norms, theoretical) 2592 * ); 2593 * fig.showAsMain(); 2594 * --- 2595 */ 2596 static QQPlot opCall(R, C)(R dataRange, C quantileFunction) 2597 if(is(typeof(C.init(2.0)) : double) && isInputRange!R && 2598 is(ElementType!R : double)) { 2599 auto ret = new QQPlot; 2600 2601 ret.y = toDoubleArray(dataRange); 2602 sort(ret.y); 2603 immutable double N = ret.y.length + 1.0; 2604 2605 ret.x = new double[ret.y.length]; 2606 foreach(i, ref elem; ret.x) { 2607 elem = quantileFunction((i + 1) / N); 2608 } 2609 2610 fixXYGraphBounds(ret); 2611 addFudgeFactors(ret); 2612 return ret; 2613 } 2614 2615 /**Default x label is "Theoretical Quantiles". Default y label is 2616 * "Empirical Quantiles". 2617 */ 2618 override Figure toLabeledFigure() { 2619 auto ret = toFigure; 2620 ret.xLabel = "Theoretical Quantiles"; 2621 ret.yLabel = "Empirical Quantiles"; 2622 return ret; 2623 } 2624 } 2625 2626 /** 2627 Draw a basic box-and-whisker plot. The plots are drawn centered at Y 2628 coordinates [0, 1, ..., N) unless the offset property is set to something else 2629 (which is useful for putting multiple box pots with different color boxes on 2630 the same figure). 2631 */ 2632 class BoxPlot : Plot { 2633 private: 2634 double[] medians; 2635 double[] boxBottoms; 2636 double[] boxTops; 2637 double[] whiskerBottoms; 2638 double[] whiskerTops; 2639 double[][] outliers; 2640 double whiskerPercentile; 2641 double _offset = 0; 2642 Color _color; 2643 2644 this() { 2645 _color = getColor(0, 0, 0); // Default color. 2646 } 2647 2648 void updateBounds() { 2649 if(medians.length == 0) return; 2650 2651 upperLim = reduce!max(-double.infinity, 2652 chain(whiskerTops, join(outliers)) 2653 ); 2654 2655 lowerLim = reduce!min(double.infinity, 2656 chain(whiskerBottoms, join(outliers)) 2657 ); 2658 2659 immutable diff = upperLim - lowerLim; 2660 upperLim += 0.01 * diff; 2661 lowerLim -= 0.01 * diff; 2662 2663 leftLim = -0.6; 2664 rightLim = medians.length - 0.4; 2665 2666 leftLim += _offset; 2667 rightLim += _offset; 2668 2669 } 2670 2671 void addDataImpl(R)(R range) { 2672 auto doubles = toDoubleArray(range); 2673 enforce(doubles.length, 2674 "Cannot add a zero-length range to a box and whisker plot."); 2675 sort(doubles); 2676 2677 if(doubles.length & 1) { 2678 medians ~= doubles[$ / 2]; 2679 } else { 2680 medians ~= 0.5 * doubles[$ / 2] + 0.5 * doubles[$ / 2 + 1]; 2681 } 2682 2683 double doInterp(double percentile) { 2684 immutable floatIndex = percentile * (doubles.length - 1); 2685 immutable floored = to!size_t(floatIndex); 2686 immutable fract = floatIndex - floored; 2687 2688 if(fract == 1 || floored == doubles.length - 1) { 2689 return doubles[floored]; 2690 } else { 2691 return (1 - fract) * doubles[floored] + 2692 fract * doubles[floored + 1]; 2693 } 2694 } 2695 2696 boxBottoms ~= doInterp(0.25); 2697 boxTops ~= doInterp(0.75); 2698 whiskerBottoms ~= doInterp(whiskerPercentile); 2699 whiskerTops ~= doInterp(1 - whiskerPercentile); 2700 2701 immutable whiskerTopIndex = to!size_t( 2702 ceil((1 - whiskerPercentile) * (doubles.length - 1)) + 1 2703 ); 2704 immutable whiskerBottomIndex = to!size_t( 2705 whiskerPercentile * (doubles.length - 1) 2706 ); 2707 2708 auto outlierRange = chain( 2709 doubles[0..whiskerBottomIndex], 2710 doubles[whiskerTopIndex..$] 2711 ); 2712 2713 outliers.length += 1; 2714 2715 foreach(outlier; outlierRange) { 2716 outliers[$ - 1] ~= outlier; 2717 } 2718 2719 // We're absolutely sure we don't need doubles anymore, and may 2720 // be working with huge datasets. 2721 delete doubles; 2722 2723 updateBounds(); 2724 } 2725 2726 protected: 2727 protected override void drawPlot( 2728 Figure form, 2729 double leftMargin, 2730 double topMargin, 2731 double plotWidth, 2732 double plotHeight 2733 ) { 2734 mixin(toPixels); 2735 immutable nBoxes = medians.length; 2736 immutable boxWidth = 1; 2737 2738 auto pen = form.getPen(_color, 1); 2739 auto font = getFont(plot2kill.util.defaultFont, 2740 10 + Figure.fontSizeAdjust); 2741 auto outlierDrawer = ScatterCharDrawer('o', font, _color, form); 2742 2743 foreach(boxIndex, med; medians) { 2744 immutable left = boxWidth * boxIndex + 0.15 - 0.5 + _offset; 2745 immutable center = boxWidth * boxIndex + _offset; 2746 immutable right = boxWidth * boxIndex + 0.85 - 0.5 + _offset; 2747 immutable wLeft = boxWidth * boxIndex + 0.35 - 0.5 + _offset; 2748 immutable wRight = boxWidth * boxIndex + 0.65 - 0.5 + _offset; 2749 2750 immutable leftPixels = toPixelsX(left); 2751 immutable centerPixels = toPixelsX(center); 2752 immutable rightPixels = toPixelsX(right); 2753 immutable wLeftPixels = toPixelsX(wLeft); 2754 immutable wRightPixels = toPixelsX(wRight); 2755 immutable boxTopPixels = toPixelsY(boxTops[boxIndex]); 2756 immutable boxBotPixels = toPixelsY(boxBottoms[boxIndex]); 2757 immutable wTopPixels = toPixelsY(whiskerTops[boxIndex]); 2758 immutable wBotPixels = toPixelsY(whiskerBottoms[boxIndex]); 2759 2760 // Draw box. 2761 form.drawClippedRectangle(pen, 2762 leftPixels, boxTopPixels, 2763 rightPixels - leftPixels, 2764 boxBotPixels - boxTopPixels 2765 ); 2766 2767 // Draw median lines. 2768 form.drawClippedLine( 2769 pen, 2770 PlotPoint(leftPixels, toPixelsY(med)), 2771 PlotPoint(rightPixels, toPixelsY(med)) 2772 ); 2773 2774 2775 // Draw whiskers. 2776 form.drawClippedLine( 2777 pen, 2778 PlotPoint(centerPixels, boxTopPixels), 2779 PlotPoint(centerPixels, wTopPixels) 2780 ); 2781 2782 form.drawClippedLine( 2783 pen, 2784 PlotPoint(centerPixels, boxBotPixels), 2785 PlotPoint(centerPixels, wBotPixels) 2786 ); 2787 2788 form.drawClippedLine( 2789 pen, 2790 PlotPoint(wLeftPixels, wBotPixels), 2791 PlotPoint(wRightPixels, wBotPixels) 2792 ); 2793 2794 form.drawClippedLine( 2795 pen, 2796 PlotPoint(wLeftPixels, wTopPixels), 2797 PlotPoint(wRightPixels, wTopPixels) 2798 ); 2799 2800 if(whiskerPercentile == 0) continue; 2801 2802 // Draw outliers. 2803 outlierDrawer.initialize(); 2804 scope(exit) outlierDrawer.restore(); 2805 2806 foreach(outlier; outliers[boxIndex]) { 2807 outlierDrawer.draw( 2808 PlotPoint(centerPixels, toPixelsY(outlier)) 2809 ); 2810 } 2811 } 2812 } 2813 2814 override void drawLegendSymbol(FigureBase fig, PlotRect where) { 2815 auto pen = fig.getPen(_color, 1); 2816 scope(exit) doneWith(pen); 2817 drawLineLegend(pen, fig, where); 2818 } 2819 2820 public: 2821 2822 /** 2823 Create a BoxPlot. whiskerPercentile controls the percentile at which 2824 data points are considered outliers and plotted as individual points. 2825 For any x, a percentile of x is equivalent to a percentile of 1 - x. 2826 For example, if whiskerPercentile is either 0 or 1, no data is plotted as 2827 individual points and the whiskers extend all the way to the extrema. 2828 */ 2829 static BoxPlot opCall(double whiskerPercentile = 0) { 2830 enforce(whiskerPercentile >= 0 && whiskerPercentile <= 1, 2831 "whiskerPercentile must be between 0 and 1."); 2832 auto ret = new BoxPlot; 2833 ret.whiskerPercentile = min(whiskerPercentile, 1 - whiskerPercentile); 2834 return ret; 2835 } 2836 2837 /** 2838 Add data to the box plot. data may be any combination of ranges of 2839 ranges and individual ranges. 2840 */ 2841 This addData(this This, R...)(R data) 2842 if(allSatisfy!(isInputRange, R)) { 2843 foreach(r; data) { 2844 static if(isInputRange!(ElementType!(typeof(r)))) { 2845 foreach(rr; r) addDataImpl(rr); 2846 } else { 2847 addDataImpl(r); 2848 } 2849 } 2850 2851 return cast(This) this; 2852 } 2853 2854 /// The offset from zero at which the first box is centered. 2855 double offset()() { 2856 return _offset; 2857 } 2858 2859 /// Setter 2860 This offset(this This)(double newOffset) { 2861 this._offset = newOffset; 2862 updateBounds(); 2863 return cast(This) this; 2864 } 2865 2866 /// The color that the boxes, whiskers and outliers are drawn in. 2867 Color color()() { 2868 return _color; 2869 } 2870 2871 /// Setter. 2872 This color(this This)(Color newColor) { 2873 this._color = newColor; 2874 return cast(This) this; 2875 } 2876 2877 /// The number of boxes currently on the plot. 2878 int nBoxes() { 2879 // If you have more than a few billion boxes, you have bigger 2880 // problems than integer overflow. 2881 return cast(int) medians.length; 2882 } 2883 } 2884 2885 /** 2886 A dendrogram is a tree of hierarchically clustered data such that the height 2887 of each split is proportional to the distance between clusters. 2888 */ 2889 class Dendrogram : Plot { 2890 private: 2891 Cluster* clusters; 2892 immutable int nLeaves; 2893 string[] _names; 2894 2895 // Make min leaf lines 10% of the difference between max, min distance. 2896 enum leafLineFactor = 0.1; 2897 immutable double minLeafLineLength; 2898 2899 this(Cluster* clusters) { 2900 this.clusters = clusters; 2901 this.leftLim = -0.5; 2902 int nLeaves = 0; 2903 2904 foreach(leaf; *clusters) { 2905 _names ~= leaf.name; 2906 nLeaves++; 2907 } 2908 2909 this.nLeaves = nLeaves; 2910 2911 double minDist = double.infinity, maxDist = -double.infinity; 2912 void doDist(Cluster* node) { 2913 if(node.isLeaf) return; 2914 minDist = min(node.distance, minDist); 2915 maxDist = max(node.distance, maxDist); 2916 2917 doDist(node.left); 2918 doDist(node.right); 2919 } 2920 2921 doDist(clusters); 2922 2923 this.rightLim = nLeaves - 0.5; 2924 this.upperLim = maxDist; 2925 2926 auto dummyForm = new Figure(); 2927 2928 // Find lower limit by adding in the space taken by the leaf names. 2929 this.minLeafLineLength = leafLineFactor * (maxDist - minDist); 2930 this.lowerLim = minDist - minLeafLineLength; 2931 } 2932 2933 protected: 2934 override void drawPlot( 2935 Figure form, 2936 double leftMargin, 2937 double topMargin, 2938 double plotWidth, 2939 double plotHeight 2940 ) { 2941 mixin(toPixels); 2942 int curLeafIndex = 0; 2943 2944 // TODO: Make these settings customizable. 2945 auto pen = form.getPen(getColor(0, 0, 0), 1); 2946 2947 double drawLeaf(double parentDistance) { 2948 immutable parentDistPixels = toPixelsY(parentDistance); 2949 immutable lineBottomPixels = toPixelsY(lowerLim); 2950 immutable xPixels = toPixelsX(curLeafIndex); 2951 2952 form.drawClippedLine(pen, 2953 PlotPoint(xPixels, lineBottomPixels), 2954 PlotPoint(xPixels, parentDistPixels) 2955 ); 2956 2957 immutable double ret = curLeafIndex; 2958 curLeafIndex++; 2959 return ret; 2960 } 2961 2962 // Returns: The center of the just-drawn sub-dendrogram. 2963 double drawImpl(Cluster* node) { 2964 double leftCenter, rightCenter; 2965 if(node.left.isLeaf) { 2966 leftCenter = drawLeaf(node.distance); 2967 } else { 2968 leftCenter = drawImpl(node.left); 2969 } 2970 2971 if(node.right.isLeaf) { 2972 rightCenter = drawLeaf(node.distance); 2973 } else { 2974 rightCenter = drawImpl(node.right); 2975 } 2976 2977 immutable leftTop = (node.left.isLeaf) ? (node.distance) : 2978 node.left.distance; 2979 immutable rightTop = (node.right.isLeaf) ? (node.distance) : 2980 node.right.distance; 2981 2982 immutable leftCenterPixels = toPixelsX(leftCenter); 2983 immutable rightCenterPixels = toPixelsX(rightCenter); 2984 immutable leftTopPixels = toPixelsY(leftTop); 2985 immutable rightTopPixels = toPixelsY(rightTop); 2986 immutable distPixels = toPixelsY(node.distance); 2987 2988 form.drawClippedLine(pen, 2989 PlotPoint(leftCenterPixels, leftTopPixels), 2990 PlotPoint(leftCenterPixels, distPixels), 2991 ); 2992 2993 form.drawClippedLine(pen, 2994 PlotPoint(leftCenterPixels, distPixels), 2995 PlotPoint(rightCenterPixels, distPixels) 2996 ); 2997 2998 form.drawClippedLine(pen, 2999 PlotPoint(rightCenterPixels, distPixels), 3000 PlotPoint(rightCenterPixels, rightTopPixels) 3001 ); 3002 3003 return 0.5 * leftCenter + 0.5 * rightCenter; 3004 } 3005 3006 drawImpl(clusters); 3007 } 3008 3009 protected override void drawLegendSymbol(FigureBase fig, PlotRect where) { 3010 auto pen = fig.getPen(getColor(0, 0, 0), 1); 3011 scope(exit) doneWith(pen); 3012 drawLineLegend(pen, fig, where); 3013 } 3014 3015 public: 3016 /** 3017 Create a Dendrogram from hierarchically clustered data. 3018 */ 3019 static Dendrogram opCall(Cluster* clusters) { 3020 return new Dendrogram(clusters); 3021 } 3022 3023 /** 3024 Create a Dendrogram figure with rotated text leaf labels on the X-axis 3025 tick labels and "Distance" as the Y-axis label. 3026 */ 3027 override Figure toLabeledFigure() @property { 3028 auto ret = this.toFigure; 3029 3030 ret.xTickLabels(iota(nLeaves), _names); 3031 ret.yLabel("Distance"); 3032 ret.rotatedXTick(true); 3033 3034 return ret; 3035 } 3036 3037 /** 3038 The names of the leaf nodes, in order from left to right on the plot. 3039 */ 3040 final const(string)[] leafNames() const pure nothrow { 3041 return _names; 3042 } 3043 } 3044 3045 private: 3046 3047 void addFudgeFactors(P)(P plot) { 3048 with(plot) { 3049 // Add fudge factors to bounds to make points not appear off the chart. 3050 immutable horizFudge = (upperLim - lowerLim) * 0.03; 3051 immutable verticalFudge = (rightLim - leftLim) * 0.03; 3052 leftLim -= verticalFudge; 3053 rightLim += verticalFudge; 3054 upperLim += horizFudge; 3055 lowerLim -= horizFudge; 3056 } 3057 } 3058 3059 void drawLineLegend(Pen pen, FigureBase fig, PlotRect where) { 3060 auto mid = where.y + where.height / 2; 3061 fig.drawLine(pen, 3062 PlotPoint(where.x, mid), 3063 PlotPoint(where.x + where.width, mid)); 3064 } 3065 3066 void drawFillLegend(Color color, FigureBase fig, PlotRect where) { 3067 auto brush = fig.getBrush(color); 3068 scope(exit) doneWith(brush); 3069 fig.fillRectangle(brush, where.x, where.y, where.width, where.height); 3070 } 3071 3072 void drawTextLegend(char symbol, Color color, FigureBase fig, PlotRect where) { 3073 // Draw this small even if the actual symbol is huge so it fits in the 3074 // allotted space. 3075 auto font = getFont(plot2kill.util.defaultFont, 3076 10 + Figure.fontSizeAdjust); 3077 3078 // Center location. 3079 string writeThis = [cast(immutable) symbol]; 3080 immutable meas = fig.measureText(writeThis, font); 3081 immutable stdMeas = fig.measureText("A", font); 3082 3083 where.y += (where.height - meas.height) / 2 - (stdMeas.height - meas.height); 3084 where.height = meas.height; 3085 3086 scope(exit) doneWith(font); 3087 fig.drawText(writeThis, font, color, where, TextAlignment.Center); 3088 }