view scripts/plot/draw/polar.m @ 32062:ada96a467a28

quiver: Improve plotting with non-float numeric inputs (bug #59695) * scripts/plot/draw/private/__quiver__.m: Change firstnonnumeric check to look for char instead of numeric to allow for logical inputs. Recast all inputs up to firstnonnumeric as doubles. Check if firstnonnumeric element is 'off' and if so set scale factor to 0 and increment firstnonnumeric. * scripts/plot/draw/quiver.m: Update docstring to include scaling factor option 'off'. Add BIST for int and logical input types. * scripts/plot/draw/quiver3.m: Update docstring to include scaling factor option 'off'. Add BISTs for too-few inputs. * etc/NEWS.9.md: Appended details of changes to quiver note under General Improvements and noted it also applies to quiver3.
author Nicholas R. Jankowski <jankowski.nicholas@gmail.com>
date Wed, 26 Apr 2023 17:18:50 -0400
parents 597f3ee61a48
children a3e17876a05b
line wrap: on
line source

########################################################################
##
## Copyright (C) 1993-2023 The Octave Project Developers
##
## See the file COPYRIGHT.md in the top-level directory of this
## distribution or <https://octave.org/copyright/>.
##
## This file is part of Octave.
##
## Octave is free software: you can redistribute it and/or modify it
## under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## Octave is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Octave; see the file COPYING.  If not, see
## <https://www.gnu.org/licenses/>.
##
########################################################################

## -*- texinfo -*-
## @deftypefn  {} {} polar (@var{theta}, @var{rho})
## @deftypefnx {} {} polar (@var{theta}, @var{rho}, @var{fmt})
## @deftypefnx {} {} polar (@var{cplx})
## @deftypefnx {} {} polar (@var{cplx}, @var{fmt})
## @deftypefnx {} {} polar (@var{hax}, @dots{})
## @deftypefnx {} {@var{h} =} polar (@dots{})
## Create a 2-D plot from polar coordinates @var{theta} and @var{rho}.
##
## The input @var{theta} is assumed to be radians and is converted to degrees
## for plotting.  If you have degrees then you must convert
## (@pxref{XREFcart2pol,,@code{cart2pol}}) to radians before passing the
## data to this function.
##
## If a single complex input @var{cplx} is given then the real part is used
## for @var{theta} and the imaginary part is used for @var{rho}.
##
## The optional argument @var{fmt} specifies the line format in the same way
## as @code{plot}.
##
## If the first argument @var{hax} is an axes handle, then plot into this axes,
## rather than the current axes returned by @code{gca}.
##
## The optional return value @var{h} is a graphics handle to the created plot.
##
## Implementation Note: The polar axis is drawn using line and text objects
## encapsulated in an hggroup.  The hggroup properties are linked to the
## original axes object such that altering an appearance property, for example
## @code{fontname}, will update the polar axis.  Two new properties are
## added to the original axes--@code{rtick}, @code{ttick}--which replace
## @code{xtick}, @code{ytick}.  The first is a list of tick locations in the
## radial (rho) direction; The second is a list of tick locations in the
## angular (theta) direction specified in degrees, i.e., in the range 0--359.
## @seealso{rose, compass, plot, cart2pol}
## @end deftypefn

function h = polar (varargin)

  [hax, varargin, nargs] = __plt_get_axis_arg__ ("polar", varargin{:});

  if (nargs < 1)
    print_usage ();
  endif

  oldfig = [];
  if (! isempty (hax))
    oldfig = get (0, "currentfigure");
  endif
  unwind_protect
    hax = newplot (hax);

    if (nargs == 3)
      if (! ischar (varargin{3}))
        error ("polar: FMT argument must be a string");
      endif
      htmp = __plr2__ (hax, varargin{:});
      maxr = max (abs (varargin{2}(:)));
    elseif (nargs == 2)
      if (ischar (varargin{2}))
        htmp = __plr1__ (hax, varargin{:});
        if (iscomplex (varargin{1}))
          maxr = max (abs (imag (varargin{1})(:)));
        else
          maxr = max (abs (varargin{1}(:)));
        endif
      else
        fmt = "";
        htmp = __plr2__ (hax, varargin{:}, fmt);
        maxr = max (abs (varargin{2}(:)));
      endif
    elseif (nargs == 1)
      fmt = "";
      htmp = __plr1__ (hax, varargin{:}, fmt);
      if (iscomplex (varargin{1}))
        maxr = max (abs (imag (varargin{1})(:)));
      else
        maxr = max (abs (varargin{1}(:)));
      endif
    else
      print_usage ();
    endif

    if (! ishold ())
      hg = hggroup (hax, "tag", "polar_grid", "handlevisibility", "off");

      set (hax, "visible", "off", "plotboxaspectratio", [1, 1, 1],
                "zlim", [-1 1], "tag", "polaraxes");

      if (! isprop (hax, "rtick"))
        addproperty ("rtick", hax, "data");
      endif

      set (hax, "rtick", __calc_rtick__ (hax, maxr));

      ## add t(heta)tick
      if (! isprop (hax, "ttick"))
        addproperty ("ttick", hax, "data");
      endif

      ## theta(angular) ticks in degrees
      set (hax, "ttick", 0:30:330);

      __update_polar_grid__ (hax, [], hg);

      set (hg, "deletefcn", {@resetaxis, hax});

      addlistener (hax, "rtick", {@__update_polar_grid__, hg});
      addlistener (hax, "ttick", {@__update_polar_grid__, hg});
      addlistener (hax, "color", {@__update_patch__, hg});
      addlistener (hax, "fontangle", {@__update_text__, hg, "fontangle"});
      addlistener (hax, "fontname", {@__update_text__, hg, "fontname"});
      addlistener (hax, "fontsize", {@__update_text__, hg, "fontsize"});
      addlistener (hax, "fontunits", {@__update_text__, hg, "fontunits"});
      addlistener (hax, "fontweight", {@__update_text__, hg, "fontweight"});
      addlistener (hax, "ticklabelinterpreter",
                   {@__update_text__, hg, "interpreter"});
      addlistener (hax, "layer", {@__update_layer__, hg});
      addlistener (hax, "gridlinestyle",{@__update_lines__,hg,"gridlinestyle"});
      addlistener (hax, "linewidth", {@__update_lines__, hg, "linewidth"});
    else
      hg = findall (hax, "tag", "polar_grid");
      if (! isempty (hg))
        oldrtick = max (get (hax, "rtick"));
        if (maxr > oldrtick)
          set (hax, "rtick", __calc_rtick__ (hax, maxr));
        endif
      endif
    endif

  unwind_protect_cleanup
    if (! isempty (oldfig))
      set (0, "currentfigure", oldfig);
    endif
  end_unwind_protect

  if (nargout > 0)
    h = htmp;
  endif

endfunction

function rtick = __calc_rtick__ (hax, maxr)

  ## FIXME: workaround: calculate r(ho)tick from xtick
  ##        It would be better to just calculate the values,
  ##        but that code is deep in the C++ for the plot engines.
  saved_lims = get (hax, {"xlim", "ylim"});
  set (hax, "xlim", [-maxr maxr], "ylim", [-maxr maxr]);

  xtick = get (hax, "xtick");
  minidx = find (xtick > 0, 1);
  maxidx = find (xtick >= maxr, 1);
  if (! isempty (maxidx))
    rtick = xtick(minidx:maxidx);
  else
    ## Add one more tick through linear interpolation
    rtick = xtick(minidx:end);
    rtick(end+1) = xtick(end) + diff (xtick(end-1:end));
  endif

  set (hax, {"xlim", "ylim"}, saved_lims);

endfunction

function retval = __plr1__ (h, theta, fmt)

  theta = theta(:);
  if (iscomplex (theta))
    rho = imag (theta);
    theta = real (theta);
  else
    rho = theta;
    theta = (1:rows (rho))';
  endif

  retval = __plr2__ (h, theta, rho, fmt);

endfunction

function retval = __plr2__ (h, theta, rho, fmt)

  if (ndims (theta) > 2 || ndims (rho) > 2)
    error ("polar: THETA and RHO must be 2-D objects");
  endif

  theta = real (theta);
  rho = real (rho);

  if (isscalar (theta))
    if (isscalar (rho))
      x = rho * cos (theta);
      y = rho * sin (theta);
      retval = __plt__ ("polar", h, x, y, fmt);
    else
      error ("polar: Can't plot constant THETA with varying RHO");
    endif
  elseif (isvector (theta))
    if (isvector (rho))
      if (length (theta) != length (rho))
        error ("polar: THETA and RHO vector lengths must match");
      endif
      rho = rho(:);
      theta = theta(:);
      x = rho .* cos (theta);
      y = rho .* sin (theta);
      retval = __plt__ ("polar", h, x, y, fmt);
    elseif (ismatrix (rho))
      theta = theta(:);
      t_nr = rows (theta);
      [r_nr, r_nc] = size (rho);
      if (t_nr != r_nr)
        rho = rho';
        r_nr = r_nc;
      endif
      if (t_nr != r_nr)
        error ("polar: THETA vector and RHO matrix sizes must match");
      endif
      x = diag (cos (theta)) * rho;
      y = diag (sin (theta)) * rho;
      retval = __plt__ ("polar", h, x, y, fmt);
    else
      error ("polar: invalid data for plotting");
    endif
  elseif (ismatrix (theta))
    if (isvector (rho))
      rho = rho(:);
      r_nr = rows (rho);
      [t_nr, t_nc] = size (theta);
      if (r_nr != t_nr)
        theta = theta';
        t_nr = t_nc;
      endif
      if (r_nr != t_nr)
        error ("polar: THETA matrix and RHO vector sizes must match");
      endif
      diag_r = diag (rho);
      x = diag_r * cos (theta);
      y = diag_r * sin (theta);
      retval = __plt__ ("polar", h, x, y, fmt);
    elseif (ismatrix (rho))
      if (! size_equal (rho, theta))
        error ("polar: THETA and RHO matrix dimensions must match");
      endif
      x = rho .* cos (theta);
      y = rho .* sin (theta);
      retval = __plt__ ("polar", h, x, y, fmt);
    else
      error ("polar: invalid data for plotting");
    endif
  else
    error ("polar: invalid data for plotting");
  endif

endfunction

## Callback functions for listeners

function __update_text__ (hax, ~, hg, prop)

  kids = get (hg, "children");
  idx = strcmp (get (kids, "type"), "text");
  set (kids(idx).', prop, get (hax, prop));

endfunction

function __update_lines__ (hax, ~, hg, prop)

  kids = get (hg, "children");
  idx = strcmp (get (kids, "type"), "line");
  lprop = prop;
  if (strcmp (prop, "gridlinestyle"))
    lprop = "linestyle";
  endif
  set (kids(idx).', lprop, get (hax, prop));

endfunction

function __update_patch__ (hax, ~, hg)

  kids = get (hg, "children");
  idx = strcmp (get (kids, "type"), "patch");
  set (kids(idx).', "facecolor", get (hax, "color"));

endfunction

function __update_layer__ (hax, ~, hg)

  ## FIXME: This re-implements allchild() because setting the "children"
  ##        property needs to preserve all children (titles, xlabels, etc.).
  shh = get (0, "showhiddenhandles");
  unwind_protect
    set (0, "showhiddenhandles", "on");
    kids = get (hax, "children");
    if (strcmp (get (hax, "layer"), "bottom"))
      set (hax, "children", [kids(kids != hg); hg]);
    else
      set (hax, "children", [hg; kids(kids != hg)]);
    endif
  unwind_protect_cleanup
    set (0, "showhiddenhandles", shh);
  end_unwind_protect

endfunction

function __update_polar_grid__ (hax, ~, hg)

  ## Delete existing polar grid
  delete (get (hg, "children"));

  rtick = unique (get (hax, "rtick")(:)');
  rtick = rtick(rtick > 0);
  if (isempty (rtick))
    rtick = [0.5, 1];
  endif

  ttick = unique (get (hax, "ttick")(:)');
  ttick = ttick(ttick >= 0);
  if (isempty (ttick))
    ttick = 0:30:330;
  endif

  lprops = {"linestyle", get(hax, "gridlinestyle"), ...
            "linewidth", get(hax, "linewidth"), ...
            "color", min(5.8167 * get(hax, "xcolor"), 1)};
  ## "fontunits" should be first because it affects "fontsize" property.
  tprops(1:2:12) = {"fontunits", "fontangle", "fontname", "fontsize", ...
                    "fontweight", "ticklabelinterpreter"};
  tprops(2:2:12) = get (hax, tprops(1:2:12));
  tprops(1:2:12) = strrep (tprops(1:2:12), "ticklabelinterpreter",
                           "interpreter");

  ## The number of points used for a circle
  circle_points = 50;
  t = linspace (0, 2*pi, circle_points)';
  x = kron (cos (t), rtick);
  y = kron (sin (t), rtick);

  ## Draw colored disk under axes at Z-depth = -1
  patch (x(:,end), y(:,end), -ones (circle_points, 1),
         get (hax, "color"), "parent", hg);

  ## Plot grid circles
  line (x(:,1:end-1), y(:,1:end-1), lprops{:}, "parent", hg);

  ## Outer circle (axes "box") is always drawn solid
  line (x(:,end), y(:,end), lprops{:}, "linestyle", "-", "parent", hg);

  ## Add radial labels
  ## Labels are arranged along a radius with an angle of 75 degrees.
  ## 2% addition puts a small visual gap between grid circle and label.
  [x, y] = pol2cart (0.42 * pi, rtick + (.02 * max (rtick(:))));
  text (x, y, num2cell (rtick), "verticalalignment", "bottom", tprops{:},
        "parent", hg);

  ## Add radial lines
  [x, y] = pol2cart (deg2rad (ttick), rtick(end));
  x = [zeros(1, numel (ttick)); x];
  y = [zeros(1, numel (ttick)); y];
  line (x, y, "linestyle", ":", lprops{:}, "parent", hg);

  ## Add angular labels
  tticklabel = num2cell (ttick);
  ## FIXME: The 1.08 factor does not work as fontsize increases
  [x, y] = pol2cart (deg2rad (ttick), 1.08 * rtick(end));
  text (x, y, tticklabel, "horizontalalignment", "center", tprops{:},
        "parent", hg);

  lim = 1.1 * rtick(end);
  set (hax, "xlim", [-lim, lim], "ylim", [-lim, lim]);

  ## Put polar grid behind or ahead of plot
  __update_layer__ (hax, [], hg);

endfunction

function resetaxis (~, ~, hax)

  if (isaxes (hax))
    dellistener (hax, "rtick");
    dellistener (hax, "ttick");
    dellistener (hax, "color");
    dellistener (hax, "fontangle");
    dellistener (hax, "fontname");
    dellistener (hax, "fontsize");
    dellistener (hax, "fontunits");
    dellistener (hax, "fontweight");
    dellistener (hax, "ticklabelinterpreter");
    dellistener (hax, "layer");
    dellistener (hax, "gridlinestyle");
    dellistener (hax, "linewidth");
  endif

endfunction


%!demo
%! clf;
%! theta = linspace (0,2*pi,1000);
%! rho = sin (7*theta);
%! polar (theta, rho);
%! title ("polar() plot");

%!demo
%! clf;
%! theta = linspace (0,2*pi,1000);
%! cplx = theta + i*sin (7*theta);
%! polar (cplx, "g");
%! title ("polar() plot of complex data");

%!demo
%! clf;
%! theta = linspace (0,2*pi,1000);
%! rho = sin (2*theta).*cos (2*theta);
%! polar (theta, rho, "--r");
%! set (gca, "rtick", 0.1:0.1:0.6, "ttick", 0:20:340);
%! title ("polar() plot with finer grid");

%!demo
%! clf;
%! theta = linspace (0,2*pi,1000);
%! rho = sin (2*theta).*cos (2*theta);
%! polar (theta, rho, "--b");
%! set (gca, "fontsize", 12, "linewidth", 2, "color", [0.8 0.8 0.8]);
%! title ("polar() plot with modified axis appearance");

%!demo
%! clf;
%! theta = linspace (0,8*pi,1000);
%! rho = sin (5/4*theta);
%! polar (theta, rho);
%! set (gca, "rtick", 0.2:0.2:1);
%! title ("polar() plot");