1 /**This module contains the Subplot object. This allows placing multiple 2 * plots on a single form in a simple grid arrangement.. 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.subplot; 33 34 import plot2kill.figure; 35 import plot2kill.util; 36 37 /**This is the GUI-agnostic base class for a Subplot. See the Subplot 38 * class, which derives from this class and has a few GUI-specific things added. 39 * 40 * Subplot objects allows for one or more subplots to be created in a single 41 * window or a single file. Each subplot is represented by a FigureBase. 42 * In the default plot window, double-clicking on any subplot zooms 43 * in on it. Double-clicking again zooms out. 44 * 45 * Examples: 46 * --- 47 * auto histFig = Histogram(someNumbers, 10).toFigure; 48 * auto scatterFig = ScatterPlot(someNumbers, someMoreNumbers).toFigure; 49 * auto sub = SubPlot(1, 2); // 1 row, 2 columns. 50 * sub.addPlot(histFig, 0, 0); // Add the histogram in the 0th row, 0th column. 51 * sub.addPlot(scatterFig, 0, 1); // Ditto. 52 * sub.showAsMain(); 53 * --- 54 */ 55 abstract class SubplotBase : FigureBase { 56 private: 57 uint nRows; 58 uint nColumns; 59 60 double topMargin = 0; 61 double bottomMargin = 0; 62 double leftMargin = 0; 63 enum int rightMargin = 10; // No label here so it can be an enum. 64 65 FigureBase[][] figs; 66 FigureBase _zoomedFigure; 67 68 invariant() { 69 assert(figs.length == nRows); 70 foreach(row; figs) { 71 assert(row.length == nColumns); 72 } 73 } 74 75 void nullFontsToDefaults() { 76 if(nullOrInit(_titleFont)) { 77 _titleFont = getFont(plot2kill.util.defaultFont, 18 + fontSizeAdjust); 78 assert(!nullOrInit(_titleFont)); 79 } 80 81 if(nullOrInit(_xLabelFont)) { 82 _xLabelFont = getFont(plot2kill.util.defaultFont, 14 + fontSizeAdjust); 83 assert(!nullOrInit(_xLabelFont)); 84 } 85 86 if(nullOrInit(_yLabelFont)) { 87 _yLabelFont = getFont(plot2kill.util.defaultFont, 14 + fontSizeAdjust); 88 assert(!nullOrInit(_yLabelFont)); 89 } 90 } 91 92 void drawLabels() { 93 // The amount of margin that's left before the labels are even drawn. 94 enum labelMargin = 10; 95 nullFontsToDefaults(); 96 if(_xLabel.length > 0) { 97 immutable textSize = measureText(_xLabel, _xLabelFont); 98 bottomMargin = textSize.height + labelMargin; 99 drawText( 100 _xLabel, _xLabelFont, getColor(0, 0, 0), 101 PlotRect(0, 102 this.height - bottomMargin, 103 this.width, textSize.height), 104 TextAlignment.Center 105 ); 106 } else { 107 bottomMargin = 0; 108 } 109 110 if(_title.length > 0) { 111 immutable textSize = measureText(_title, _titleFont); 112 topMargin = textSize.height + labelMargin; 113 drawText( 114 _title, _titleFont, getColor(0, 0, 0), 115 PlotRect(0, labelMargin, width, topMargin - labelMargin), 116 TextAlignment.Center 117 ); 118 } else { 119 topMargin = 0; 120 } 121 122 if(_yLabel.length > 0) { 123 immutable textSize = measureText(_yLabel, _yLabelFont); 124 leftMargin = textSize.height + labelMargin; 125 126 127 drawRotatedText( 128 _yLabel, _yLabelFont, getColor(0, 0, 0), 129 PlotRect(labelMargin, 0, textSize.height, this.height), 130 TextAlignment.Center 131 ); 132 } 133 } 134 135 // Gets the figure width assuming this has a width of width. 136 // The explicit parameter is necessary because this can be called 137 // at times other than during drawing. 138 double getFigWidth(double width) { 139 return (width - rightMargin - leftMargin) / nColumns; 140 } 141 142 // Ditto. 143 double getFigHeight(double height) { 144 return (height - topMargin - bottomMargin) / nRows; 145 } 146 147 148 void drawFigureZoomedOut() { 149 assert(context !is null); 150 151 fillRectangle(getBrush(getColor(255, 255, 255)), 0, 0, width, height); 152 drawLabels(); 153 154 immutable figWidth = getFigWidth(this.width); 155 immutable figHeight = getFigHeight(this.height); 156 157 foreach(rowIndex, row; figs) { 158 foreach(colIndex, fig; row) { 159 if(fig is null) { 160 continue; 161 } 162 163 immutable xPos = colIndex * figWidth + leftMargin + xOffset; 164 immutable yPos = rowIndex * figHeight + topMargin + yOffset; 165 auto whereToDraw = PlotRect(xPos, yPos, figWidth, figHeight); 166 fig.drawTo(context, whereToDraw); 167 } 168 } 169 170 // Temporary kludge: Draw labels a second time to prevent them 171 // from being cut off by plots. Linux's text measuring sucks. 172 drawLabels(); 173 } 174 175 void drawFigureZoomedIn() { 176 assert(context !is null); 177 assert(zoomedFigure !is null); 178 179 zoomedFigure.drawTo 180 (context, PlotRect(xOffset, yOffset, width, height)); 181 } 182 183 // This code is really, REALLY inefficient, but I don't care because it's 184 // safe to say N will always be tiny, and it's simple and readable. 185 void doAdd(FigureBase fig) { 186 // Search for empty slots; 187 foreach(row; figs) foreach(ref cell; row) { 188 if(cell is null) { 189 cell = fig; 190 return; 191 } 192 } 193 194 // Add a new row. 195 if(nColumns > nRows) { 196 nRows++; 197 figs ~= new FigureBase[nColumns]; 198 figs[$ - 1][0] = fig; 199 return; 200 } 201 202 // Add a new column. 203 nColumns++; 204 auto oldFigs = figs; 205 figs = new FigureBase[][](nRows, nColumns); 206 207 foreach(row; oldFigs) foreach(cell; row) { 208 doAdd(cell); 209 } 210 211 doAdd(fig); 212 } 213 214 protected: 215 216 this() {} 217 218 this(uint nRows, uint nColumns) { 219 enforce(nRows >= 1 && nColumns >= 1, 220 "Subplot figures must have at least 1 cell. Can't create a " ~ 221 " subplot of dimensions " ~ to!string(nRows) ~ "x" ~ 222 to!string(nColumns) ~ "." 223 ); 224 225 this.nRows = nRows; 226 this.nColumns = nColumns; 227 228 figs = new FigureBase[][](nRows, nColumns); 229 } 230 231 override void drawImpl() { 232 assert(context); 233 234 if(zoomedFigure is null) { 235 drawFigureZoomedOut(); 236 } else { 237 drawFigureZoomedIn(); 238 } 239 } 240 241 public: 242 243 /**Create an instance with nRows rows and nColumns columns.*/ 244 static Subplot opCall(uint nRows, uint nColumns) { 245 return new Subplot(nRows, nColumns); 246 } 247 248 /**Create an empty Subplot instance.*/ 249 static Subplot opCall() { 250 return new Subplot(); 251 } 252 253 override int defaultWindowWidth() { 254 return 1024; 255 } 256 257 override int defaultWindowHeight() { 258 return 768; 259 } 260 261 override int minWindowWidth() { 262 return 800; 263 } 264 265 override int minWindowHeight() { 266 return 600; 267 } 268 269 /**Add a figure to the subplot in the given row and column. 270 */ 271 This addFigure(this This)(FigureBase fig, uint row, uint col) { 272 enforce(row < nRows && col < nColumns, std.conv.text( 273 "Can't add a plot to cell (",row, ", ", col, ") of a ", nRows, 274 "x", nColumns, " Subplot.")); 275 276 figs[row][col] = fig; 277 return cast(This) this; 278 } 279 280 /**Add a figure to the subplot using the default layout, which is as 281 * follows: 282 * 283 * 1. Slots will be searched left-to-right, top-to-bottom, rows first. 284 * If an empty one is found, the figure will be added there. 285 * 286 * 2. If no empty slots are found and nColumns <= nRows, then another 287 * column will be added. The N existing figures will be moved such that 288 * they are the first N figures in left-to-right, top-to-bottom, 289 * rows first order and their ordering according to this predicate 290 * does not change. The figure to be added will be the last figure 291 * according to this predicate. 292 * 293 * 3. If no empty slots are found and nColumns > nRows, a new row will 294 * be created and the figure will be stored as the first element in 295 * that row. 296 * 297 * Notes: If you add figures with the overload that allows explicit row and 298 * column specification, and then call this overload, the coordinates 299 * of previously added figures may be changed. 300 * 301 * If you pass multiple figures, they are simply added iteratively 302 * according to these rules. 303 */ 304 This addFigure(this This)(FigureBase[] toAdd...) { 305 foreach(fig; toAdd) { 306 doAdd(fig); 307 } 308 309 return cast(This) this; 310 } 311 312 /// Ditto 313 This addFigure(this This, F)(F[] toAdd) 314 if(is(F : FigureBase) && !is(F == FigureBase)) { 315 // I have no idea why forwarding to the overload doesn't work. 316 // Must be some obscure DMD bug. 317 foreach(fig; toAdd) { 318 doAdd(fig); 319 } 320 321 return cast(This) this; 322 } 323 324 /** 325 Returns the zoomed figure, or null if no figure is currently zoomed. 326 */ 327 FigureBase zoomedFigure() { 328 return _zoomedFigure; 329 } 330 }; 331 332 version(dfl) { 333 334 import dfl.form, dfl.label, dfl.control, dfl.event, dfl.picturebox, dfl.base, 335 dfl.application; 336 337 /// 338 class Subplot : SubplotBase { 339 340 private this(uint nRows, uint nColumns) { 341 super(nRows, nColumns); 342 } 343 344 private this() { 345 super(); 346 } 347 348 /// 349 override FigureControl toControl() { 350 return new SubplotControl(this); 351 } 352 353 /// 354 override void showAsMain() { 355 Application.run(new DefaultPlotWindow(this.toControl)); 356 } 357 } 358 359 /* This class is an implementation detail. All public code should use it as 360 * its base class. It's very tightly coupled to the Subplot class because 361 * it contains behavior that doesn't make any sense to expose in a more 362 * transparent way. 363 */ 364 package class SubplotControl : FigureControl { 365 366 this(Subplot sp) { 367 super(sp); 368 // this.doubleClick ~= &zoomEvent; 369 this.size = Size(1024, 768); 370 this.mouseDown ~= &zoomEvent; 371 } 372 373 /* Returns the FigureBase, downcast to a Subplot. This is safe because our 374 * C'tor only accepts Subplots. 375 */ 376 Subplot subplot() { 377 auto ret = cast(Subplot) figure; 378 379 // Safeguard in case this gets refactored and our assumptions break: 380 assert(ret); 381 return ret; 382 } 383 384 FigureBase getFigureAt(double x, double y, Subplot sp) { 385 with(sp) { 386 if(x < leftMargin || y < topMargin) { 387 return null; 388 } 389 390 immutable figWidth = getFigWidth(this.width); 391 immutable figHeight = getFigHeight(this.height); 392 393 394 immutable xCoord = to!int((x - leftMargin) / figWidth); 395 immutable yCoord = to!int((y - topMargin) / figHeight); 396 if(xCoord < nColumns && yCoord < nRows) { 397 return figs[yCoord][xCoord]; 398 } else { 399 return null; 400 } 401 } 402 } 403 404 // Handles zooming in on double click. 405 void zoomEvent(Control c, MouseEventArgs ea) { 406 auto sp = subplot(); 407 408 if(ea.button != MouseButtons.LEFT || ea.clicks != 2) { 409 return; 410 } 411 412 with(sp) { 413 if(_zoomedFigure is null) { 414 auto toZoom = getFigureAt(ea.x, ea.y, subplot()); 415 if(toZoom !is null) { 416 _zoomedFigure = toZoom; 417 draw(); 418 } 419 } else if(cast(Subplot) _zoomedFigure) { 420 // Support multilevel zoom. 421 auto toZoom = getFigureAt(ea.x, ea.y, 422 cast(Subplot) _zoomedFigure); 423 424 // If toZoom is null, that's fine. Just zoom out. 425 _zoomedFigure = toZoom; 426 draw(); 427 } else { 428 _zoomedFigure = null; 429 draw(); 430 } 431 } 432 } 433 } 434 435 } else { 436 437 import gdk.Event, gtk.DrawingArea, gtk.Widget; 438 439 /// 440 class Subplot : SubplotBase { 441 442 private this(uint nRows, uint nColumns) { 443 super(nRows, nColumns); 444 } 445 446 private this() { 447 super(); 448 } 449 450 /// 451 override FigureWidget toWidget() { 452 plot2kill.gtkwrapper.defaultInit(); 453 return new SubplotWidget(this); 454 } 455 } 456 457 /* This class is an implementation detail. All public code should use it as 458 * its base class. It's very tightly coupled to the Subplot class because 459 * it contains bejavior that doesn't make any sense to expose in a more 460 * transparent way. 461 */ 462 package class SubplotWidget : FigureWidget { 463 464 this(Subplot sp) { 465 super(sp); 466 this.addOnButtonPress(&zoomEvent); 467 //this.addOnExpose(&onDrawingExpose); 468 this.setSizeRequest(800, 600); 469 } 470 471 /* Returns the FigureBase, downcast to a Subplot. This is safe because our 472 * C'tor only accepts Subplots. 473 */ 474 Subplot subplot() { 475 auto ret = cast(Subplot) figure; 476 477 // Safeguard in case this gets refactored and our assumptions break: 478 assert(ret); 479 return ret; 480 } 481 482 // bool onDrawingExpose(GdkEventExpose* event, Widget drawingArea) { 483 // draw(); 484 // return true; 485 // } 486 487 FigureBase getFigureAt(double x, double y, Subplot sp) { 488 with(sp) { 489 if(x < leftMargin || y < topMargin) { 490 return null; 491 } 492 493 immutable figWidth = getFigWidth(this.getWidth); 494 immutable figHeight = getFigHeight(this.getHeight); 495 496 497 immutable xCoord = to!int((x - leftMargin) / figWidth); 498 immutable yCoord = to!int((y - topMargin) / figHeight); 499 if(xCoord < nColumns && yCoord < nRows) { 500 return figs[yCoord][xCoord]; 501 } else { 502 return null; 503 } 504 } 505 } 506 507 // Handles zooming in on double click. 508 bool zoomEvent(Event event, Widget widget) { 509 auto sp = subplot(); 510 auto press = event.button; 511 512 with(sp) { 513 if(press.type != GdkEventType.DOUBLE_BUTTON_PRESS 514 || press.button != 1) { 515 return false; 516 } 517 518 if(_zoomedFigure is null) { 519 auto toZoom = getFigureAt(press.x, press.y, sp); 520 if(toZoom !is null) { 521 _zoomedFigure = toZoom; 522 draw(); 523 } 524 } else if(cast(Subplot) _zoomedFigure) { 525 // Support multilevel zoom. 526 auto toZoom = getFigureAt(press.x, press.y, 527 cast(Subplot) _zoomedFigure); 528 529 // If toZoom is null, that's fine. Just zoom out. 530 _zoomedFigure = toZoom; 531 draw(); 532 } else { 533 _zoomedFigure = null; 534 draw(); 535 } 536 537 return true; 538 } 539 } 540 } 541 }