changeset 22679:e70551dacef6

whitebg.m: Completely overhaul function. * whitebg.m: Rewrite docstring adding more calling forms, more detail, and a Programming Note about how function works. Add FIXME notes about unclear parts of Matlab compatibility. Add input validation. Add Input validation tests. Adjust BIST tests to test new behavior. Add new input 'none' to return colors to factory defaults. Use regexp for performance instead of multiple cellfun calls. Correct typo in error() text messages. * whitebg.m (calc_contrast_color): New function to attempt to find a contrasting color to the one specified by the user for the background.
author Rik <rik@octave.org>
date Thu, 27 Oct 2016 10:41:31 -0700
parents 0705873185bc
children 62702ba67a38
files scripts/plot/appearance/whitebg.m
diffstat 1 files changed, 161 insertions(+), 79 deletions(-) [+]
line wrap: on
line diff
--- a/scripts/plot/appearance/whitebg.m	Thu Oct 27 13:17:07 2016 -0400
+++ b/scripts/plot/appearance/whitebg.m	Thu Oct 27 10:41:31 2016 -0700
@@ -20,31 +20,55 @@
 ## @deftypefn  {} {} whitebg ()
 ## @deftypefnx {} {} whitebg (@var{color})
 ## @deftypefnx {} {} whitebg ("none")
-## @deftypefnx {} {} whitebg (@var{hfig}, @dots{})
+## @deftypefnx {} {} whitebg (@var{hfig})
+## @deftypefnx {} {} whitebg (@var{hfig}, @var{color})
+## @deftypefnx {} {} whitebg (@var{hfig}, "none")
 ## Invert the colors in the current color scheme.
 ##
-## The root properties are also inverted such that all subsequent plot use the
-## new color scheme.
+## The root properties are also inverted such that all subsequent plots will
+## use the new color scheme.
 ##
 ## If the optional argument @var{color} is present then the background color
 ## is set to @var{color} rather than inverted.  @var{color} may be a string
 ## representing one of the eight known colors or an RGB triplet.  The special
-## string argument @qcode{"none"} restores the plot to the default colors.
+## string argument @qcode{"none"} restores the plot to the factory default
+## colors.
+##
+## If the first argument @var{hfig} is a figure handle or list of figure
+## handles, then operate on these figures rather than the current figure
+## returned by @code{gcf}.  The root properties will not be changed unless 0
+## is in the list of figures.
 ##
-## If the first argument @var{hfig} is a figure handle, then operate on
-## this figure rather than the current figure returned by @code{gcf}.  The
-## root properties will not be changed.
+## Programming Note: @code{whitebg} operates by changing the color properties
+## of the children of the specified figures.  Only objects with a single color
+## are affected.  For example, a patch with a single @qcode{"FaceColor"} will
+## be changed, but a patch with shading (@qcode{"interp"}) will not be
+## modified.  For inversion, the new color is simply the inversion in RGB
+## space: @code{@var{cnew} = [1-@var{R} 1-@var{G} 1-@var{B}]}.  When a color
+## is specified, the axes and figure are set to the new color, and the color
+## of child objects are then adjusted to have some contrast (visibility)
+## against the new background.
 ## @seealso{reset, get, set}
 ## @end deftypefn
 
+## FIXME: It's not clear whether Matlab also changes color properties
+## of the figure object itself, or only the children.  However, visually,
+## it looks better to change the figure along with the axes background.
+
 function whitebg (varargin)
 
+  if (nargin > 2)
+    print_usage ();
+  endif
+
   h = 0;
   color = NaN;
+  have_fig = false;
 
-  if (nargin > 0 && nargin < 3)
-    if (ishandle (varargin{1}))
+  if (nargin > 0)
+    if (all (ishandle (varargin{1})))
       h = varargin{1};
+      have_fig = true;
       if (nargin == 2)
         color = varargin{2};
       endif
@@ -53,117 +77,175 @@
     else
       print_usage ();
     endif
-  elseif (nargin != 0)
-    print_usage ();
   endif
 
-  typ = get (h, "type");
-
-  if (strcmp (typ, "root"))
-    isroot = true;
+  if (! have_fig)
     fig = gcf ();
-  elseif (strcmp (typ, "figure"))
-    isroot = false;
-    fig = h;
+    do_root = true;
+  elseif (all (isfigure (h) | h == 0))
+    fig = h(h != 0);
+    do_root = any (h == 0);
   else
