1########################################################################
2##
3## Copyright (C) 1995-2021 The Octave Project Developers
4##
5## See the file COPYRIGHT.md in the top-level directory of this
6## distribution or <https://octave.org/copyright/>.
7##
8## This file is part of Octave.
9##
10## Octave is free software: you can redistribute it and/or modify it
11## under the terms of the GNU General Public License as published by
12## the Free Software Foundation, either version 3 of the License, or
13## (at your option) any later version.
14##
15## Octave is distributed in the hope that it will be useful, but
16## WITHOUT ANY WARRANTY; without even the implied warranty of
17## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18## GNU General Public License for more details.
19##
20## You should have received a copy of the GNU General Public License
21## along with Octave; see the file COPYING.  If not, see
22## <https://www.gnu.org/licenses/>.
23##
24########################################################################
25
26## -*- texinfo -*-
27## @deftypefn  {} {} subplot (@var{rows}, @var{cols}, @var{index})
28## @deftypefnx {} {} subplot (@var{rows}, @var{cols}, @var{index}, @var{hax})
29## @deftypefnx {} {} subplot (@var{rcn})
30## @deftypefnx {} {} subplot (@var{hax})
31## @deftypefnx {} {} subplot (@dots{}, "align")
32## @deftypefnx {} {} subplot (@dots{}, "replace")
33## @deftypefnx {} {} subplot ("position", @var{pos})
34## @deftypefnx {} {} subplot (@dots{}, @var{prop}, @var{val}, @dots{})
35## @deftypefnx {} {@var{hax} =} subplot (@dots{})
36## Set up a plot grid with @var{rows} by @var{cols} subwindows and set the
37## current axes for plotting (@code{gca}) to the location given by @var{index}.
38##
39## If an axes handle @var{hax} is provided after the (@var{rows}, @var{cols},
40## @var{index}) arguments, the corresponding axes is turned into a
41## subplot.
42##
43## If only one numeric argument is supplied, then it must be a three digit
44## value specifying the number of rows in digit 1, the number of columns in
45## digit 2, and the plot index in digit 3.
46##
47## The plot index runs row-wise; First, all columns in a row are numbered
48## and then the next row is filled.
49##
50## For example, a plot with 2x3 grid will have plot indices running as follows:
51## @tex
52## \vskip 10pt
53## \hfil\vbox{\offinterlineskip\hrule
54## \halign{\vrule#&&\qquad\hfil#\hfil\qquad\vrule\cr
55## height13pt&1&2&3\cr height12pt&&&\cr\noalign{\hrule}
56## height13pt&4&5&6\cr height12pt&&&\cr\noalign{\hrule}}}
57## \hfil
58## \vskip 10pt
59## @end tex
60## @ifnottex
61##
62## @example
63## @group
64## +-----+-----+-----+
65## |  1  |  2  |  3  |
66## +-----+-----+-----+
67## |  4  |  5  |  6  |
68## +-----+-----+-----+
69## @end group
70## @end example
71##
72## @end ifnottex
73##
74## @var{index} may also be a vector.  In this case, the new axes will enclose
75## the grid locations specified.  The first demo illustrates this:
76##
77## @example
78## demo ("subplot", 1)
79## @end example
80##
81## The index of the subplot to make active may also be specified by its axes
82## handle, @var{hax}, returned from a previous @code{subplot} command.
83##
84## If the option @qcode{"align"} is given then the plot boxes of the subwindows
85## will align, but this may leave no room for axes tick marks or labels.
86##
87## If the option @qcode{"replace"} is given then the subplot axes will be
88## reset, rather than just switching the current axes for plotting to the
89## requested subplot.
90##
91## The @qcode{"position"} property can be used to exactly position the subplot
92## axes within the current figure.  The option @var{pos} is a 4-element vector
93## [x, y, width, height] that determines the location and size of the axes.
94## The values in @var{pos} are normalized in the range [0,1].
95##
96## Any property/value pairs are passed directly to the underlying axes object.
97## The full list of properties is documented at @ref{Axes Properties}.
98##
99## Any previously existing axes that would be (partly) covered by the newly
100## created axes are deleted.
101##
102## If the output @var{hax} is requested, subplot returns the axes handle for
103## the subplot.  This is useful for modifying the properties of a subplot
104## using @code{set}.
105##
106## Under some circumstances, @code{subplot} might not be able to identify axes
107## that it could re-use and might replace them.  If @code{subplot} axes
108## should be referenced repeatedly, consider creating and storing their axes
109## handles beforehand instead of calling @code{subplot} repeatedly for the same
110## position.
111##
112## Example:
113##
114## @example
115## @group
116## x = 1:10;
117## y = rand (16, 10);
118## for i_plot = 1:4
119##   hax(i_plot) = subplot (2, 2, i_plot);
120##   hold (hax(i_plot), "on");
121##   grid (hax(i_plot), "on");
122## endfor
123## for i_loop = 1:2
124##   for i_plot = 1:4
125##     iy = (i_loop - 1)*4 + i_plot;
126##     plotyy (hax(i_plot), x,y(iy,:), x,y(iy+1,:));
127##   endfor
128##  endfor
129## @end group
130## @end example
131##
132## @seealso{axes, plot, gca, set}
133## @end deftypefn
134
135function h = subplot (varargin)
136
137  align_axes = false;
138  replace_axes = false;
139  have_position = false;
140  initial_args_decoded = false;
141  make_subplot = false;
142  hsubplot = [];
143
144  if (nargin >= 3)
145    ## R, C, N?
146    arg1 = varargin{1};
147    arg2 = varargin{2};
148    arg3 = varargin{3};
149    if (   isnumeric (arg1) && isscalar (arg1)
150        && isnumeric (arg2) && isscalar (arg2)
151        && isnumeric (arg3))
152      rows = arg1;
153      cols = arg2;
154      index = arg3;
155      if (nargin > 3 && isaxes (varargin{4}))
156        make_subplot = true;
157        hsubplot = varargin{4};
158        varargin(1:4) = [];
159      else
160        varargin(1:3) = [];
161      endif
162      initial_args_decoded = true;
163    endif
164  endif
165
166  if (! initial_args_decoded && nargin > 1)
167    ## check for "position", pos, ...
168    if (strcmpi (varargin{1}, "position"))
169      arg = varargin{2};
170      if (isnumeric (arg) && numel (arg) == 4)
171        pos = arg;
172        varargin(1:2) = [];
173        have_position = true;
174        initial_args_decoded = true;
175      else
176        error ("subplot: POSITION must be a 4-element numeric array");
177      endif
178    endif
179  endif
180
181  if (! initial_args_decoded && nargin > 0)
182    arg = varargin{1};
183    if (nargin == 1 && isaxes (arg))
184      ## Axes handle
185      axes (arg);
186      cf = get (0, "currentfigure");
187      set (cf, "nextplot", "add");
188      return;
189    elseif (isscalar (arg) && arg >= 0)
190      ## RCN?
191      index = rem (arg, 10);
192      arg = (arg - index) / 10;
193      cols = rem (arg, 10);
194      arg = (arg - cols) / 10;
195      rows = rem (arg, 10);
196      varargin(1) = [];
197      initial_args_decoded = true;
198    else
199      error ("subplot: invalid axes handle or RCN argument");
200    endif
201  endif
202
203  if (! initial_args_decoded)
204    print_usage ();
205  endif
206
207  if (! have_position)
208    cols = round (cols);
209    rows = round (rows);
210    index = round (index);
211
212    if (any (index < 1) || any (index > rows*cols))
213      error ("subplot: INDEX value must be >= 1 and <= ROWS*COLS");
214    endif
215
216    if (rows < 1 || cols < 1 || index < 1)
217      error ("subplot: ROWS, COLS, and INDEX must be positive");
218    endif
219  endif
220
221  ## Process "align" and "replace" options
222  idx = strcmpi (varargin, "align");
223  if (any (idx))
224    align_axes = true;
225    varargin(idx) = [];
226  endif
227
228  idx = strcmpi (varargin, "replace");
229  if (any (idx))
230    replace_axes = true;
231    varargin(idx) = [];
232  endif
233
234  axesunits = get (0, "defaultaxesunits");
235  cf = gcf ();
236  figureunits = get (cf, "units");
237  unwind_protect
238    set (0, "defaultaxesunits", "normalized");
239    set (cf, "units", "pixels");
240
241    ## FIXME: At the moment we force gnuplot to use the aligned mode
242    ##        which will set "activepositionproperty" to "position".
243    ##        This can yield to text overlap between labels and titles.
244    ##        See bug #31610.
245    if (strcmp (get (cf, "__graphics_toolkit__"), "gnuplot"))
246      align_axes = true;
247    endif
248
249    if (! have_position)
250      ## Subplots that cover more that one base subplot are not updated
251      align_axes = (align_axes || (! isscalar (index)));
252      ## Normal case where subplot indices have been given
253      [pos, opos, li] = subplot_position (cf, rows, cols, index);
254    else
255      ## Position is specified by the user.
256      li = zeros (1,4);
257      align_axes = true;
258    endif
259
260    set (cf, "nextplot", "add");
261
262    if (! make_subplot)
263      found = false;
264      kids = get (cf, "children");
265      for child = kids(:)'
266        ## Check whether this child is still valid; this might not be the
267        ## case anymore due to the deletion of previous children (due to
268        ## "deletefcn" callback or for legends/colorbars that are deleted
269        ## with their corresponding axes).
270        if (! ishghandle (child))
271          continue;
272        endif
273        if (strcmp (get (child, "type"), "axes"))
274          ## Skip legend and colorbar objects.
275          if (any (strcmp (get (child, "tag"), {"legend", "colorbar"})))
276            continue;
277          endif
278
279          if (! replace_axes)
280            if (isappdata (child, "__subplotposition__"))
281              objpos = getappdata (child, "__subplotposition__");
282            else
283              objpos = get (child, "position");
284            endif
285            if (all (abs (objpos - pos) < eps))
286              ## If the new axes are in exactly the same position
287              ## as an existing axes object, or if they share the same
288              ## appdata "__subplotposition__", use the existing axes.
289              found = true;
290              hsubplot = child;
291            else
292              ## Check if this axes is a subplot with the same layout and
293              ## index as the requested one
294              rcn = getappdata (child, "__subplotrcn__");
295              if (all (size (rcn) == [1 3])
296                  && rcn{1} == rows && rcn{2} == cols && all (rcn{3} == index))
297                found = true;
298                hsubplot = child;
299              endif
300            endif
301          endif
302
303          if (! found)
304            ## If the new axes overlap an old axes object, delete the old axes.
305            objpos = get (child, "position");
306
307            x0 = pos(1);
308            x1 = x0 + pos(3);
309            y0 = pos(2);
310            y1 = y0 + pos(4);
311            objx0 = objpos(1);
312            objx1 = objx0 + objpos(3);
313            objy0 = objpos(2);
314            objy1 = objy0 + objpos(4);
315            if (! (x0 >= objx1 || x1 <= objx0 || y0 >= objy1 || y1 <= objy0))
316              delete (child);
317            endif
318          endif
319        endif
320      endfor
321    else
322      found = true;
323    endif
324
325    if (found && ! make_subplot)
326      ## Switch to existing subplot and set requested properties
327      set (cf, "currentaxes", hsubplot);
328      if (! isempty (varargin))
329        set (hsubplot, varargin{:});
330      endif
331    else
332      pval = [{"activepositionproperty", "position", ...
333               "position", pos, "looseinset", li} varargin];
334      if (! make_subplot)
335        hsubplot = axes (pval{:});
336      else
337        set (hsubplot, pval{:})
338      endif
339
340      if (! align_axes)
341        ## base position (no ticks, no annotation, no cumbersome neighbor)
342        setappdata (hsubplot, "__subplotposition__", pos);
343        ## max outerposition
344        setappdata (hsubplot, "__subplotouterposition__", opos);
345        setappdata (hsubplot, "__subplotrcn__", {rows, cols, index});
346        addlistener (hsubplot, "outerposition", @subplot_align);
347        addlistener (hsubplot, "xaxislocation", @subplot_align);
348        addlistener (hsubplot, "yaxislocation", @subplot_align);
349        addlistener (hsubplot, "position", {@subplot_align, true});
350        subplot_align (hsubplot);
351      endif
352
353    endif
354  unwind_protect_cleanup
355    set (0, "defaultaxesunits", axesunits);
356    set (cf, "units", figureunits);
357  end_unwind_protect
358
359  if (nargout > 0)
360    h = hsubplot;
361  endif
362
363endfunction
364
365function [pos, opos, li] = subplot_position (hf, nrows, ncols, idx)
366
367  if (nrows == 1 && ncols == 1)
368    ## Trivial result for subplot (1,1,1)
369    pos = get (0, "defaultaxesposition");
370    opos = get (0, "defaultaxesouterposition");
371    li = get (0, "defaultaxeslooseinset");
372    return;
373  endif
374
375  ## Row/Column inside the axes array
376  row = ceil (idx / ncols);
377  col = idx .- (row - 1) * ncols;
378  row = [min(row) max(row)];
379  col = [min(col) max(col)];
380
381  ## Minimal margins around subplots defined in points
382  fig_units = get (hf, "units");
383  set (hf, "units", "points");
384  pts_size = get (gcf, "position")(3:4);
385  xbasemargin = 6 / pts_size(1);
386  ybasemargin = 6 / pts_size(2);
387
388  ## Column/row separation
389  margin.column = .2 / ncols + 2 * xbasemargin;
390  margin.row = .2 / nrows + 2 * ybasemargin;
391
392  set (hf, "units", fig_units);
393  margin.left = xbasemargin;
394  margin.right = xbasemargin;
395  margin.bottom = ybasemargin;
396  margin.top = ybasemargin;
397
398  ## Boundary axes have default margins
399  borders = get (0, "defaultaxesposition");
400  if (col(1) == 1)
401    margin.left = borders(1);
402  else
403    margin.left = margin.column - margin.right;
404  endif
405  if (col(2) == ncols)
406    margin.right = 1 - borders(1) - borders(3);
407  endif
408
409
410  if (row(2) == nrows)
411    margin.bottom = borders(2);
412  else
413    margin.bottom = margin.row - margin.top;
414  endif
415  if (row(1) == 1)
416    margin.top = 1 - borders(2) - borders(4);
417  endif
418
419
420  ## Compute base width and height
421  width = (borders(3) - (ncols - 1) * margin.column) / ncols;
422  height = (borders(4) - (nrows - 1) * margin.row) /nrows;
423
424  ## Position, outerposition and looseinset
425  x0 = borders(1) + (col(1) - 1) * (width + margin.column);
426  y0 = borders(2) + (nrows - row(2)) * (height + margin.row);
427  width += diff (col) * (width + margin.column);
428  height += diff (row) * (height + margin.row);
429
430  pos = [x0 y0 width height];
431  opos = [(x0 - margin.left), (y0 - margin.bottom), ...
432          (width + margin.left + margin.right), ...
433          (height + margin.bottom + margin.top)];
434  li = [margin.left, margin.bottom, margin.right, margin.top];
435
436endfunction
437
438function subplot_align (h, d, rmupdate = false)
439  persistent updating = false;
440
441  if (! updating)
442    if (rmupdate)
443      ## The "position" property has been changed from outside this routine.
444      ## Don't update anymore.
445      if (isappdata (h, "__subplotposition__"))
446        rmappdata (h, "__subplotposition__");
447        rmappdata (h, "__subplotouterposition__");
448      endif
449      return
450    endif
451
452    unwind_protect
453      updating = true;
454      hf = ancestor (h, "figure");
455      children = get (hf, "children");
456
457      ## Base position of the subplot
458      pos = getappdata (children, "__subplotposition__");
459
460      if (iscell (pos))
461        do_align = ! cellfun (@isempty, pos);
462        pos = cell2mat (pos(do_align));
463      else
464        return
465      endif
466      hsubplots = children(do_align);
467
468
469      ## There may be mixed subplot series (e.g., 2-by-6 and 1-by-6) in
470      ## the same figure.  Only subplots that have the same width and
471      ## height as this one are updated.
472      if (any (h == hsubplots))
473        width = pos(h == hsubplots, 3);
474        height = pos(h == hsubplots, 4);
475        do_align = (pos(:,3) == width) & (pos(:,4) == height);
476        hsubplots(! do_align) = [];
477        pos(! do_align,:) = [];
478      else
479        return
480      endif
481
482      ## Reset outerpositions to their default value
483      opos = getappdata (hsubplots, "__subplotouterposition__");
484      if (iscell (opos))
485        opos = cell2mat (opos);
486      endif
487      for ii = 1:numel (hsubplots)
488        set (hsubplots(ii), "outerposition", opos(ii,:), ...
489             "activepositionproperty", "position");
490      endfor
491
492      ## Compare current positions to default and compute the new ones
493      curpos = get (hsubplots, "position");
494      if (iscell (curpos))
495        curpos = cell2mat (curpos);
496      endif
497      dx0 = max (curpos(:,1) - pos(:,1));
498      dx0(dx0<0) = 0;
499      dx1 = max ((pos(:,1) + pos(:,3)) - (curpos(:,1) + curpos(:,3)));
500      dx1(dx1<0) = 0;
501      dy0 = max (curpos(:,2) - pos(:,2));
502      dy0(dy0<0) = 0;
503      dy1 = max ((pos(:,2) + pos(:,4)) - (curpos(:,2) + curpos(:,4)));
504      dy1(dy1<0) = 0;
505
506      pos(:,1) += dx0;
507      pos(:,2) += dy0;
508      pos(:,3) -= dx0 + dx1;
509      pos(:,4) -= dy0 + dy1;
510
511      for ii = 1:numel (hsubplots)
512        set (hsubplots(ii), "position", pos(ii,:));
513      endfor
514
515    unwind_protect_cleanup
516      updating = false;
517    end_unwind_protect
518  endif
519
520endfunction
521
522
523%!demo
524%! clf;
525%! r = 3;
526%! c = 3;
527%! fmt = {"horizontalalignment", "center", "verticalalignment", "middle"};
528%! for n = 1 : r*c
529%!   subplot (r, c, n);
530%!    xlabel (sprintf ("xlabel #%d", n));
531%!    ylabel (sprintf ("ylabel #%d", n));
532%!    title (sprintf ("title #%d", n));
533%!    text (0.5, 0.5, sprintf ("subplot(%d,%d,%d)", r, c, n), fmt{:});
534%!    axis ([0 1 0 1]);
535%! endfor
536%! subplot (r, c, 1:3);
537%!  xlabel (sprintf ("xlabel #%d:%d", 1, 3));
538%!  ylabel (sprintf ("ylabel #%d:%d", 1, 3));
539%!  title (sprintf ("title #%d:%d", 1, 3));
540%!  text (0.5, 0.5, sprintf ("subplot(%d,%d,%d:%d)", r, c, 1, 3), fmt{:});
541%! axis ([0 1 0 1]);
542
543%!demo
544%! clf;
545%! x = 0:1;
546%! for n = 1:4
547%!   subplot (2,2,n, "align");
548%!    plot (x, x);
549%!    xlabel (sprintf ("xlabel (2,2,%d)", n));
550%!    ylabel (sprintf ("ylabel (2,2,%d)", n));
551%!    title (sprintf ("title (2,2,%d)", n));
552%! endfor
553%! subplot (1,2,1, "align");
554%!  plot (x, x);
555%!  xlabel ("xlabel (1,2,1)");
556%!  ylabel ("ylabel (1,2,1)");
557%!  title ("title (1,2,1)");
558
559%!demo
560%! clf;
561%! x = 0:10;
562%! ax(1) = subplot (221);
563%! set (ax(1), "tag", "1");
564%! plot (x, rand (3, 11));
565%! title ("x & y labels & ticklabels");
566%! xlabel xlabel;
567%! ylabel ylabel;
568%! ax(2) = subplot (222);
569%! set (ax(2), "tag", "2");
570%! plot (x, rand (3, 11));
571%! title ("no labels");
572%! axis ("nolabel","tic");
573%! ax(3) = subplot (223);
574%! set (ax(3), "tag", "3");
575%! plot (x, rand (3, 11));
576%! title ("no labels");
577%! axis ("nolabel","tic");
578%! ax(4) = subplot (224);
579%! set (ax(4), "tag", "4");
580%! plot (x, rand (3, 11));
581%! title ("x & y labels & ticklabels");
582%! xlabel xlabel;
583%! ylabel ylabel;
584
585%!demo
586%! clf;
587%! x = 0:10;
588%! subplot (221);
589%!  plot (x, rand (3, 11));
590%!  ylim ([0, 1]);
591%!  text (0.5, 0.5, "{x,y}labels & {x,y}ticklabels", ...
592%!                  "horizontalalignment", "center", ...
593%!                  "units", "normalized");
594%!  xlabel xlabel;
595%!  ylabel ylabel;
596%!  title title;
597%! subplot (222);
598%!  plot (x, rand (3, 11));
599%!  axis ("labely");
600%!  ylabel ylabel;
601%!  text (0.5, 0.5, "no xlabels, xticklabels", ...
602%!                  "horizontalalignment", "center", ...
603%!                  "units", "normalized");
604%! subplot (223);
605%!  plot (x, rand (3, 11));
606%!  axis ("labelx");
607%!  text (0.5, 0.5, "no ylabels, yticklabels", ...
608%!                  "horizontalalignment", "center", ...
609%!                  "units", "normalized");
610%!  xlabel xlabel;
611%!  title title;
612%! subplot (224);
613%!  plot (x, rand (3, 11));
614%!  axis ("nolabel", "tic");
615%!  text (0.5, 0.5, "no {x,y}labels, {x,y}ticklabels", ...
616%!                  "horizontalalignment", "center", ...
617%!                  "units", "normalized");
618
619## Test recognition/deletion of previous axes
620## Default mode
621%!test
622%! hf = figure ("visible", "off");
623%! unwind_protect
624%!   for ii = 1:9
625%!     hax(ii) = subplot (3,3,ii);
626%!   endfor
627%!   subplot (3,3,1);
628%!   assert (gca (), hax(1));
629%!   subplot (2,1,1);
630%!   assert (ishghandle (hax),[false(1,6), true(1,3)]);
631%! unwind_protect_cleanup
632%!   delete (hf);
633%! end_unwind_protect
634
635## Position mode
636%!test
637%! hf = figure ("visible", "off");
638%! unwind_protect
639%!   h1 = subplot ("position", [0.1 0.1 0.3 0.3]);
640%!   h2 = subplot ("position", [0.5 0.5 0.3 0.3]);
641%!   subplot ("position", [0.1 0.1 0.3 0.3]);
642%!   assert (gca (), h1);
643%!   subplot ("position", [0.5 0.5 0.3 0.3]);
644%!   assert (gca (), h2);
645%!   subplot ("position", [0.5 0.5 0.3 0.2]);
646%!   assert (! ishghandle (h2));
647%! unwind_protect_cleanup
648%!   delete (hf);
649%! end_unwind_protect
650
651## Align mode
652%!test
653%! hf = figure ("visible", "off");
654%! unwind_protect
655%!   h1 = subplot (3,5,1, "align");
656%!   h2 = subplot (3,5,2, "align");
657%!   subplot (3,5,1, "align");
658%!   assert (gca (), h1);
659%!   subplot (3,2,1, "align");
660%!   assert (! ishghandle (h1));
661%!   assert (! ishghandle (h2));
662%! unwind_protect_cleanup
663%!   delete (hf);
664%! end_unwind_protect
665