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 }