-    error ("whitebg: HFIF must be a valid figure handle");
+    error ("whitebg: HFIG must be a valid figure handle");
   endif
 
-  axes = findall (fig, "type", "axes");
   if (isnan (color))
-    ## Root figure.  Set the default axes and figure properties so that
-    ## subsequent plots have the new color scheme.
-    if (isroot)
+    if (do_root)
+      ## Set the default axes and figure properties on root
+      ## so that subsequent plots have the new color scheme
       fac = get (0, "factory");
       fields = fieldnames (fac);
-      fieldindex = intersect (find (! cellfun ("isempty", regexp (fields, 'color'))), union (find (! cellfun ("isempty", regexp (fields, 'factoryaxes.*'))), find (! cellfun ("isempty", regexp (fields, 'factoryfigure.*')))));
+      idx = ! cellfun ("isempty", regexp (fields,
+                                          '^factory(axes|figure).*color$'));
 
-      ## Check whether the factory value has been replaced
-      for nf = 1 : numel (fieldindex);
-        defaultfield = strrep (fields{fieldindex(nf)}, "factory", "default");
+      ## Use default value in place of factory value if specified.
+      for field = fields(idx)'
+        defaultfield = strrep (field{1}, "factory", "default");
         try
-          defaultvalue = 1 - get (0, defaultfield{n});
+          defaultvalue = 1 - get (0, defaultfield);
         catch
-          field = fields{fieldindex(nf)};
-          defaultvalue = 1 - subsref (fac, struct ("type", ".", "subs", field));
+          defaultvalue = 1 - fac.(field{1});
         end_try_catch
         set (0, defaultfield, defaultvalue);
       endfor
     endif
 
-    ## Load all objects which qualify for being searched.
-    handles = fig;
-    h = fig;
-    while (numel (handles))
-      children = [];
-      for n = 1 : numel (handles)
-        children = union (children, get (handles(n), "children"));
-      endfor
-      handles = children;
-      h = union (h, children);
-    endwhile
+    ## The sort is necessary so that child legend objects are acted on
+    ## before the legend axes object.
+    hlist = sort (findobj (fig));
 
-    for nh = 1 : numel (h)
-      p = get (h (nh));
-      fields = fieldnames (p);
-      fieldindex = find (! cellfun ("isempty", regexp (fields, 'color')));
-      if (numel (fieldindex))
-        for nf = 1 : numel (fieldindex);
-          field = fields{fieldindex(nf)};
-          c = subsref (p, struct ("type", ".", "subs", field));
-          if (! ischar (c) && columns (c) == 3)
-            set (h (nh), field, 1 - c);
-          endif
-        endfor
+    for h = hlist'
+      props = get (h);
+      fields = fieldnames (props);
+      ## Find all fields with the word color in them and invert.
+      idx = find (! cellfun ("isempty", regexp (fields, 'color$')));
+      for field = fields(idx)';
+        c = props.(field{1});
+        if (! ischar (c) && columns (c) == 3)
+          set (h, field{1}, 1 - c);
+        endif
+      endfor
+    endfor
+
+  else  # 2nd argument such as a color or "none"
+
+    if (! strcmp (color, "none"))
+      ## Set the specified color on the figure and all axes objects
+      hlist = [fig; findobj(fig, "type", "axes")];
+      set (hlist, "color", color);
+      if (do_root)
+        defs = get (0, "default");
+        if (isfield (defs, "defaultaxescolor")
+            && strcmp (defs.defaultaxescolor, "none"))
+          set (0, "defaultaxescolor", color);
+        endif
       endif
 
