changeset 24427:51ead71394bc

colorbar.m: Overhaul function. * colorbar.m: Change docstring to note that "loc" argument must be last. Add note that "peer" argument may disappear and its use is discouraged. Rename variables "ax"->"hax", "cax"->"hcb" for clarity. Redo input validation and do tighter checking of inputs including having an even number of PROP/VAL pairs. Use findall() rather than unwind_protect block around findobj. Add code to make colorbar ready to support plotyy axes. Add #FIXME comment that positioning algorithm in legend.m should be copied to colorbar. Add listener on axes colormap property. Rename callback functions to begin with "cb_". Issue warning if logscale is set on x or y axes of colorbar. Add BIST tests for input validation.
author Rik <rik@octave.org>
date Mon, 18 Dec 2017 16:19:37 -0800
parents a51497205f4c
children 3472c6760ad2
files scripts/plot/draw/colorbar.m
diffstat 1 files changed, 184 insertions(+), 140 deletions(-) [+]
line wrap: on
line diff
--- a/scripts/plot/draw/colorbar.m	Mon Dec 18 11:03:07 2017 -0800
+++ b/scripts/plot/draw/colorbar.m	Mon Dec 18 16:19:37 2017 -0800
@@ -18,7 +18,7 @@
 
 ## -*- texinfo -*-
 ## @deftypefn  {} {} colorbar
-## @deftypefnx {} {} colorbar (@var{loc})
+## @deftypefnx {} {} colorbar (@dots, @var{loc})
 ## @deftypefnx {} {} colorbar (@var{delete_option})
 ## @deftypefnx {} {} colorbar (@var{hcb}, @dots{})
 ## @deftypefnx {} {} colorbar (@var{hax}, @dots{})
@@ -31,8 +31,9 @@
 ## A colorbar displays the current colormap along with numerical rulings
 ## so that the color scale can be interpreted.
 ##
-## The optional input @var{loc} determines the location of the colorbar.
-## Valid values for @var{loc} are
+## The optional input @var{loc} determines the location of the colorbar.  If
+## present, it must be the last argument to @code{colorbar}.  Valid values for
+## @var{loc} are
 ##
 ## @table @asis
 ## @item @qcode{"EastOutside"}
@@ -61,12 +62,14 @@
 ## @end table
 ##
 ## To remove a colorbar from a plot use any one of the following keywords for
-## the @var{delete_option}: @qcode{"delete"}, @qcode{"hide"}, @qcode{"off"}.
+## the @var{delete_option}: @qcode{"off"}, @qcode{"delete"}, @qcode{"hide"}.
 ##
-## If the argument @qcode{"peer"} is given, then the following argument is
-## treated as the axes handle in which to add the colorbar.  Alternatively,
 ## If the first argument @var{hax} is an axes handle, then the colorbar is
 ## added to this axis, rather than the current axes returned by @code{gca}.
+## Alternatively, If the argument @qcode{"peer"} is given, then the following
+## argument is treated as the axes handle in which to add the colorbar.  The
+## @qcode{"peer"} calling syntax may be removed in the future and is not
+## recommended.
 ##
 ## If the first argument @var{hcb} is a handle to a colorbar object, then
 ## operate on this colorbar directly.
@@ -89,10 +92,10 @@
   [hcb, varargin, nargin] = __plt_get_axis_arg__ ("colorbar", varargin{:});
 
   if (hcb && ! strcmp (get (hcb, "tag"), "colorbar"))
-    ax = hcb;
+    hax = hcb;
     hcb = [];
   else
-    ax = [];
+    hax = [];
   endif
   loc = "";
   args = {};
@@ -101,34 +104,46 @@
   i = 1;
   while (i <= nargin)
     arg = varargin{i++};
-    if (ischar (arg))
-      switch (tolower (arg))
-        case "peer"
-          if (i > nargin)
-            error ('colorbar: missing axes handle after "peer"');
-          else
-            ax = varargin{i++};
-            if (! isscalar (ax) && ! isaxes (ax))
-              error ('colorbar: invalid axes handle following "peer"');
-            endif
-          endif
-        case {"north", "south", "east", "west",
-              "northoutside", "southoutside", "eastoutside", "westoutside"}
-          loc = tolower (arg);
-        case "location"
-          if (i > nargin)
-            error ('colorbar: missing value after "location"');
-          else
-            loc = tolower (varargin{i++});
-          endif
-        case {"delete", "hide", "off", "none"}
-          deleting = true;
-        otherwise
-          args{end+1} = arg;
-      endswitch
-    else
-      args{end+1} = arg;
+    if (! ischar (arg))
+      error ("colorbar: expected string argument at position %d", i-1);
     endif
