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