-      ## If h(nh) is a figure or axes invert default color properties
-      typ = subsref (p, struct ("type", ".", "subs", "type"));
-      if (strcmp (typ, "axes") || strcmp (typ, "figure"))
-        def = get (h (nh), "default");
-        fields = fieldnames (def);
-        if (! isempty (fields))
-          fieldindex = find (! cellfun ("isempty", regexp (fields, 'color')));
-          for nf = 1 : numel (fieldindex)
-            defaultfield = fields{fieldindex(nf)};
-            defaultvalue = ...
-              1 - subsref (def, struct ("type", ".", "subs", defaultfield));
-            set (h (nh), defaultfield, defaultvalue);
-          endfor
-        endif
+      ## Adjust colors of remaining objects to have some contrast
+      bg = rgb2hsv (get (fig, "color"));
+      ## List of children without the figure and axes objects already changed
+      hlist = setdiff (findobj (fig), hlist);
+      for h = hlist'
+        props = get (h);
+        fields = fieldnames (props);
+        ## Find all fields with the word color in them and adjust HSV.
+        idx = find (! cellfun ("isempty", regexp (fields, 'color$')));
+        for field = fields(idx)';
+          c = props.(field{1});
+          if (! ischar (c) && columns (c) == 3)
+            set (h, field{1}, calc_contrast_color (bg, c));
+          endif
+        endfor
+      endfor
+
+    else
+      ## Reset colors to factory defaults
+      if (do_root)
+        fac = get (0, "factory");
+        fields = fieldnames (fac);
+        idx = ! cellfun ("isempty", regexp (fields,
+                                            '^factory(axes|figure).*color$'));
+        for field = fields(idx)'
+          factoryfield = field{1};
+          factoryvalue = fac.(factoryfield);
+          if (strncmp (factoryfield, "factoryfigure", 13))
+            ## Strip off "factoryfigure" part of fieldname before applying
+            set (fig, factoryfield(14:end), factoryvalue);
+          endif
+          ## Remove applied default from root
+          defaultfield = strrep (factoryfield, "factory", "default");
+          set (0, defaultfield, "remove");
+        endfor 
       endif
-    endfor
-  else
-    ## FIXME: Is this the right thing to do in this case?
-    set (findall (fig, "type", "axes"), "color", color);
-    if (isroot)
-      defs = get (0, "default");
-      if (isfield (defs, "defaultaxescolor")
-          && strcmp (defs.defaultaxescolor, "none"))
-        set (0, "defaultaxescolor", color);
-      endif
+
+      hlist = sort (findobj (fig));
+      for h = hlist'
+        props = get (h);
+        fields = fieldnames (props);
+        ## Find all fields with the word color in them and restore to factory.
+        idx = find (! cellfun ("isempty", regexp (fields, 'color$')));
+        for field = fields(idx)'
+          set (h, field{1}, "factory");
+        endfor
+      endfor
     endif
   endif
 
 endfunction
 
+## Calculate a new color which contrasts with the supplied bg color and is
+## perceptually related to the original color.
+##
+## Input color bg must be in HSV space.  Input color corig is in RGB space.
+##
+## FIXME: Calculation is segregated into its own function in case anyone wants
+## to try and improve the selection of a "contrasting" color.
+##
+## This algorithm maintains at least 90 degrees separation between corig and bg
+## in Hue rotation space.  No modifications are done for saturation or value.
+function cnew = calc_contrast_color (bg, corig)
+  
+  hsv = rgb2hsv (corig);
+  contrast_hue = mod (bg(1) + 0.5, 1);  # Generate a contrasting bg color
+
+  ## If close to existing contrast color, leave alone
+  delta = abs (hsv(1) - contrast_hue);
+  if (delta < 0.25 || delta > 0.75)
+    cnew(1) = hsv(1);
+  else
+    cnew(1) = mod (hsv(1) + 0.5, 1);
+  endif
+
+  ## No modifications to saturation or value.
+  cnew(2:3) = hsv(2:3);
+
+  cnew = hsv2rgb (cnew);   
+
+endfunction
+
 
 %!test
 %! dac = get (0, "defaultaxescolor");
 %! dfc = get (0, "defaultfigurecolor");
 %! hf = figure ("visible", "off");
 %! unwind_protect
-%!   l = line;
+%!   hl = line ("Color", "r");
 %!   assert (get (hf, "color"), dfc);
 %!   assert (get (gca, "color"), dac);
+%!   assert (get (hl, "color"), [1 0 0]);
 %!   whitebg (hf);
 %!   assert (get (hf, "color"), 1 - dfc);
 %!   assert (get (gca, "color"), 1 - dac);
+%!   assert (get (hl, "color"), [0 1 1]);
 %!   c = [0.2 0.2 0.2];
 %!   whitebg (hf, c);
-%!   assert (get (hf, "color"), 1 - dfc);
+%!   assert (get (hf, "color"), c);
 %!   assert (get (gca, "color"), c);
 %! unwind_protect_cleanup
 %!   close (hf);
 %! end_unwind_protect
 
+## Test input validation
+%!error whitebg (1,2,3)
+%!error whitebg ({1}, 2)
+