+
+    switch (tolower (arg))
+      case {"north", "south", "east", "west", ...
+            "northoutside", "southoutside", "eastoutside", "westoutside"}
+        if (i <= nargin)
+          error ("colorbar: LOC specification must occur as final argument");
+        endif
+        loc = tolower (arg);
+
+      case "location"
+        if (i > nargin)
+          error ('colorbar: missing value after "location"');
+        endif
+        loc = tolower (varargin{i++});
+
+      case {"delete", "hide", "off", "none"}
+        deleting = true;
+
+      case "peer"
+        if (i > nargin)
+          error ('colorbar: missing axes handle after "peer"');
+        endif
+
+        hax = varargin{i++};
+        if (! isscalar (hax) || ! isaxes (hax))
+          error ('colorbar: invalid axes handle following "peer"');
+        endif
+
+      otherwise
+        ## Property/Value pair
+        if (i > nargin)
+          error ("colorbar: PROP/VAL inputs must occur in pairs");
+        endif
+        args{end+1} = arg;
+        args{end+1} = varargin{i++};
+
+    endswitch
   endwhile
 
   ## Handle changes to existing colorbar
@@ -136,7 +151,7 @@
     if (deleting)
       delete (hcb);
       if (nargout > 0)
-        h = hcb;
+        h = [];
       endif
       return;
     else
@@ -149,162 +164,176 @@
       ## if (! isempty (args))
       ##   set (hcb, args{:});
       ## endif
-      ax = get (ancestor (hcb, "figure"), "currrentaxes");
+      hax = get (ancestor (hcb, "figure"), "currrentaxes");
     endif
   endif
 
   if (isempty (loc))
     loc = "eastoutside";
   endif
-  if (isempty (ax))
-    ax = gca ();
+  if (isempty (hax))
+    hax = gca ();
+  endif
+
+  ## Remove existing colorbar
+  hpar = ancestor (hax, "figure");
+  hcb = findall (hpar, "tag", "colorbar", "type", "axes", "axes", hax);
+  if (! isempty (hcb))
+    delete (hcb);
   endif
 
-  showhiddenhandles = get (0, "showhiddenhandles");
-  unwind_protect
-    set (0, "showhiddenhandles", "on");
-    cax = findobj (ancestor (ax, "figure"),
-                   "tag", "colorbar", "type", "axes", "axes", ax);
-    if (! isempty (cax))
-      delete (cax);
+  if (! deleting)
+    ## Create a colorbar
+
+    ## Special handling for plotyy which has two axes objects
+    if (isprop (hax, "__plotyy_axes__"))
+      hyy = get (hax, "__plotyy_axes__");
+
+      ## Use axis which is appropriate for legend location.
+      ## necessary for plotyy figures where there are two axes.
+      if (strfind (loc, "east"))
+        hax = hyy(2);
+      else
+        hax = hyy(1);
+      endif
     endif
-  unwind_protect_cleanup
-    set (0, "showhiddenhandles", showhiddenhandles);
-  end_unwind_protect
 
-  if (! deleting)
     ## FIXME: Matlab does not require the "position" property to be active.
     ##        Is there a way to determine the plotbox position for the
     ##        gnuplot graphics toolkit with the outerposition is active?
-    set (ax, "activepositionproperty", "position");
-    obj = get (ax);
-    obj.__cbar_hax__ = ax;
-    position = obj.position;
+    set (hax, "activepositionproperty", "position");
+    props = get (hax);
+    props.__cbar_hax__ = hax;
+    position = props.position;
 
-    hpar = ancestor (ax, "figure");
-    clen = rows (get (hpar, "colormap"));
-    cext = get (ax, "clim");
+    clen = rows (get (hax, "colormap"));
+    cext = get (hax, "clim");
     cdiff = (cext(2) - cext(1)) / clen / 2;
     cmin = cext(1) + cdiff;
     cmax = cext(2) - cdiff;
 
-    [pos, cpos, vertical, mirror] = ...
-       __position_colorbox__ (loc, obj, ancestor (ax, "figure"));
-    set (ax, "position", pos);
+    [pos, cpos, vertical, mirror] = __position_colorbox__ (loc, props, hpar);
+    set (hax, "position", pos);
 
