1 /**This file contains Figure, which is a holds and draws one or more Plots 2 * onto a drawable surface. 3 * 4 * Copyright (C) 2010-2011 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 module plot2kill.figure; 33 34 import plot2kill.plot, plot2kill.util, std.typetuple, std.random; 35 36 version(dfl) { 37 public import plot2kill.dflwrapper; 38 } else { 39 public import plot2kill.gtkwrapper; 40 } 41 42 package enum legendSymbolSize = 15; // 30 by 30 pixels. 43 package enum legendSymbolTextSpace = 3; 44 45 /**A container form for one or more Plot objects. 46 * 47 * Examples: 48 * --- 49 * auto nums = [1,1,1,1,2,2,3,3,4,5,6,7]; 50 * auto hist = Histogram(nums, 10); 51 * auto fig = new Figure; 52 * fig.addPllot(hist); 53 * fig.title = "A plot"; 54 * fig.xLabel = "X label"; 55 * fig.yLabel = "Y label"; 56 * fig.showAsMain(); 57 * --- 58 */ 59 class Figure : FigureBase { 60 private: 61 double upperLim = -double.infinity; 62 double lowerLim = double.infinity; 63 double leftLim = double.infinity; 64 double rightLim = -double.infinity; 65 66 // These control whether to auto set the axes. 67 bool userSetXAxis = false; 68 bool userSetYAxis = false; 69 70 bool _horizontalGrid; 71 bool _verticalGrid; 72 73 bool _rotatedXTick; 74 bool _rotatedYTick; 75 76 enum tickPixels = 10; 77 enum legendMarginHoriz = 20; 78 enum legendMarginVert = 10; 79 double xTickLabelWidth; 80 double yTickLabelWidth; 81 double tickLabelHeight; 82 83 Pen axesPen; 84 ubyte _gridIntensity = 128; 85 Pen gridPen; 86 87 Font _axesFont; 88 Font _legendFont; 89 90 Color[] _xTickColors; 91 Color[] _yTickColors; 92 93 LegendLocation _legendLoc = LegendLocation.bottom; 94 95 void fixTickSizes() { 96 void fixTickLabelSize(ref double toFix, string[] axisText) { 97 toFix = 0; 98 foreach(lbl; axisText) { 99 auto lblSize = measureText(lbl, _axesFont); 100 if(lblSize.height > tickLabelHeight) { 101 tickLabelHeight = lblSize.height; 102 } 103 104 if(lblSize.width > toFix) { 105 toFix = lblSize.width; 106 } 107 } 108 } 109 110 tickLabelHeight = 0; 111 fixTickLabelSize(xTickLabelWidth, xAxisText); 112 fixTickLabelSize(yTickLabelWidth, yAxisText); 113 } 114 115 void fixMargins() { 116 fixTickSizes(); 117 immutable legendMeasure = measureLegend(); 118 immutable legendHeight = legendMeasure.height; 119 immutable legendWidth = legendMeasure.width; 120 121 immutable xLabelSize = measureText(xLabel(), xLabelFont()); 122 immutable bottomTickHeight = (rotatedXTick()) ? 123 xTickLabelWidth : tickLabelHeight; 124 immutable leftTickWidth = (rotatedYTick()) ? 125 tickLabelHeight : yTickLabelWidth; 126 127 bottomMargin = bottomTickHeight + tickPixels + xLabelSize.height 128 + (legendLocation() == LegendLocation.bottom) * legendHeight + 20; 129 130 topMargin = measureText(title(), titleFont(), plotWidth).height 131 + (legendLocation() == LegendLocation.top) * legendHeight + 20; 132 133 leftMargin = measureText(yLabel(), yLabelFont()).height + 134 tickPixels + leftTickWidth 135 + (legendLocation() == LegendLocation.left) * legendWidth + 30; 136 137 rightMargin = (legendLocation() == LegendLocation.right) 138 * legendWidth + 30; 139 } 140 141 Tuple!(double, "height", double, "width", int, "nRows") measureLegend() { 142 immutable alwaysWrap = legendLocation() == LegendLocation.right || 143 legendLocation() == LegendLocation.left; 144 145 PlotSize ret = PlotSize(0, 0); 146 147 double maxHeight = 0; 148 double maxWidth = 0; 149 int nRows = 1; 150 double rowPos = legendMarginHoriz; 151 152 bool shouldReturnZero = true; 153 foreach(plot; plotData) { 154 if(plot.hasLegend && plot.legendText().length) { 155 shouldReturnZero = false; 156 } 157 } 158 159 if(shouldReturnZero) { 160 return typeof(return)(0, 0, 0); 161 } 162 163 foreach(plot; plotData) if(plot.hasLegend) { 164 auto itemSize = plot.measureLegend(legendFont(), this); 165 maxHeight = max(maxHeight, itemSize.height); 166 maxWidth = max(maxWidth, itemSize.width); 167 168 if(alwaysWrap || 169 (itemSize.width + rowPos >= this.width - legendMarginHoriz 170 && rowPos > legendMarginHoriz)) { 171 nRows++; 172 rowPos = itemSize.width + legendMarginHoriz; 173 } 174 175 rowPos += itemSize.width + legendMarginHoriz; 176 } 177 178 return typeof(return)( 179 (maxHeight + legendMarginVert) * nRows, 180 maxWidth + legendMarginHoriz, 181 nRows 182 ); 183 } 184 185 FigureLine[] extraLines; 186 187 final double plotWidth() { 188 return this.width - leftMargin - rightMargin; 189 } 190 191 final double plotHeight() { 192 return this.height - topMargin - bottomMargin; 193 } 194 195 mixin(toPixels); 196 197 void drawTitle() { 198 if(nullOrInit(titleFont())) { 199 return; 200 } 201 202 auto height = measureText(title(), titleFont()).height; 203 auto rect = PlotRect(leftMargin, 204 10, this.plotWidth, height); 205 auto format = TextAlignment.Center; 206 drawText(title(), titleFont(), getColor(0, 0, 0), rect, format); 207 } 208 209 void drawXlabel() { 210 if(nullOrInit(xLabelFont())) { 211 return; 212 } 213 214 immutable textSize = measureText(xLabel(), xLabelFont()); 215 immutable yTop = this.height - textSize.height - 10 216 - (legendLocation() == LegendLocation.bottom) * 217 measureLegend().height; 218 auto rect = PlotRect(leftMargin, yTop, 219 this.width - leftMargin - rightMargin, textSize.height); 220 221 auto format = TextAlignment.Center; 222 drawText(xLabel(), xLabelFont(), getColor(0, 0, 0), rect, format); 223 } 224 225 void drawYlabel() { 226 if(nullOrInit(yLabelFont()) || yLabel().length == 0) { 227 return; 228 } 229 230 immutable textSize = measureText(yLabel(), yLabelFont()); 231 immutable margin = (plotHeight - textSize.width) / 2 + topMargin; 232 immutable xCoord = 10 + measureLegend().width 233 * (legendLocation() == LegendLocation.left); 234 235 auto rect = PlotRect(xCoord, margin, textSize.height, textSize.width); 236 237 drawRotatedText(yLabel(), 238 yLabelFont(), getColor(0, 0, 0), rect, TextAlignment.Center); 239 } 240 241 void drawExtraLines() { 242 foreach(line; extraLines) { 243 auto pen = getPen(line.lineColor, line.lineWidth); 244 scope(exit) doneWith(pen); 245 246 auto start = PlotPoint(toPixelsX(line.x1), toPixelsY(line.y1)); 247 auto end = PlotPoint(toPixelsX(line.x2), toPixelsY(line.y2)); 248 drawClippedLine(pen, start, end); 249 } 250 } 251 252 void drawAxes() { 253 immutable origin = PlotPoint(toPixelsX(leftLim), toPixelsY(lowerLim)); 254 immutable topLeft = PlotPoint(origin.x, toPixelsY(upperLim)); 255 immutable bottomRight = PlotPoint(toPixelsX(rightLim), origin.y); 256 257 drawLine(axesPen, origin, topLeft); 258 drawLine(axesPen, origin, bottomRight); 259 } 260 261 void drawTicks() { 262 auto black = getColor(0, 0, 0); 263 264 foreach(i, tickPoint; xAxisLocations) { 265 if(!(tickPoint >= leftLim && tickPoint <= rightLim)) continue; 266 auto color = (_xTickColors.length) ? _xTickColors[i] : black; 267 drawXTick(tickPoint, xAxisText[i], color); 268 } 269 270 foreach(i, tickPoint; yAxisLocations) { 271 if(!(tickPoint >= lowerLim && tickPoint <= upperLim)) continue; 272 auto color = (_yTickColors.length) ? _yTickColors[i] : black; 273 drawYTick(tickPoint, yAxisText[i], color); 274 } 275 } 276 277 void drawLegend() { 278 immutable loc = legendLocation(); 279 if(loc == LegendLocation.top || loc == LegendLocation.bottom) { 280 drawLegendImplTopBottom(); 281 } else { 282 drawLegendImplLeftRight(); 283 } 284 } 285 286 void drawLegendImplTopBottom() { 287 immutable measurements = measureLegend(); 288 if(measurements.height == 0) return; // No legend. 289 immutable rowHeight = measurements.height / measurements.nRows; 290 291 // This needs to be precomputed for centering purposes. 292 double[] rowStarts; 293 294 double curX = legendMarginHoriz; 295 296 immutable loc = legendLocation(); 297 double curY; 298 if(loc == LegendLocation.bottom) { 299 curY = this.height - measurements.height - 10; 300 } else { 301 assert(loc == LegendLocation.top); 302 curY = measureText(title(), titleFont()).height + 10; 303 } 304 305 size_t rowStartIndex = 0; 306 307 foreach(plot; plotData) { 308 if(!plot.legendText.length) continue; 309 immutable itemSize = plot.measureLegend(legendFont(), this); 310 311 if(itemSize.width + curX >= this.width - legendMarginHoriz 312 && curX > legendMarginHoriz) { 313 // Find centering. 314 auto rowSize = curX - legendMarginHoriz; 315 rowStarts ~= max(0, (this.width - rowSize) / 2); 316 curX = legendMarginHoriz; 317 } 318 319 curX += itemSize.width + legendMarginHoriz; 320 } 321 // Append last row. 322 auto rowSize = curX - legendMarginHoriz; 323 rowStarts ~= max(0, (this.width - rowSize) / 2); 324 325 curX = rowStarts[rowStartIndex]; 326 double nextX; 327 328 foreach(plot; plotData) { 329 if(!plot.legendText.length) continue; 330 331 immutable itemSize = plot.measureLegend(legendFont(), this); 332 if(itemSize.width + curX >= this.width - legendMarginHoriz 333 && curX > legendMarginHoriz) { 334 curY += rowHeight; 335 rowStartIndex++; 336 curX = rowStarts[rowStartIndex]; 337 nextX = curX; 338 } 339 340 drawLegendElem(curX, curY, plot, rowHeight); 341 curX += itemSize.width + legendMarginHoriz; 342 } 343 } 344 345 void drawLegendImplLeftRight() { 346 int nRows; 347 foreach(plot; plotData) { 348 if(plot.legendText().length) nRows++; 349 } 350 351 immutable measurements = measureLegend(); 352 immutable rowHeight = (measurements.height / measurements.nRows); 353 354 double curY = this.height / 2 - nRows * rowHeight / 2; 355 immutable loc = legendLocation(); 356 immutable x = (loc == LegendLocation.left) ? 10 : 357 (this.width - measurements.width - 10); 358 359 foreach(plot; plotData) if(plot.legendText().length) { 360 drawLegendElem(x, curY, plot, rowHeight); 361 curY += rowHeight; 362 } 363 } 364 365 void drawLegendElem(double curX, double curY, Plot plot, double rowHeight) { 366 immutable textSize = measureText(plot.legendText(), legendFont()); 367 assert(textSize.height <= rowHeight); 368 369 immutable stdLetterHeight = measureText("A", legendFont()).height; 370 immutable textX = curX + legendSymbolSize + legendSymbolTextSpace; 371 auto textRect = PlotRect( 372 textX, 373 curY + rowHeight / 2 - stdLetterHeight / 2, 374 textSize.width, 375 textSize.height 376 ); 377 drawText(plot.legendText(), legendFont(), getColor(0, 0, 0), textRect, 378 TextAlignment.Left); 379 380 auto ySlack = (stdLetterHeight - legendSymbolSize) / 2; 381 auto where = PlotRect( 382 curX, curY + rowHeight / 2 - legendSymbolSize / 2, 383 legendSymbolSize, legendSymbolSize 384 ); 385 plot.drawLegendSymbol(this, where); 386 } 387 388 // Controls the space between a tick line and the tick label. 389 enum lineLabelSpace = 2; 390 391 void drawXTick(double where, string text, Color color) { 392 immutable wherePixels = toPixelsX(where); 393 drawLine( 394 axesPen, 395 PlotPoint(wherePixels, this.height - bottomMargin), 396 PlotPoint(wherePixels, this.height - bottomMargin + tickPixels) 397 ); 398 399 if(verticalGrid()) { 400 drawLine(gridPen, 401 PlotPoint(wherePixels, topMargin), 402 PlotPoint(wherePixels, this.height - bottomMargin)); 403 } 404 405 if(nullOrInit(_axesFont)) { 406 return; 407 } 408 409 auto format = TextAlignment.Center; 410 411 immutable textSize = measureText(text, _axesFont, format); 412 immutable tickTextStart = 413 this.height - bottomMargin + tickPixels + lineLabelSpace; 414 415 if(rotatedXTick()) { 416 auto rect = PlotRect(wherePixels - tickLabelHeight / 2, 417 tickTextStart + tickPixels / 2, 418 textSize.height, 419 textSize.width 420 ); 421 422 drawRotatedText(text, _axesFont, color, rect, format); 423 } else { 424 auto rect = PlotRect(wherePixels - textSize.width / 2, 425 tickTextStart, 426 textSize.width, 427 textSize.height 428 ); 429 430 drawText(text, _axesFont, color, rect, format); 431 } 432 } 433 434 void drawYTick(double where, string text, Color color) { 435 immutable wherePixels = this.height - toPixelsY(where); 436 drawLine( 437 axesPen, 438 PlotPoint(leftMargin, this.height - wherePixels), 439 PlotPoint(leftMargin - tickPixels, this.height - wherePixels) 440 ); 441 442 if(nullOrInit(_axesFont)) { 443 return; 444 } 445 446 if(horizontalGrid()) { 447 drawLine( 448 gridPen, 449 PlotPoint(leftMargin, this.height - wherePixels), 450 PlotPoint(this.width - rightMargin, this.height - wherePixels)); 451 } 452 453 auto format = TextAlignment.Right; 454 455 immutable textSize = measureText(text, _axesFont, format); 456 if(rotatedYTick()) { 457 auto rect = PlotRect( 458 leftMargin - textSize.height - tickPixels - lineLabelSpace, 459 this.height - wherePixels - textSize.width / 2, 460 textSize.height, 461 textSize.width 462 ); 463 464 drawRotatedText(text, _axesFont, color, rect, format); 465 } else { 466 auto rect = PlotRect( 467 leftMargin - textSize.width - tickPixels - lineLabelSpace, 468 this.height - wherePixels - textSize.height / 2, 469 textSize.width, 470 textSize.height 471 ); 472 473 drawText(text, _axesFont, color, rect, format); 474 } 475 } 476 477 // Used in setupAxes() via delegate. 478 double marginSizeX() { 479 return leftMargin; 480 } 481 482 // Used in setupAxes() via delegate. 483 double marginSizeY() { 484 return topMargin + bottomMargin; 485 } 486 487 void setupAxes( 488 double lower, 489 double upper, 490 ref double[] axisLocations, 491 ref string[] axisText, 492 double axisSize, 493 ref double labelSize, 494 double delegate() marginSize 495 ) 496 in { 497 assert(upper > lower, std.conv.text(lower, '\t', upper)); 498 } body { 499 500 immutable diff = upper - lower; 501 502 double tickWidth = 10.0 ^^ floor(log10(diff)); 503 if(diff / tickWidth < 2) { 504 tickWidth /= 10; 505 } 506 507 if(diff / tickWidth > 9) { 508 tickWidth *= 2; 509 } 510 511 512 if(diff / tickWidth < 4) { 513 tickWidth /= 2; 514 } 515 516 void updateAxes() { 517 double startPoint = ceil(lower / tickWidth) * tickWidth; 518 519 // The tickWidth * 0.01 is a fudge factor to make the last tick 520 // get drawn in the presence of rounding error. 521 axisLocations = array( 522 iota(startPoint, upper + tickWidth * 0.01, tickWidth) 523 ); 524 525 axisText = doublesToStrings(axisLocations); 526 fixMargins(); 527 } 528 529 do { 530 updateAxes(); 531 532 // Prevent labels from running together on small plots. 533 if((axisSize - marginSize()) / axisLocations.length < labelSize * 4 534 && diff / tickWidth > 2) { 535 tickWidth *= 2; 536 continue; 537 } else { 538 break; 539 } 540 } while(true); 541 542 // Force at least two ticks come hell or high water. 543 while(axisLocations.length < 2) { 544 tickWidth /= 2; 545 updateAxes(); 546 } 547 } 548 549 void setLim( 550 double lower, 551 double upper, 552 ref double oldLower, 553 ref double oldUpper, 554 ) { 555 enforce(upper > lower, "Can't have upper limit < lower limit."); 556 oldLower = lower; 557 oldUpper = upper; 558 } 559 560 void nullFontsToDefaults() { 561 if(nullOrInit(titleFont())) { 562 _titleFont = getFont(plot2kill.util.defaultFont, 14 + fontSizeAdjust); 563 } 564 if(nullOrInit(xLabelFont())) { 565 _xLabelFont = getFont(plot2kill.util.defaultFont, 14 + fontSizeAdjust); 566 } 567 568 if(nullOrInit(yLabelFont())) { 569 _yLabelFont = getFont 570 (plot2kill.util.defaultFont, 14 + fontSizeAdjust); 571 } 572 573 if(nullOrInit(axesFont())) { 574 _axesFont = getFont(plot2kill.util.defaultFont, 12 + fontSizeAdjust); 575 } 576 577 if(nullOrInit(legendFont())) { 578 _legendFont = getFont(plot2kill.util.defaultFont, 12 + fontSizeAdjust); 579 } 580 } 581 582 static bool isValidPlot(Plot plot) { 583 if(plot is null) { 584 return false; 585 } 586 587 return plot.leftMost <= plot.rightMost && 588 plot.bottomMost <= plot.topMost; 589 } 590 591 package: 592 593 this() {} 594 595 this(Plot[] plots...) { 596 this(); 597 addPlot!(Figure)(plots); 598 } 599 600 // These goodies need to be known by various GUI-related code, but end 601 // users have no business fiddling with them. 602 Plot[] plotData; 603 604 double[] xAxisLocations; 605 string[] xAxisText; 606 607 double[] yAxisLocations; 608 string[] yAxisText; 609 610 double topMargin = 10; 611 double bottomMargin = 10; 612 double leftMargin = 10; 613 double rightMargin = 30; 614 615 public: 616 617 override int defaultWindowWidth() { 618 return 800; 619 } 620 621 override int defaultWindowHeight() { 622 return 600; 623 } 624 625 override int minWindowWidth() { 626 return 400; 627 } 628 629 override int minWindowHeight() { 630 return 300; 631 } 632 633 // These drawing commands aren't documented for now b/c they're subject 634 // to change. 635 636 // Returns whether any part of the rectangle is on screen. 637 bool clipRectangle(ref double x, ref double y, ref double width, ref double height) { 638 // Do clipping. 639 auto bottom = y + height; 640 auto right = x + width; 641 if(x < leftMargin) { 642 x = leftMargin; 643 } 644 if(right > this.width - rightMargin) { 645 right = this.width - rightMargin; 646 } 647 648 if(y < topMargin) { 649 y = topMargin; 650 } 651 652 if(bottom > this.height - bottomMargin) { 653 bottom = this.height - bottomMargin; 654 } 655 656 width = right - x; 657 height = bottom - y; 658 return width > 0 && height > 0; 659 } 660 661 // Convenience 662 static bool between(T, U, V)(T num, U lower, V upper) { 663 return lower <= num && num <= upper; 664 } 665 666 bool clipLine(ref double x1, ref double y1, ref double x2, ref double y2) { 667 immutable topPixel = topMargin; 668 immutable bottomPixel = this.height - bottomMargin - 1; 669 immutable leftPixel = leftMargin + 1; 670 immutable rightPixel = this.width - rightMargin; 671 672 if(between(x1, leftPixel, rightPixel) && 673 between(x2, leftPixel, rightPixel) && 674 between(y1, topPixel, bottomPixel) && 675 between(y2, topPixel, bottomPixel)) { 676 677 return true; 678 } 679 680 // Handle slope of zero or infinity as a special case. 681 if(x1 == x2) { 682 if(!between(x1, leftPixel, rightPixel)) { 683 return false; 684 } else if(y1 < topPixel && y2 < topPixel) { 685 return false; 686 } else if(y1 > bottomPixel && y2 > bottomPixel) { 687 return false; 688 } 689 690 y1 = max(y1, topPixel); 691 y1 = min(y1, bottomPixel); 692 y2 = max(y2, topPixel); 693 y2 = min(y2, bottomPixel); 694 return true; 695 } else if(y1 == y2) { 696 if(!between(y1, topPixel, bottomPixel)) { 697 return false; 698 } else if(x1 < leftPixel && x2 < leftPixel) { 699 return false; 700 } else if(x1 > rightPixel && x2 > rightPixel) { 701 return false; 702 } 703 704 x1 = max(x1, leftPixel); 705 x1 = min(x1, rightPixel); 706 x2 = max(x2, leftPixel); 707 x2 = min(x2, rightPixel); 708 return true; 709 } 710 711 immutable slope = (y2 - y1) / (x2 - x1); 712 enum tol = 0; // Compensate for rounding error. 713 714 void fixX(ref double x, ref double y) { 715 if(x < leftPixel) { 716 immutable diff = leftPixel - x; 717 x = leftPixel; 718 y = diff * slope + y; 719 } else if(x > rightPixel) { 720 immutable diff = rightPixel - x; 721 x = rightPixel; 722 y = diff * slope + y; 723 } 724 } 725 726 void fixY(ref double x, ref double y) { 727 if(y < topPixel) { 728 immutable diff = topPixel - y; 729 y = topPixel; 730 x = diff / slope + x; 731 } else if(y > bottomPixel) { 732 immutable diff = bottomPixel - y; 733 y = bottomPixel; 734 x = diff / slope + x; 735 } 736 } 737 fixX(x1, y1); 738 fixX(x2, y2); 739 fixY(x1, y1); 740 fixY(x2, y2); 741 742 // This prevents weird rounding artifacts where a line appears as a 743 // single point on the edge of the figure. 744 if(y1 == y2 && x1 == x2 && ( 745 (y1 == topPixel || y1 == bottomPixel) || 746 (x1 == leftPixel || x1 == rightPixel))) { 747 return false; 748 } 749 750 // The minuses and pluses are to deal w/ rounding error. 751 return between(x1, leftPixel - tol, rightPixel + tol) && 752 between(x2, leftPixel - tol, rightPixel + tol) && 753 between(y1, topPixel - tol, bottomPixel + tol) && 754 between(y2, topPixel - tol, bottomPixel + tol); 755 } 756 757 void drawClippedRectangle 758 (Pen pen, double x, double y, double width, double height) { 759 if(clipRectangle(x, y, width, height)) { 760 drawRectangle(pen, x, y, width, height); 761 } 762 } 763 764 void drawClippedRectangle(Pen pen, PlotRect r) { 765 drawClippedRectangle(pen, r.x, r.y, r.width, r.height); 766 } 767 768 void fillClippedRectangle 769 (Brush brush, double x, double y, double width, double height) { 770 if(clipRectangle(x, y, width, height)) { 771 fillRectangle(brush, x, y, width, height); 772 } 773 } 774 775 void fillClippedRectangle(Brush brush, PlotRect rect) { 776 fillClippedRectangle(brush, rect.x, rect.y, rect.width, rect.height); 777 } 778 779 void drawClippedLine(Pen pen, PlotPoint from, PlotPoint to) { 780 auto x1 = from.x; 781 auto y1 = from.y; 782 auto x2 = to.x; 783 auto y2 = to.y; 784 immutable shouldDraw = clipLine(x1, y1, x2, y2); 785 786 if(!shouldDraw) { 787 return; 788 } 789 790 drawLine(pen, PlotPoint(x1, y1), PlotPoint(x2, y2)); 791 } 792 793 bool insideAxes(PlotPoint point) { 794 if(between(point.x, leftMargin, this.width - rightMargin) && 795 between(point.y, topMargin, this.height - bottomMargin)) { 796 return true; 797 } else { 798 return false; 799 } 800 } 801 802 void drawClippedText(string text, Font font, 803 Color pointColor, PlotRect rect) { 804 805 // To avoid cutting points off of scatter plots, this function only 806 // checks whether the center of each point is on the graph. Therefore, 807 // it may allow points to extend slightly off the graph. This is 808 // annoying, but there's no easy way to fix it w/o risking cutting off 809 // points. 810 immutable xMid = rect.x + rect.width / 2; 811 immutable yMid = rect.y + rect.height / 2; 812 813 if(insideAxes(PlotPoint(xMid, yMid))) { 814 drawText(text, font, pointColor, rect); 815 } 816 } 817 818 /// 819 final Font axesFont()() { 820 return _axesFont; 821 } 822 823 /// 824 final This axesFont(this This)(Font newFont) { 825 _axesFont = newFont; 826 return cast(This) this; 827 } 828 829 /// 830 final Font legendFont()() { 831 return _legendFont; 832 } 833 834 /// 835 final This legendFont(this This)(Font newFont) { 836 _legendFont = newFont; 837 return cast(This) this; 838 } 839 840 /// 841 static Figure opCall()() { 842 return new Figure; 843 } 844 845 /**Convenience factory that adds all plots provided to the Figure.*/ 846 static Figure opCall()(Plot[] plots...) { 847 return new Figure(plots); 848 } 849 850 /// Ditto 851 static Figure opCall(P)(P[] plots) 852 if(is(P : Plot) && !is(P == Plot)) { 853 return new Figure(cast(Plot[]) plots); 854 } 855 856 /**Manually set the X axis limits. 857 */ 858 final This xLim(this This)(double newLower, double newUpper) { 859 setLim(newLower, newUpper, leftLim, rightLim); 860 return cast(This) this; 861 } 862 863 /**Manually set the Y axis limits. 864 */ 865 This yLim(this This)(double newLower, double newUpper) { 866 setLim(newLower, newUpper, lowerLim, upperLim); 867 return cast(This) this; 868 } 869 870 /** 871 Set the zoom back to the default value, i.e. just large enough to fit 872 everything on the screen. 873 */ 874 This defaultZoom(this This)() { 875 upperLim = -double.infinity; 876 lowerLim = double.infinity; 877 leftLim = double.infinity; 878 rightLim = -double.infinity; 879 880 foreach(plot; plotData) { 881 upperLim = max(upperLim, plot.topMost); 882 rightLim = max(rightLim, plot.rightMost); 883 leftLim = min(leftLim, plot.leftMost); 884 lowerLim = min(lowerLim, plot.bottomMost); 885 } 886 887 return cast(This) this; 888 } 889 890 /**Set the X axis labels. If text is null (default) the axis text is 891 * just the text of the axis locations. R should be any range with 892 * length identical to text (unless text is null) and elements implicitly 893 * convertible to double. If colors is empty, all labels will be made 894 * black. Otherwise it must be the same length as locations. 895 */ 896 This xTickLabels(R, this This) 897 (R locations, const string[] text = null, Color[] colors = null) 898 if(isInputRange!R && is(ElementType!R : double)) { 899 userSetXAxis = true; 900 xAxisLocations = toDoubleArray(locations); 901 enforce(colors.length == xAxisLocations.length || colors.length == 0); 902 this._xTickColors = colors.dup; 903 904 if(text.length > 0) { 905 enforce(text.length == xAxisLocations.length, 906 "Length mismatch between X axis locations and X axis text."); 907 xAxisText = text.dup; 908 } else { 909 xAxisText = doublesToStrings(xAxisLocations); 910 } 911 912 return cast(This) this; 913 } 914 915 /** 916 Resets the X tick labels to the default, effectively undoing a call to 917 xTickLabels. 918 */ 919 This defaultXTick(this This)() { 920 userSetXAxis = false; 921 return cast(This) this; 922 } 923 924 /**Set the Y axis labels. If text is null (default) the axis text is 925 * just the text of the axis locations. R should be any range with 926 * length identical to text (unless text is null) and elements implicitly 927 * convertible to double. If colors is empty, all labels will be made 928 * black. Otherwise it must be the same length as locations. 929 */ 930 This yTickLabels(R, this This) 931 (R locations, const string[] text = null, Color[] colors = null) 932 if(isInputRange!R && is(ElementType!R : double)) { 933 userSetYAxis = true; 934 yAxisLocations = toDoubleArray(locations); 935 enforce(colors.length == xAxisLocations.length || colors.length == 0); 936 this._yTickColors = colors.dup; 937 938 if(text.length > 0) { 939 enforce(text.length == yAxisLocations.length, 940 "Length mismatch between Y axis locations and Y axis text."); 941 yAxisText = text.dup; 942 } else { 943 yAxisText = doublesToStrings(yAxisLocations); 944 } 945 946 return cast(This) this; 947 } 948 949 /** 950 Resets the X tick labels to the default, effectively undoing a call to 951 xTickLabels. 952 */ 953 This defaultYTick(this This)() { 954 userSetYAxis = false; 955 return cast(This) this; 956 } 957 958 /**Determines whether vertical gridlines are drawn. Default is false.*/ 959 bool verticalGrid()() { 960 return _verticalGrid; 961 } 962 963 /// 964 This verticalGrid(this This)(bool val) { 965 this._verticalGrid = val; 966 return cast(This) this; 967 } 968 969 /**Determines whether horizontal gridlines are drawn. Default is false.*/ 970 bool horizontalGrid()() { 971 return _horizontalGrid; 972 } 973 974 /// 975 This horizontalGrid(this This)(bool val) { 976 this._horizontalGrid = val; 977 return cast(This) this; 978 } 979 980 /// Grid intensity from zero (pure white) to 255 (pure black). 981 ubyte gridIntensity()() { 982 return _gridIntensity; 983 } 984 985 /// Setter. 986 This gridIntensity(this This)(ubyte newIntensity) { 987 _gridIntensity = newIntensity; 988 return cast(This) this; 989 } 990 991 /// 992 LegendLocation legendLocation()() { 993 return _legendLoc; 994 } 995 996 /// 997 This legendLocation(this This)(LegendLocation newLoc) { 998 this._legendLoc = newLoc; 999 return cast(This) this; 1000 } 1001 1002 /** 1003 Determines whether rotated text is used for the X tick labels. 1004 */ 1005 final bool rotatedXTick()() { 1006 return _rotatedXTick; 1007 } 1008 1009 /// Setter 1010 final This rotatedXTick(this This)(bool newVal) { 1011 _rotatedXTick = newVal; 1012 return cast(This) this; 1013 } 1014 1015 /** 1016 Determines whether rotated text is used for the Y tick labels. 1017 */ 1018 final bool rotatedYTick()() { 1019 return _rotatedYTick; 1020 } 1021 1022 /// Setter 1023 final This rotatedYTick(this This)(bool newVal) { 1024 _rotatedYTick = newVal; 1025 return cast(This) this; 1026 } 1027 1028 /**The leftmost point on the figure.*/ 1029 double leftMost() { 1030 return leftLim; 1031 } 1032 1033 /**The rightmost point on the figure.*/ 1034 double rightMost() { 1035 return rightLim; 1036 } 1037 1038 /**The topmost point on the figure.*/ 1039 double topMost() { 1040 return upperLim; 1041 } 1042 1043 /**The bottommost point on the figure.*/ 1044 double bottomMost() { 1045 return lowerLim; 1046 } 1047 1048 /**Add individual lines to the figure. Coordinates are specified relative 1049 * to the plot area, not in pixels. The lines are is clipped 1050 * to the visible part of the plot area. This is useful for adding 1051 * annotation lines, as opposed to plot lines. 1052 */ 1053 This addLines(this This)(FigureLine[] lines...) { 1054 extraLines ~= lines; 1055 return cast(This) this; 1056 } 1057 1058 /**Add one or more plots to the figure.*/ 1059 This addPlot(this This)(Plot[] plots...) { 1060 foreach(plot; plots) { 1061 if(!isValidPlot(plot)) { 1062 continue; 1063 } 1064 1065 upperLim = max(upperLim, plot.topMost); 1066 rightLim = max(rightLim, plot.rightMost); 1067 leftLim = min(leftLim, plot.leftMost); 1068 lowerLim = min(lowerLim, plot.bottomMost); 1069 plotData ~= plot; 1070 } 1071 1072 return cast(This) this; 1073 } 1074 1075 /// Ditto 1076 This addPlot(this This, P)(P[] plots) 1077 if(is(P : Plot) && !is(P == Plot)) { 1078 return addPlot!(This)(cast(Plot[]) plots); 1079 } 1080 1081 /** 1082 Remove one or more plots from the figure. If the plots are not in the 1083 figure, they are silently ignored. 1084 */ 1085 This removePlot(this This)(Plot[] plots...) { 1086 void removePlotImpl(Plot p) { 1087 auto plotIndex = countUntil!"a is b"(plotData, p); 1088 if(plotIndex == -1) return; 1089 plotData = plotData[0..plotIndex] ~ plotData[plotIndex + 1..$]; 1090 } 1091 1092 foreach(p; plots) { 1093 removePlotImpl(p); 1094 } 1095 1096 upperLim = reduce!max(-double.infinity, map!"a.topMost"(plotData)); 1097 lowerLim = reduce!min(double.infinity, map!"a.bottomMost"(plotData)); 1098 rightLim = reduce!max(-double.infinity, map!"a.rightMost"(plotData)); 1099 leftLim = reduce!min(double.infinity, map!"a.leftMost"(plotData)); 1100 1101 return cast(This) this; 1102 } 1103 1104 /// Ditto 1105 This removePlot(this This, P)(P[] plots) 1106 if(is(P : Plot) && !is(P == Plot)) { 1107 return removePlot!(This)(cast(Plot[]) plots); 1108 } 1109 1110 /**Draw the plot but don't display it on screen.*/ 1111 override void drawImpl() { 1112 // This block of code adds fudge factors if leftLim == rightLim or 1113 // lowerLim == upperLim (i.e. perfectly horizontal or vertical lines). 1114 // This avoids figures that are infinitely narrow in one direction. 1115 // It resets them on exit so that if more stuff is added to this figure, 1116 // there are no unanticipated side effects of this kludge. 1117 immutable oldLeft = leftLim; 1118 immutable oldRight = rightLim; 1119 immutable oldUpper = upperLim; 1120 immutable oldLower = lowerLim; 1121 1122 scope(exit) { 1123 leftLim = oldLeft; 1124 rightLim = oldRight; 1125 upperLim = oldUpper; 1126 lowerLim = oldLower; 1127 } 1128 1129 enum fudgeFactor = 1e-5; 1130 if(leftLim == rightLim) { 1131 leftLim -= fudgeFactor; 1132 rightLim += fudgeFactor; 1133 } 1134 1135 if(upperLim == lowerLim) { 1136 upperLim += fudgeFactor; 1137 lowerLim -= fudgeFactor; 1138 } 1139 1140 auto whiteBrush = getBrush(getColor(255, 255, 255)); 1141 fillRectangle(whiteBrush, 0, 0, this.width, this.height); 1142 doneWith(whiteBrush); 1143 // If this is not a valid Figure, leave a big blank white rectangle. 1144 // It beats crashing. 1145 if(!(leftLim < rightLim && lowerLim < upperLim)) { 1146 return; 1147 } 1148 axesPen = getPen(getColor(0, 0, 0), 2); 1149 scope(exit) doneWith(axesPen); 1150 1151 auto notGridIntens = cast(ubyte) (ubyte.max - gridIntensity()); 1152 gridPen = getPen( 1153 getColor(notGridIntens, notGridIntens, notGridIntens), 1 1154 ); 1155 scope(exit) doneWith(gridPen); 1156 1157 nullFontsToDefaults(); 1158 1159 if(!userSetXAxis) { 1160 setupAxes(leftLim, rightLim, xAxisLocations, xAxisText, 1161 this.width, xTickLabelWidth, &marginSizeX); 1162 } 1163 1164 if(!userSetYAxis) { 1165 setupAxes(lowerLim, upperLim, yAxisLocations, yAxisText, 1166 this.height, tickLabelHeight, &marginSizeY); 1167 } 1168 1169 fixMargins(); 1170 drawTicks(); 1171 1172 foreach(plot; plotData) { 1173 if(!isValidPlot(plot)) { 1174 continue; 1175 } 1176 1177 immutable x = toPixelsX(plot.leftMost); 1178 immutable y = toPixelsY(plot.topMost); 1179 immutable subHeight = toPixelsY(plot.bottomMost) - y; 1180 immutable subWidth = toPixelsX(plot.rightMost) - x; 1181 plot.drawPlot(this, x, y, subWidth, subHeight); 1182 } 1183 1184 drawYlabel(); 1185 drawExtraLines(); 1186 drawAxes(); 1187 drawTitle(); 1188 drawXlabel(); 1189 drawLegend(); 1190 } 1191 1192 version(none) { 1193 void showUsingImplicitMain() { 1194 ImplicitMain.initialize(); 1195 ImplicitMain.addForm(this); 1196 } 1197 } 1198 } 1199 1200 /// 1201 enum LegendLocation { 1202 /// 1203 top, 1204 1205 /// 1206 bottom, 1207 1208 /// 1209 left, 1210 1211 /// 1212 right 1213 } 1214 1215 /**For drawing extra lines on a Figure, with coordinates specified in plot 1216 * units and relative to the plot area, not in pixels.*/ 1217 struct FigureLine { 1218 private: 1219 double x1; 1220 double y1; 1221 double x2; 1222 double y2; 1223 Color lineColor; 1224 uint lineWidth = 1; 1225 1226 public: 1227 this(double x1, double y1, double x2, 1228 double y2, Color lineColor, uint lineWidth = 1) { 1229 1230 enforce(isFinite(x1) && isFinite(x2) && isFinite(y1) && isFinite(y2), 1231 "Line coordinates must be finite."); 1232 this.x1 = x1; 1233 this.y1 = y1; 1234 this.x2 = x2; 1235 this.y2 = y2; 1236 this.lineColor = lineColor; 1237 this.lineWidth = lineWidth; 1238 } 1239 } 1240 1241 /** 1242 Most of these classes copy their input data into a double[] by default. Use 1243 this to signal that copying is unnecessary. The range primitives just forward 1244 to data. 1245 */ 1246 struct NoCopy { 1247 /// 1248 double[] data; 1249 1250 /// 1251 double front() @property { return data.front; } 1252 1253 /// 1254 void popFront() { data.popFront(); } 1255 1256 /// 1257 bool empty() @property { return data.empty; } 1258 1259 /// 1260 typeof(this) save() @property { return this; } 1261 1262 /// 1263 double opIndex(size_t index) { return data[index]; } 1264 1265 /// 1266 typeof(this) opSlice(size_t lower, size_t upper) { 1267 return NoCopy(data[lower..upper]); 1268 } 1269 } 1270 1271 private template isPlot(P) { enum isPlot = is(P : Plot); }