-    cax = __go_axes__ (hpar, "tag", "colorbar",
-                             "handlevisibility", "on",
+    hcb = __go_axes__ (hpar, "tag", "colorbar",
                              "activepositionproperty", "position",
                              "position", cpos,
                              "box", "on");
-    addproperty ("location", cax, "radio",
-                 "eastoutside|east|westoutside|west|northoutside|north|southoutside|south",
+    addproperty ("location", hcb, "radio",
+                 "eastoutside|east|westoutside|west|northoutside|north|southoutside|south|manual",
                  loc);
-    addproperty ("axes", cax, "handle", ax);
+    addproperty ("axes", hcb, "handle", hax);
 
     if (vertical)
       ## Use low-level form to avoid calling newplot which changes axes
-      hi = image (cax, "xdata", [0,1], "ydata", [cmin, cmax],
+      hi = image (hcb, "xdata", [0,1], "ydata", [cmin, cmax],
                        "cdata", [1 : clen]');
       if (mirror)
-        set (cax, "xtick", [], "xdir", "normal", "ydir", "normal",
+        set (hcb, "xtick", [], "xdir", "normal", "ydir", "normal",
                   "ylim", cext, "ylimmode", "manual",
                   "yaxislocation", "right", "layer", "top", args{:});
       else
-        set (cax, "xtick", [], "xdir", "normal", "ydir", "normal",
+        set (hcb, "xtick", [], "xdir", "normal", "ydir", "normal",
                   "ylim", cext, "ylimmode", "manual",
                   "yaxislocation", "left", "layer", "top", args{:});
       endif
     else
-      hi = image (cax, "xdata", [cmin, cmax], "ydata", [0,1],
+      hi = image (hcb, "xdata", [cmin, cmax], "ydata", [0,1],
                        "cdata", [1 : clen]);
       if (mirror)
-        set (cax, "ytick", [], "xdir", "normal", "ydir", "normal",
+        set (hcb, "ytick", [], "xdir", "normal", "ydir", "normal",
                   "xlim", cext, "xlimmode", "manual",
                   "xaxislocation", "top", "layer", "top", args{:});
       else
-        set (cax, "ytick", [], "xdir", "normal", "ydir", "normal",
+        set (hcb, "ytick", [], "xdir", "normal", "ydir", "normal",
                   "xlim", cext, "xlimmode", "manual",
                   "xaxislocation", "bottom", "layer", "top", args{:});
       endif
     endif
 
-    ## Dummy object placed in axis to delete colorbar when axis is deleted.
+    ## Dummy object placed on axes to delete colorbar when axes is deleted.
     ctext = text (0, 0, "", "tag", "colorbar",
                   "visible", "off", "handlevisibility", "off",
                   "xliminclude", "off", "yliminclude", "off",
                   "zliminclude", "off",
-                  "deletefcn", {@deletecolorbar, cax, obj});
+                  "deletefcn", {@cb_axes_deleted, hcb});
 
-    set (cax, "deletefcn", {@resetaxis, ax, obj});
+    set (hcb, "deletefcn", {@cb_restore_axes, hax, props});
 
-    addlistener (cax, "yscale", @error_if_logscale);
-    addlistener (hpar, "colormap", {@update_colorbar_cmap, hi, vertical, clen});
-    addlistener (ax, "clim", {@update_colorbar_clim, hi, vertical});
-    addlistener (ax, "dataaspectratio", {@update_colorbar_axis, cax, obj});
-    addlistener (ax, "dataaspectratiomode", {@update_colorbar_axis, cax, obj});
-    addlistener (ax, "plotboxaspectratio", {@update_colorbar_axis, cax, obj});
-    addlistener (ax, "plotboxaspectratiomode", {@update_colorbar_axis, cax, obj});
-    addlistener (ax, "position", {@update_colorbar_axis, cax, obj});
+    if (vertical)
+      addlistener (hcb, "yscale", {@cb_error_on_logscale, "yscale"});
+    else
+      addlistener (hcb, "xscale", {@cb_error_on_logscale, "xscale"});
+    endif
+    addlistener (hpar, "colormap", {@cb_colormap, hi, vertical, clen});
+    ## FIXME: listener on axes colormap does not work yet.
+    addlistener (hax, "colormap", {@cb_colormap, hi, vertical, clen});
+    addlistener (hax, "clim", {@cb_clim, hi, vertical});
+    addlistener (hax, "dataaspectratio", {@cb_colorbar_axis, hcb, props});
+    addlistener (hax, "dataaspectratiomode", {@cb_colorbar_axis, hcb, props});
+    addlistener (hax, "plotboxaspectratio", {@cb_colorbar_axis, hcb, props});
+    addlistener (hax, "plotboxaspectratiomode",{@cb_colorbar_axis, hcb, props});
+    addlistener (hax, "position", {@cb_colorbar_axis, hcb, props});
 
   endif
 
   if (nargout > 0)
-    h = cax;
+    h = hcb;
   endif
 
 endfunction
 
-function deletecolorbar (h, ~, hc, orig_props)
-
-  if (isaxes (hc))
-    set (hc, "deletefcn", []);
-    delete (hc);
+## Axes to which colorbar was attached has been deleted.  Delete colorbar.
+function cb_axes_deleted (~, ~, hcb, orig_props)
+  if (isaxes (hcb))
+    set (hcb, "deletefcn", []);
+    delete (hcb);
   endif
-
 endfunction
 
-function error_if_logscale (cax, ~)
-  if (strcmp (get (cax, "yscale"), "log"))
-    set (cax, "yscale", "linear");
+## Error on attempt to set logscale on colorbar axes
+function cb_error_on_logscale (hcb, ~, scale)
+  if (strcmp (get (hcb, scale), "log"))
+    set (hcb, scale, "linear");
     error ("colorbar: Only linear colorbars are possible");
   endif
 endfunction
 
-function resetaxis (cax, ~, ax, orig_props)
+## Restore position of axes object when colorbar is deleted.
+function cb_restore_axes (hcb, ~, hax, orig_props)
 
-  hf = ancestor (ax, "figure");
+  hf = ancestor (hax, "figure");
   if (strcmp (get (hf, "beingdeleted"), "on"))
     ## Skip restoring axes if entire figure is being destroyed.
     return;
   endif
 
-  if (isaxes (ax))
+  if (isaxes (hax))
     ## FIXME: It is wrong to delete every listener for colormap on figure,
     ##        but we don't have a way of deleting just this instance.
     dellistener (hf, "colormap");
-    dellistener (ax, "clim");
-    dellistener (ax, "dataaspectratio");
-    dellistener (ax, "dataaspectratiomode");
-    dellistener (ax, "plotboxaspectratio");
-    dellistener (ax, "plotboxaspectratiomode");
-    dellistener (ax, "position");
+    dellistener (hax, "dataaspectratio");
+    dellistener (hax, "dataaspectratiomode");
+    dellistener (hax, "plotboxaspectratio");
+    dellistener (hax, "plotboxaspectratiomode");
+    dellistener (hax, "position");
 
-    ## Restore axes position
-    units = get (ax, "units");
-    set (ax, "units", orig_props.units);
-    set (ax, "position", orig_props.position,
-             "outerposition", orig_props.outerposition,
-             "activepositionproperty", orig_props.activepositionproperty);
-    set (ax, "units", units);
+    ## Restore original axes position
+    units = get (hax, "units");
+    set (hax, "units", orig_props.units);
+    set (hax, "position", orig_props.position,
+              "outerposition", orig_props.outerposition,
+              "activepositionproperty", orig_props.activepositionproperty);
+    set (hax, "units", units);
   endif
 
 endfunction
 
-function update_colorbar_clim (hax, ~, hi, vert)
+## Update colorbar when clim has changed
+function cb_clim (hax, ~, hi, vert)
 
   if (isaxes (hax))
-    clen = rows (get (ancestor (hax, "figure"), "colormap"));
+    clen = rows (get (hax, "colormap"));
     cext = get (hax, "clim");
     cdiff = (cext(2) - cext(1)) / clen / 2;
     cmin = cext(1) + cdiff;
@@ -322,11 +351,12 @@
 
 endfunction
 
-function update_colorbar_cmap (hf, d, hi, vert, init_sz)
+## Update colorbar when changes to axes or figure colormap have occurred.
+function cb_colormap (h, d, hi, vert, init_sz)
   persistent sz = init_sz;
 
-  if (isfigure (hf))
-    clen = rows (get (hf, "colormap"));
+  if (ishghandle (h))
+    clen = rows (get (h, "colormap"));
     if (clen != sz)
       if (vert)
         set (hi, "cdata", [1:clen]');
@@ -334,38 +364,39 @@
         set (hi, "cdata", [1:clen]);
       endif
       sz = clen;
-      ## Also update limits on axis or there will be white gaps
-      update_colorbar_clim (get (hi, "parent"), d, hi, vert);
+      ## Also update limits on colorbar axes or there will be white gaps
+      cb_clim (get (hi, "parent"), d, hi, vert);
     endif
   endif
 
 endfunction
 
-function update_colorbar_axis (h, ~, cax, orig_props)
+## Update positioning of colorbar when original axes has changed position.
+function cb_colorbar_axis (hax, ~, hcb, orig_props)
 
-  if (isaxes (cax))
-    loc = get (cax, "location");
-    obj = get (h);
-    obj.__cbar_hax__ = h;
-    obj.position = orig_props.position;
-    obj.outerposition = orig_props.outerposition;
+  if (isaxes (hcb))
+    loc = get (hcb, "location");
+    props = get (hax);
+    props.__cbar_hax__ = hax;
+    props.position = orig_props.position;
+    props.outerposition = orig_props.outerposition;
     [pos, cpos, vertical, mirror] = ...
-       __position_colorbox__ (loc, obj, ancestor (h, "figure"));
+       __position_colorbox__ (loc, props, ancestor (hax, "figure"));
 
     if (vertical)
       if (mirror)
-        set (cax, "xtick", [], "xdir", "normal", "ydir", "normal",
+        set (hcb, "xtick", [], "xdir", "normal", "ydir", "normal",
                   "yaxislocation", "right", "position", cpos);
       else
-        set (cax, "xtick", [], "xdir", "normal", "ydir", "normal",
+        set (hcb, "xtick", [], "xdir", "normal", "ydir", "normal",
                   "yaxislocation", "left", "position", cpos);
       endif
     else
       if (mirror)
-        set (cax, "ytick", [], "xdir", "normal", "ydir", "normal",
+        set (hcb, "ytick", [], "xdir", "normal", "ydir", "normal",
                   "xaxislocation", "top", "position", cpos);
       else
-        set (cax, "ytick", [], "xdir", "normal", "ydir", "normal",
+        set (hcb, "ytick", [], "xdir", "normal", "ydir", "normal",
                   "xaxislocation", "bottom", "position", cpos);
       endif
     endif
@@ -374,14 +405,17 @@
 
 endfunction
 
-function [pos, cpos, vertical, mirr] = __position_colorbox__ (cbox, obj, cf)
+## FIXME: The algorithm for positioning in legend.m is much more sophisticated
+##        and should be borrowed for colorbar.  One problem is that colorbar
+##        positioning does not take in to account multi-line axes labels
+function [pos, cpos, vertical, mirr] = __position_colorbox__ (cbox, props, cf)
 
   ## This will always represent the position prior to adding the colorbar.
-  pos = obj.position;
+  pos = props.position;
   sz = pos(3:4);
 
-  if (strcmp (obj.plotboxaspectratiomode, "manual")
-      || strcmp (obj.dataaspectratiomode, "manual"))
+  if (strcmp (props.plotboxaspectratiomode, "manual")
+      || strcmp (props.dataaspectratiomode, "manual"))
     if (isempty (strfind (cbox, "outside")))
       scale = 1.0;
     else
@@ -393,12 +427,12 @@
       scale = [scale, 1];
     endif
     if (strcmp (get (cf, "__graphics_toolkit__"), "gnuplot")
-        && strcmp (obj.activepositionproperty, "outerposition"))
-      obj.outerposition = obj.outerposition .* [1, 1, scale];
-      off = 0.5 * (obj.outerposition (3:4) - __actual_axis_position__ (obj)(3:4));
+        && strcmp (props.activepositionproperty, "outerposition"))
+      props.outerposition = props.outerposition .* [1, 1, scale];
+      off = 0.5 * (props.outerposition (3:4) - __actual_axis_position__ (props)(3:4));
     else
-      obj.position = obj.position .* [1, 1, scale];
-      off = 0.5 * (obj.position (3:4) - __actual_axis_position__ (obj)(3:4));
+      props.position = props.position .* [1, 1, scale];
+      off = 0.5 * (props.position (3:4) - __actual_axis_position__ (props)(3:4));
     endif
   else
     off = 0.0;
@@ -455,10 +489,10 @@
 
   cpos = [origin, sz];
 
-  if (strcmp (obj.plotboxaspectratiomode, "manual")
-      || strcmp (obj.dataaspectratiomode, "manual"))
-    obj.position = pos;
-    actual_pos = __actual_axis_position__ (obj);
+  if (strcmp (props.plotboxaspectratiomode, "manual")
+      || strcmp (props.dataaspectratiomode, "manual"))
+    props.position = pos;
+    actual_pos = __actual_axis_position__ (props);
     if (strfind (cbox, "outside"))
       scale = 1.0;
     else
@@ -758,3 +792,13 @@
 %! plot ([0, 2]);
 %! colorbar ("eastoutside");
 %! axis equal;
+
+
+## Test input validation
+%!error <expected string argument at position 1> colorbar (-pi)
+%!error <LOC specification must occur as final arg> colorbar ("east", "p", "v")
+%!error <missing value after "location"> colorbar ("location")
+%!error <missing axes handle after "peer"> colorbar ("peer")
+%!error <invalid axes handle following "peer"> colorbar ("peer", -1)
+%!error <PROP/VAL inputs must occur in pairs> colorbar ("PROP")
+