Mercurial > octave
changeset 27708:c66467f74278
Rewrite legend (task #14243)
* __gnuplot_legend__.m: Rename previous legend.m to __gnuplot_legend__.m and
use it to display legends in gnuplot.
* legend.m: Complete rewrite.
* plotyy.m: Redo listeners and use "outerposition" as the default
"activepositionproperty" when the toolkit is not gnuplot.
* NEWS: announce function rewrite.
author | Pantxo Diribarne <pantxo.diribarne@gmail.com> |
---|---|
date | Wed, 13 Nov 2019 09:53:07 +0100 |
parents | 377f069841c1 |
children | 0a84a7d0998d |
files | NEWS scripts/plot/appearance/legend.m scripts/plot/appearance/private/__gnuplot_legend__.m scripts/plot/draw/plotyy.m |
diffstat | 4 files changed, 3064 insertions(+), 1324 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Sat Nov 16 23:32:42 2019 +0100 +++ b/NEWS Wed Nov 13 09:53:07 2019 +0100 @@ -49,6 +49,11 @@ #### Graphics backend +- The `"legend"` function has been entirely rewritten. This mainly fixes + a number of historical bugs, but also implements new properties such as + `"AutoUpdate"` and `"NumColumns"`. Gnuplot which is no more actively + maintained will keep using the old legend function. + - Graphic primitives now accept a color property value of `"none"` which is useful when a particular primitive needs to be hidden (for example, the Y-axis of an axes object with `"ycolor" = "none"`)
--- a/scripts/plot/appearance/legend.m Sat Nov 16 23:32:42 2019 +0100 +++ b/scripts/plot/appearance/legend.m Wed Nov 13 09:53:07 2019 +0100 @@ -21,21 +21,18 @@ ## @deftypefnx {} {} legend (@var{str1}, @var{str2}, @dots{}) ## @deftypefnx {} {} legend (@var{charmat}) ## @deftypefnx {} {} legend (@{@var{cellstr}@}) -## @deftypefnx {} {} legend (@dots{}, "location", @var{pos}) -## @deftypefnx {} {} legend (@dots{}, "orientation", @var{orient}) -## @deftypefnx {} {} legend (@var{hax}, @dots{}) +## @deftypefnx {} {} legend (@dots{}, @var{property}, @var{value}, @dots{}) ## @deftypefnx {} {} legend (@var{hobjs}, @dots{}) -## @deftypefnx {} {} legend (@var{hax}, @var{hobjs}, @dots{}) -## @deftypefnx {} {} legend ("@var{option}") -## @deftypefnx {} {} legend (@dots{}, @{@var{cellstr}@}, @var{property}, @var{value}, @dots{}) -## @deftypefnx {} {[@var{hleg}, @var{hleg_obj}, @var{hplot}, @var{labels}] =} legend (@dots{}) +## @deftypefnx {} {} legend (@var{command}) +## @deftypefnx {} {} legend (@var{hax}, @dots{}) +## @deftypefnx {} {@var{hleg, hplt} =} legend (@dots{}) ## ## Display a legend for the current axes using the specified strings as labels. ## ## Legend entries may be specified as individual character string arguments, ## a character array, or a cell array of character strings. When label names -## might be confused with options to @code{legend}, the labels should be -## protected by specifying them as a cell array of strings. +## might be confused with legend properties, or @var{command} arguments, +## the labels should be protected by specifying them as a cell array of strings. ## ## If the first argument @var{hax} is an axes handle, then add a legend to this ## axes, rather than the current axes returned by @code{gca}. @@ -64,10 +61,6 @@ ## @item @tab @tab which will place the legend outside the axes ## @end multitable ## -## The optional parameter @var{orient} determines if the legend elements are -## placed vertically or horizontally. The allowed values are -## @qcode{"vertical"} (default) or @qcode{"horizontal"}. -## ## The following customizations are available using @var{option}: ## ## @table @asis @@ -102,22 +95,7 @@ ## including @var{property}/@var{value} pairs. If using this calling form, the ## labels must be specified as a cell array of strings. ## -## The optional output values are -## -## @table @var -## @item hleg -## The graphics handle of the legend object. -## -## @item hleg_obj -## Graphics handles to the text, patch, and line objects which form the -## legend. -## -## @item hplot -## Graphics handles to the plot objects which were used in making the legend. -## -## @item labels -## A cell array of strings of the labels in the legend. -## @end table +## The optional output value @var{hleg} is a handle to the legend object. ## ## Implementation Note: The legend label text is either provided in the call to ## @code{legend} or is taken from the @code{DisplayName} property of the @@ -125,9 +103,7 @@ ## this property whereas axes, figures, etc.@: do not so they are never present ## in a legend. If no labels or @code{DisplayName} properties are available, ## then the label text is simply @qcode{"data1"}, @qcode{"data2"}, @dots{}, -## @nospell{@qcode{"dataN"}}. No more than 20 data labels will be -## automatically generated. To label more, call @code{legend} explicitly and -## provide all labels. +## @nospell{@qcode{"dataN"}}. ## ## The legend @code{FontSize} property is initially set to 90% of the axes ## @code{FontSize} to which it is attached. Use @code{set} to override this @@ -140,1249 +116,1171 @@ function [hleg, hleg_obj, hplot, labels] = legend (varargin) - if (nargin > 0 - && (! ishghandle (varargin{1}) - || (strcmp (get (varargin{1}, "type"), "axes") - && ! strcmp (get (varargin{1}, "tag"), "legend")))) - [ca, varargin, nargin] = __plt_get_axis_arg__ ("legend", varargin{:}); - if (isempty (ca)) - ca = gca (); - endif - hfig = ancestor (ca, "figure"); - else - hfig = get (0, "currentfigure"); - if (isempty (hfig)) - hfig = gcf (); - endif - ca = gca (); - endif - - ## Special handling for plotyy which has two axes objects - if (isprop (ca, "__plotyy_axes__")) - plty = get (ca, "__plotyy_axes__"); - ca = [ca, plty.']; - ## Remove duplicates while preserving order - [~, n] = unique (ca, "first"); - ca = ca(sort (n)); - endif - - if (nargin > 0 && all (ishghandle (varargin{1}))) - ## List of plot objects to label given as first argument - kids = flipud (varargin{1}(:)); - varargin(1) = []; - else - ## Find list of plot objects from axes "children" - kids = ca; - kids(strcmp (get (ca, "tag"), "legend")) = []; - if (isscalar (kids)) - kids = get (kids, "children")(:); - else - kids = vertcat (flipud (get (kids, "children")){:}); - endif - endif - nargs = numel (varargin); - nkids = numel (kids); - - ## Find any existing legend object associated with axes - hlegend = []; - for hax = ca - try - hlegend = get (hax, "__legend_handle__"); - if (! isempty (hlegend)) - break; - endif - end_try_catch - endfor - - orientation = "default"; - location = "default"; - show = "create"; - textpos = "default"; - box = "default"; - delete_leg = false; - find_leg_hdl = (nargs == 0); # possibly overridden - propvals = {}; - - ## Find "location", "orientation", "textposition" property/value pairs - foundpos = foundorient = foundtextpos = false; - i = nargs - 1; - while (i > 0) - pos = varargin{i}; - str = varargin{i+1}; - if (strcmpi (pos, "location") && ischar (str)) - if (! foundpos) - location = lower (str); - foundpos = true; - endif - varargin(i:i+1) = []; - nargs -= 2; - elseif (strcmpi (pos, "orientation") && ischar (str)) - if (! foundorient) - orientation = lower (str); - foundorient = true; - endif - varargin(i:i+1) = []; - nargs -= 2; - elseif (strcmpi (pos, "textposition") && ischar (str)) - if (! foundtextpos) - textpos = lower (str); - foundtextpos = true; - endif - varargin(i:i+1) = []; - nargs -= 2; - endif - i -= 2; - endwhile - - ## Validate the orientation - if (! any (strcmp (orientation, {"vertical", "horizontal", "default"}))) - error ("legend: unrecognized legend orientation"); - endif - - ## Validate the texposition - if (! any (strcmp (textpos, {"left", "right", "default"}))) - error ("legend: unrecognized legend textposition"); - endif + opts = parse_opts (varargin{:}); - ## Validate the location type - outside = false; - inout = strfind (location, "outside"); - if (! isempty (inout)) - outside = true; - location = location(1:inout-1); - else - outside = false; - endif - - switch (location) - case {"north", "south", "east", "west", "northeast", "northwest", ... - "southeast", "southwest", "default"} - ## These are all valid locations, do nothing. - - case "best" - if (outside) - if (strcmp (orientation, "horizontal")) - location = "south"; - else - location = "northeast"; - endif - else - warning ("legend: 'best' not yet implemented for location specifier, using 'northeast' instead\n"); - location = "northeast"; - endif - - case "none" - ## FIXME: Should there be any more error checking on this? - - otherwise - error ("legend: unrecognized legend location"); - endswitch - - ## Finish input processing based on number of inputs - if (nargs == 0) - ## No labels given, create a new legend or return existing one - if (isempty (hlegend)) - show = "create"; - textpos = "right"; - find_leg_hdl = false; - endif - - elseif (nargs == 1) - ## Either OPTION value, single string label, or cellstr of labels. - arg = varargin{1}; - if (ischar (arg)) - if (rows (arg) == 1) - str = lower (strtrim (arg)); - switch (str) - case "off" - delete_leg = true; - case "hide" - show = "off"; - nargs -= 1; - case "show" - if (! isempty (hlegend)) - show = "on"; - else - show = "create"; - textpos = "right"; - endif - nargs -= 1; - case "toggle" - if (isempty (hlegend)) - show = "create"; - textpos = "right"; - elseif (strcmp (get (hlegend, "visible"), "off")) - show = "on"; - else - show = "off"; - endif - nargs -= 1; - case "boxon" - box = "on"; - nargs -= 1; - case "boxoff" - box = "off"; - nargs -= 1; - case "left" - textpos = "left"; - nargs -= 1; - case "right" - textpos = "right"; - nargs -= 1; - endswitch - else - ## Character matrix of labels - varargin = cellstr (arg); - nargs = numel (varargin); - endif - elseif (iscellstr (arg)) - ## Cell array of labels - varargin = arg; - nargs = numel (varargin); - else - error ("legend: single argument must be a string or cellstr"); - endif - - elseif (nargs > 1 && iscellstr (varargin{1})) - ## Cell array of labels followed by property/value pairs - propvals = varargin(2:end); - if (rem (numel (propvals), 2) != 0) - error ("legend: PROPERTY/VALUE arguments must occur in pairs"); - endif - varargin = {varargin{1}{:}}; - nargs = numel (varargin); + ## Use the old legend code to handle gnuplot toolkit + if (strcmp (graphics_toolkit (), "gnuplot")) + [hleg, hleg_obj, hplot, labels] = __gnuplot_legend__ (varargin{:}); + return; endif - have_labels = (nargs > 0); - hobjects = []; - hplots = []; - text_strings = {}; + hl = opts.legend_handle; + + ## Fix property/value pairs + pval = ["string", {opts.obj_labels}, opts.propval(:)']; + + if (! isempty (opts.action)) - if (delete_leg) - delete (hlegend); - hlegend = []; - elseif (find_leg_hdl) - ## Don't change anything about legend. - ## hleg output will be assigned hlegend value at end of function. - elseif (strcmp (show, "off")) - if (! isempty (hlegend)) - set (hlegend, "visible", "off"); - hlegend = []; - endif - elseif (strcmp (show, "on")) - if (! isempty (hlegend)) - set (hlegend, "visible", "on"); - ## NOTE: Matlab sets both "visible" and "box" to "on" for "show on" - ## set (hlegend, "box", "on"); - endif - elseif (strcmp (box, "on")) - if (! isempty (hlegend)) - set (hlegend, "box", "on"); - endif - elseif (strcmp (box, "off")) - if (! isempty (hlegend)) - set (hlegend, "box", "off"); - endif - elseif (! have_labels && ! isempty (hlegend) - && ! (strcmp (location, "default") - && strcmp (orientation, "default"))) - ## Changing location or orientation of existing legend - if (strcmp (location, "default")) - set (hlegend, "orientation", orientation); - elseif (strcmp (orientation, "default")) - if (outside) - set (hlegend, "location", [location "outside"]); - else - set (hlegend, "location", location); - endif - else - if (outside) - set (hlegend, "location", [location "outside"], - "orientation", orientation); - else - set (hlegend, "location", location, - "orientation", orientation); - endif - endif - else - ## Create or modify legend object + do_set_box = isempty (hl); - if (! isempty (hlegend)) - ## Disable callbacks while modifying an existing legend - setappdata (hlegend, "nocallbacks", true); - endif + switch opts.action + case "boxoff" + tmp_pval = {"box", "off"}; + do_set_box = false; - if (have_labels) - ## Check for valid data that can be labeled. - have_data = false; - have_dname = false; - for hkid = kids.' - typ = get (hkid, "type"); - if (any (strcmp (typ, {"line", "patch", "surface", "hggroup"}))) - have_data = true; - break; - endif - endfor + case "boxon" + tmp_pval = {"box", "on"}; + do_set_box = false; - if (! have_data) - warning ("legend: plot data is empty; setting key labels has no effect"); - endif - else - ## No labels. Search for DisplayName property. - have_dname = false; - n_dname = 0; - for hkid = kids.' - typ = get (hkid, "type"); - if (any (strcmp (typ, {"line", "patch", "surface", "hggroup"}))) - n_dname += 1; # count of objects which could be labeled - if (! isempty (get (hkid, "displayname"))) - have_dname = true; - break; - endif - endif - endfor - have_data = n_dname > 0; - endif + case "hide" + tmp_pval = {"visible", "off"}; - if (have_labels || ! have_dname) - k = nkids; - if (! have_labels) - ## No labels or DisplayName. Create set of "dataX" labels. - if (n_dname > 20) - warning ("legend: labeling only first 20 data objects"); - n_dname = 20; - endif - nargs = n_dname; - varargin = arrayfun (@(x) sprintf ("data%d", x), [1:nargs]', - "uniformoutput", false); - have_labels = true; - endif - for i = 1 : nargs - label = varargin{i}; - if (! ischar (label)) - error ("legend: expecting label to be a string"); - endif - ## Locate an object which can be labeled - while (k > 0) - typ = get (kids(k), "type"); - if (any (strcmp (typ, {"line","patch","surface","hggroup"}))) - break; - endif - k--; - endwhile - if (k > 0) - set (kids(k), "displayname", label); - hplots(end+1) = kids(k); - text_strings(end+1) = label; - k--; - else - if (have_data) - warning ("legend: ignoring extra labels"); - endif - break; # k = 0, no further handles to process - endif - endfor + case "show" + tmp_pval = {"visible", "on"}; - else - ## No labels specified but objects have DisplayName property set. - k = nkids; - while (k > 0) - ## Locate object to label - while (k > 0) - typ = get (kids(k), "type"); - if (any (strcmp (typ, {"line","patch","surface","hggroup"}))) - break; + case "toggle" + if (! isempty (hl)) + if (strcmp (get (hl, "visible"), "on")) + tmp_pval = {"visible", "off"}; + else + tmp_pval = {"visible", "on"}; endif - k--; - endwhile - if (k > 0) - dname = get (kids(k), "displayname"); - if (! isempty (dname)) - hplots(end+1) = kids(k); - text_strings(end+1) = dname; - endif - k--; - endif - endwhile - endif - - if (isempty (hplots)) - ## Nothing to label - if (! isempty (hlegend)) - delete (hlegend); - hlegend = []; - hobjects = []; - hplots = []; - text_strings = {}; - endif - else - ## Preserve the old legend if it exists - if (! isempty (hlegend)) - if (strcmp (textpos, "default")) - textpos = get (hlegend, "textposition"); - endif - if (strcmp (location, "default")) - location = get (hlegend, "location"); - inout = strfind (location, "outside"); - if (! isempty (inout)) - outside = true; - location = location(1:inout-1); - else - outside = false; - endif - endif - if (strcmp (orientation, "default")) - orientation = get (hlegend, "orientation"); - endif - box = get (hlegend, "box"); - else - if (strcmp (textpos, "default")) - textpos = "right"; - endif - if (strcmp (location, "default")) - location = "northeast"; - endif - if (strcmp (orientation, "default")) - orientation = "vertical"; - endif - box = "on"; - endif - - ## Use axis which is appropriate for legend location. - ## This is only necessary for plotyy figures where there are two axes. - if (numel (ca) == 1) - cax = ca(1); - elseif (strfind (location, "east")) - cax = ca(2); - else - cax = ca(1); - endif - ## Get axis size and fontsize in points. - ## Rely on listener to handle conversion. - units = get (cax, "units"); - unwind_protect - set (cax, "units", "points", "fontunits", "points"); - if (isempty (hlegend) || ! isprop (hlegend, "unmodified_axes_position")) - unmodified_axes_position = get (cax, "position"); - unmodified_axes_outerposition = get (cax, "outerposition"); - else - unmodified_axes_position = get (hlegend, "unmodified_axes_position"); - unmodified_axes_outerposition = get (hlegend, ... - "unmodified_axes_outerposition"); - endif - ca_pos = unmodified_axes_position; - ca_outpos = unmodified_axes_outerposition; - tightinset = get (ca(1), "tightinset"); - for i = 2 : numel (ca) - tightinset = max (tightinset, get (ca(i), "tightinset")); - endfor - unwind_protect_cleanup - set (cax, "units", units); - end_unwind_protect - - ## Padding between legend entries horizontally and vertically - ## measured in points. - ## FIXME: 3*xpad must be integer or strange off-by-1 pixel issues - ## with lines in OpenGL. - xpad = 2 + 1/3; - ypad = 4; - - bpad = 8; # padding of legend box from surrounding axes - - linelength = 15; - - ## Preamble code to restore figure and axes after legend creation - origfig = get (0, "currentfigure"); - if (origfig != hfig) - set (0, "currentfigure", hfig); - else - origfig = []; - endif - origaxes = get (hfig, "currentaxes"); - unwind_protect - ud = ancestor (hplots, "axes"); - if (! isscalar (ud)) - ud = unique ([ud{:}]); - endif - hpar = get (ud(1), "parent"); - - if (isempty (hlegend)) - ## Create a legend object (axes + new properties) - addprops = true; - hlegend = axes ("parent", hpar, "tag", "legend", - "box", box, - "xtick", [], "ytick", [], - "xlim", [0, 1], "ylim", [0, 1], - "activepositionproperty", "position"); - setappdata (hlegend, "__axes_handle__", ud); - try - addproperty ("__legend_handle__", ud(1), "handle", hlegend); - catch - set (ud(1), "__legend_handle__", hlegend); - end_try_catch - - ## Inherit fontsize from current axis - ## "fontunits" should be first because it affects interpretation - ## of "fontsize" property. - [fontunits, fontsz] = get (ca(1), {"fontunits", "fontsize"}){:}; - fontsz *= 0.90; # Reduce legend fontsize to 90% of axes fontsize - set (hlegend, {"fontunits", "fontsize"}, {fontunits, fontsz}); - set (hlegend, "fontunits", "points"); # legend always works in pts. - ## Also inherit colormap from axes if it is different than figure - cax_cmap = get (cax, "colormap"); - if (! isequal (cax_cmap, get (hpar, "colormap"))) - set (hlegend, "colormap", cax_cmap); - endif - old_hplots = []; - else - ## Re-use existing legend. - addprops = false; - axes (hlegend); - delete (get (hlegend, "children")); - ## Hack: get list of hplots for which addlistener has been called. - old_hplots = get (hlegend, "deletefcn"){6}; endif - if (addprops) - ## Only required for a newly created legend object - ## FIXME: "autoupdate" is not implemented. - addproperty ("autoupdate", hlegend, "radio", "{on}|off"); - addproperty ("edgecolor", hlegend, "color", [0.15, 0.15, 0.15]); - addproperty ("textcolor", hlegend, "color", [0, 0, 0]); - locations = {"north", "south", "east", "west", ... - "{northeast}", "southeast", "northwest", "southwest", ... - "northoutside", "southoutside", ... - "eastoutside", "westoutside", ... - "northeastoutside", "southeastoutside", ... - "northwestoutside", "southwestoutside", "best", ... - "bestoutside", "none"}; - addproperty ("location", hlegend, "radio", strjoin (locations, "|")); - addproperty ("orientation", hlegend, "radio", - "{vertical}|horizontal"); - addproperty ("string", hlegend, "any", text_strings); - addproperty ("interpreter", hlegend, "textinterpreter"); - addproperty ("textposition", hlegend, "radio", "left|{right}"); - endif - - ## Apply any PROPERTY/VALUE pairs given as arguments - if (! isempty (propvals)) - set (hlegend, propvals{:}); - endif - - ## Special case of PROPERTY "edgecolor" (bug #56968) - ec_idx = find (strcmpi (propvals, "edgecolor"), 1, "last"); - if (! isempty (ec_idx)) - ec_color = propvals{ec_idx + 1}; - set (hlegend, "xcolor", ec_color, "ycolor", ec_color); - endif - - ## Text objects in key inherit visual properties from legend object - legprops = { "fontunits", "fontangle", "fontname", "fontsize", ... - "fontweight", "interpreter", "textcolor" }; - - txtprops = { "fontunits", [], "fontangle", [] "fontname", [], ... - "fontsize", [], "fontweight", [] "interpreter", [], ... - "color", [] }; - propvals = get (hlegend, legprops); - txtprops(2:2:end) = propvals; + case "left" + tmp_pval = {"textposition", "left"}; - ## Add text labels to the axes first and check their extents - nentries = numel (hplots); - texthandle = []; - maxwidth = maxheight = 0; - for k = 1 : nentries - halign = ifelse (strcmp (textpos, "right"), "left", "right"); - texthandle(k) = text (0, 0, text_strings{k}, - "units", "points", - "horizontalalignment", halign, - txtprops{:}); - setappdata (texthandle(k), "handle", hplots(k)); - extents = get (texthandle(k), "extent"); - maxwidth = max (maxwidth, extents(3)); - maxheight = max (maxheight, extents(4)); - endfor - ## Restore units which were forced to points - set (texthandle, "units", get (0, "DefaultTextUnits")); - - num1 = nentries; - if (strcmp (orientation, "vertical")) - height = nentries * (ypad + maxheight); - if (outside) - if (height > ca_pos(4)) - ## Avoid shrinking the height of the axis to zero if outside - num1 = ca_pos(4) / (maxheight + ypad) / 2; - endif - else - if (height > 0.9 * ca_pos(4)) - num1 = 0.9 * ca_pos(4) / (maxheight + ypad); - endif - endif - else - width = nentries * (ypad + maxwidth); - if (outside) - if (width > ca_pos(3)) - ## Avoid shrinking the width of the axis to zero if outside - num1 = ca_pos(3) / (maxwidth + ypad) / 2; - endif - else - if (width > 0.9 * ca_pos(3)) - num1 = 0.9 * ca_pos(3) / (maxwidth + ypad); - endif - endif - endif - num2 = ceil (nentries / num1); - - ## Layout is [xpad, linelength, xpad, maxwidth, xpad] - xstep = 3 * xpad + (maxwidth + linelength); - if (strcmp (textpos, "right")) - xoffset = xpad; - txoffset = 2 * xpad + linelength; - else - xoffset = 2 * xpad + maxwidth; - txoffset = xpad + maxwidth; - endif - ystep = (ypad + maxheight); - yoffset = ystep / 2; - - ## Place the legend in the desired location - if (strcmp (orientation, "vertical")) - lpos = [0, 0, num2 * xstep, num1 * ystep]; - else - lpos = [0, 0, num1 * xstep, num2 * ystep]; - endif + case "right" + tmp_pval = {"textposition", "right"}; - gnuplot = strcmp (get (hfig, "__graphics_toolkit__"), "gnuplot"); - if (gnuplot) - ## gnuplot places the key (legend) at edge of the figure window. - ## OpenGL places the legend box at edge of the unmodified axes - ## position. - if (isempty (strfind (location, "east"))) - gnuplot_offset = unmodified_axes_outerposition(1) ... - + unmodified_axes_outerposition(3) ... - - unmodified_axes_position(1) ... - - unmodified_axes_position(3); - else - gnuplot_offset = unmodified_axes_position(1) ... - - unmodified_axes_outerposition(1); - endif - ## FIXME: The "fontsize" is added to match the behavior of OpenGL. - ## This implies that a change in fontsize should trigger a listener - ## to update the legend. The "2" was determined using a long legend - ## key in the absence of any subplots. - gnuplot_offset -= 2 * get (hlegend, "fontsize"); - else - gnuplot_offset = 0; + case "off" + if (! isempty (hl)) + delete (hl) endif - - ## For legend's outside the associated axes position, - ## align their edge to the unmodified_axes_outerposition, - ## and adjust the axes position accordingly. - switch (location) - case "north" - if (outside) - lpos = [ca_pos(1) + (ca_pos(3) - lpos(3)) / 2, ... - ca_outpos(2) + ca_outpos(4) - lpos(4) - bpad/2, ... - lpos(3), lpos(4)]; - - new_pos = [ca_pos(1), ca_pos(2), ca_pos(3), ca_pos(4) - lpos(4)]; - else - lpos = [ca_pos(1) + (ca_pos(3) - lpos(3)) / 2, ... - ca_pos(2) + ca_pos(4) - lpos(4) - bpad, ... - lpos(3), lpos(4)]; - endif - case "south" - if (outside) - lpos = [ca_pos(1) + (ca_pos(3) - lpos(3)) / 2, ... - ca_outpos(2) + bpad/2, lpos(3), lpos(4)]; - new_pos = [ca_pos(1), ... - lpos(2) + lpos(4) + bpad/2 + tightinset(2), ... - ca_pos(3), ca_pos(4) - lpos(4)]; - else - lpos = [ca_pos(1) + (ca_pos(3) - lpos(3)) / 2, ... - ca_pos(2) + bpad, lpos(3), lpos(4)]; - endif - case "east" - if (outside) - lpos = [ca_outpos(1) + ca_outpos(3) - lpos(3) - bpad/2, ... - ca_pos(2) + (ca_pos(4) - lpos(4)) / 2, ... - lpos(3), lpos(4)]; - new_pos = [ca_pos(1), ca_pos(2), ... - lpos(1) - bpad - tightinset(3) - ca_pos(1), ... - ca_pos(4)]; - new_pos(3) += gnuplot_offset; - else - lpos = [ca_pos(1) + ca_pos(3) - lpos(3) - bpad, ... - ca_pos(2) + (ca_pos(4) - lpos(4)) / 2, lpos(3), lpos(4)]; - endif - case "west" - if (outside) - lpos = [ca_outpos(1) + bpad/2, ... - ca_pos(2) + (ca_pos(4) - lpos(4)) / 2, ... - lpos(3), lpos(4)]; - new_pos = [lpos(1) + lpos(3) + bpad/2 + tightinset(1), ... - ca_pos(2), ca_pos(3) - lpos(3) - bpad/2, ca_pos(4)]; - new_pos([1, 3]) += [-gnuplot_offset, gnuplot_offset]; - else - lpos = [ca_pos(1) + bpad, ... - ca_pos(2) + (ca_pos(4) - lpos(4)) / 2, lpos(3), lpos(4)]; - endif - case "northeast" - if (outside) - lpos = [ca_outpos(1) + ca_outpos(3) - lpos(3) - bpad/2, ... - ca_pos(2) + ca_pos(4) - lpos(4), ... - lpos(3), lpos(4)]; - new_pos = [ca_pos(1), ca_pos(2), ... - lpos(1) - bpad - tightinset(3) - ca_pos(1), ... - ca_pos(4)]; - new_pos(3) += gnuplot_offset; - else - lpos = [ca_pos(1) + ca_pos(3) - lpos(3) - bpad, ... - ca_pos(2) + ca_pos(4) - lpos(4) - bpad, ... - lpos(3), lpos(4)]; - endif - case "northwest" - if (outside) - lpos = [ca_outpos(1) + bpad/2, ... - ca_pos(2) + ca_pos(4) - lpos(4), ... - lpos(3), lpos(4)]; - new_pos = [lpos(1) + lpos(3) + bpad/2 + tightinset(1), ... - ca_pos(2), ca_pos(3) - lpos(3) - bpad/2, ca_pos(4)]; - new_pos([1, 3]) += [-gnuplot_offset, gnuplot_offset]; - else - lpos = [ca_pos(1) + bpad, ... - ca_pos(2) + ca_pos(4) - lpos(4) - bpad, ... - lpos(3), lpos(4)]; - endif - case "southeast" - if (outside) - lpos = [ca_outpos(1) + ca_outpos(3) - lpos(3) - bpad/2, ... - ca_pos(2), lpos(3), lpos(4)]; - new_pos = [ca_pos(1), ca_pos(2), ... - lpos(1) - bpad - ca_pos(1) - tightinset(3), ... - ca_pos(4)]; - new_pos(3) += gnuplot_offset; - else - lpos = [ca_pos(1) + ca_pos(3) - lpos(3) - bpad, ... - ca_pos(2) + bpad, lpos(3), lpos(4)]; - endif - case "southwest" - if (outside) - lpos = [ca_outpos(1) + bpad/2, ca_pos(2), lpos(3), lpos(4)]; - new_pos = [lpos(1) + lpos(3) + bpad/2 + tightinset(1), ... - ca_pos(2), ca_pos(3) - lpos(3) - bpad/2, ca_pos(4)]; - new_pos([1, 3]) += [-gnuplot_offset, gnuplot_offset]; - else - lpos = [ca_pos(1) + bpad, ca_pos(2) + bpad, lpos(3), lpos(4)]; - endif - endswitch - - units = get (hlegend, "units"); - unwind_protect - set (hlegend, "units", "points", "position", lpos); - unwind_protect_cleanup - set (hlegend, "units", units); - end_unwind_protect + return; - ## Now write the line segments and place the text objects correctly - xk = yk = 0; - for k = 1 : numel (hplots) - hobjects(end+1) = texthandle(k); - hplt = hplots(k); - typ = get (hplt, "type"); - ## For an hggroup, find an underlying primitive object - if (strcmp (typ, "hggroup")) - for hgkid = get (hplt, "children").' - hgkid_type = get (hgkid, "type"); - if (any (strcmp (hgkid_type, {"line","patch","surface"}))) - typ = hgkid_type; - hplt = hgkid; - break; - endif - endfor - endif - - switch (typ) - - case "line" - color = get (hplt, "color"); - style = get (hplt, "linestyle"); - lwidth = min (get (hplt, "linewidth"), 5); - if (! strcmp (style, "none")) - l1 = __go_line__ (hlegend, ... - "xdata", ([xoffset, xoffset + linelength] + xk * xstep) / lpos(3), ... - "ydata", [1, 1] .* (lpos(4) - yoffset - yk * ystep) / lpos(4), ... - "color", color, "linestyle", style, ... - "linewidth", lwidth, "marker", "none"); - setappdata (l1, "handle", hplt); - hobjects(end+1) = l1; - endif - marker = get (hplt, "marker"); - if (! strcmp (marker, "none")) - l1 = __go_line__ (hlegend, ... - "xdata", (xoffset + 0.5 * linelength + xk * xstep) / lpos(3), ... - "ydata", (lpos(4) - yoffset - yk * ystep) / lpos(4), ... - "color", color, "linestyle", "none", ... - "linewidth", lwidth, "marker", marker, ... - "markeredgecolor", get (hplt, "markeredgecolor"), ... - "markerfacecolor", get (hplt, "markerfacecolor"), ... - "markersize", min (get (hplt, "markersize"),10)); - setappdata (l1, "handle", hplt); - hobjects(end+1) = l1; - endif - - ## Newly labeled objects have listeners added - if (! any (hplt == old_hplots)) - addlistener (hplt, "color", - {@cb_line_listener, hlegend, linelength, false}); - addlistener (hplt, "linestyle", - {@cb_line_listener, hlegend, linelength, false}); - addlistener (hplt, "linewidth", - {@cb_line_listener, hlegend, linelength, false}); - addlistener (hplt, "marker", - {@cb_line_listener, hlegend, linelength, false}); - addlistener (hplt, "markeredgecolor", - {@cb_line_listener, hlegend, linelength, false}); - addlistener (hplt, "markerfacecolor", - {@cb_line_listener, hlegend, linelength, false}); - addlistener (hplt, "markersize", - {@cb_line_listener, hlegend, linelength, false}); - addlistener (hplt, "displayname", - {@cb_line_listener, hlegend, linelength, true}); - endif - - case "patch" - facecolor = get (hplt, "facecolor"); - edgecolor = get (hplt, "edgecolor"); - cdata = get (hplt, "cdata"); - if (! strcmp (facecolor, "none") || ! strcmp (edgecolor, "none")) - p1 = patch ("xdata", ([0, linelength, linelength, 0] + - xoffset + xk * xstep) / lpos(3), - "ydata", (lpos(4) - yoffset - - [yk-0.3, yk-0.3, yk+0.3, yk+0.3] .* ystep) / lpos(4), - "facecolor", facecolor, "edgecolor", edgecolor, - "cdata", cdata); - setappdata (p1, "handle", hplt); - else - ## non-standard patch only making use of marker styles - ## such as scatter plot. - p1 = patch ("xdata", (xoffset + 0.5 * linelength + xk * xstep) / lpos(3), - "ydata", (lpos(4) - yoffset - yk * ystep) / lpos(4), - "marker", get (hplt, "marker"), - "markeredgecolor",get (hplt,"markeredgecolor"), - "markerfacecolor",get (hplt,"markerfacecolor"), - "markersize", min (get (hplt,"markersize"),10), - "cdata", cdata); - setappdata (p1, "handle", hplt); - endif - hobjects(end+1) = p1; - ## Copy clim from axes so that colors work out. - set (hlegend, "clim", get (ca(1), "clim")); - - ## FIXME: Need listeners, as for line objects. - ## Changing clim, for example, won't update colors - - case "surface" - facecolor = get (hplt, "facecolor"); - edgecolor = get (hplt, "edgecolor"); - cdata = sum (get (ca(1), "clim")) / 2; - if (! strcmp (facecolor, "none") || ! strcmp (edgecolor, "none")) - p1 = patch ("xdata", ([0, linelength, linelength, 0] + - xoffset + xk * xstep) / lpos(3), - "ydata", (lpos(4) - yoffset - - [yk-0.3, yk-0.3, yk+0.3, yk+0.3] .* ystep) / lpos(4), - "facecolor", facecolor, "edgecolor", edgecolor, - "cdata", cdata); - setappdata (p1, "handle", hplt); - hobjects(end+1) = p1; - endif - ## FIXME: Need listeners, as for line objects. - - endswitch + endswitch - set (texthandle(k), "position", - [(txoffset + xk * xstep) / lpos(3), ... - (lpos(4) - yoffset - yk * ystep) / lpos(4)]); - if (strcmp (orientation, "vertical")) - yk += 1; - if (yk > num1) - yk = 0; - xk += 1; - endif - else - xk += 1; - if (xk > num1) - xk = 0; - yk += 1; - endif - endif - endfor - - ## Add an invisible text object to original axis - ## that, when it is destroyed, will remove the legend. - htdel = findall (ca(1), "-depth", 1, "tag", "deletelegend", - "type", "text"); - if (isempty (htdel)) - htdel = text (0, 0, "", "parent", ca(1), "tag", "deletelegend", - "visible", "off", "handlevisibility", "off", - "xliminclude", "off", "yliminclude", "off", - "zliminclude", "off"); - set (htdel, "deletefcn", {@cb_axes_deleted, ca, hlegend}); - endif - if (isprop (hlegend, "unmodified_axes_position")) - set (hlegend, "unmodified_axes_position", - unmodified_axes_position, - "unmodified_axes_outerposition", - unmodified_axes_outerposition); - else - addproperty ("unmodified_axes_position", hlegend, - "data", unmodified_axes_position); - addproperty ("unmodified_axes_outerposition", hlegend, - "data", unmodified_axes_outerposition); - endif - - ## Resize the axis that the legend is attached to if the legend is - ## "outside" the plot and create a listener to resize axis to original - ## size if the legend is deleted, hidden, or shown. - if (outside) - for i = 1 : numel (ca) - units = get (ca(i), "units"); - unwind_protect - set (ca(i), "units", "points"); - if (gnuplot && numel (ca) == 1) - ## Let gnuplot handle the positioning of the keybox. - ## This violates strict Matlab compatibility, but reliably - ## renders an aesthetic result. - set (ca(i), "position", unmodified_axes_position, - "activepositionproperty", "outerposition"); - else - ## numel (ca) > 1 for axes overlays (like plotyy) - set (ca(i), "position", new_pos); - endif - unwind_protect_cleanup - set (ca(i), "units", units); - end_unwind_protect - endfor + pval = [tmp_pval, pval]; + if (do_set_box) + pval = [pval, "box", "on"]; + endif - set (hlegend, "deletefcn", {@cb_restore_axes, ca, ... - unmodified_axes_position, ... - unmodified_axes_outerposition, ... - htdel, hplots}); - addlistener (hlegend, "visible", {@cb_legend_hideshow, ca, ... - unmodified_axes_position, ... - new_pos}); - else - set (hlegend, "deletefcn", {@cb_restore_axes, ca, [], [], ... - htdel, hplots}); - endif - - if (! addprops) - ## Remove listeners on existing legend temporarily to stop recursion. - dellistener (hlegend, "location"); - dellistener (hlegend, "orientation"); - dellistener (hlegend, "string"); - dellistener (hlegend, "textposition"); - endif - - if (! addprops) - set (hlegend, "string", text_strings); - endif - - if (outside) - set (hlegend, "location", [location "outside"], - "orientation", orientation, "textposition", textpos); - else - set (hlegend, "location", location, "orientation", orientation, - "textposition", textpos); - endif + elseif (isempty (hl)) - if (addprops) - addlistener (cax, "colormap", {@cb_legend_colormap_update, hlegend}); - addlistener (hlegend, "edgecolor", @cb_legend_text_update); - addlistener (hlegend, "fontangle", @cb_legend_text_update); - addlistener (hlegend, "fontname", @cb_legend_text_update); - addlistener (hlegend, "fontweight", @cb_legend_text_update); - addlistener (hlegend, "textcolor", @cb_legend_text_update); - ## Properties which could change size of box, such as fontsize, - ## require legend to be redrawn. - ## FIXME: fontsize is changed by print.m function during the - ## production of a plot for output. This screws things up - ## because legend tries to return the axes size to what it - ## was when the figure was created, versus what it is now - ## when the figure is being printed. Temporary hack is - ## good enough for generating the Octave manual which still - ## relies on gnuplot for generating images. See bug #40333. - if (! gnuplot) - addlistener (hlegend, "fontsize", @cb_legend_update); - endif - addlistener (hlegend, "fontunits", @cb_legend_update); - addlistener (hlegend, "interpreter", @cb_legend_update); - addlistener (hlegend, "location", @cb_legend_location); - addlistener (hlegend, "orientation", @cb_legend_update); - addlistener (hlegend, "string", @cb_legend_update); - addlistener (hlegend, "textposition", @cb_legend_update); - ## FIXME: need to add listeners for tightinset and position - ## addlistener (ca, "tightinset", @update????); - ## addlistener (ca, "position", @update????); - else - ## Restore listeners temporarily disabled during reconstruction. - addlistener (hlegend, "location", @cb_legend_update); - addlistener (hlegend, "orientation", @cb_legend_update); - addlistener (hlegend, "string", @cb_legend_update); - addlistener (hlegend, "textposition", @cb_legend_update); - endif + pval = ["fontsize", (0.9 * get (opts.axes_handles(1), "fontsize")), ... + "box", "on", pval]; - unwind_protect_cleanup - set (hfig, "currentaxes", origaxes); - if (! isempty (origfig)) - set (0, "currentfigure", origfig); - endif - end_unwind_protect - endif endif - ## Restore operation of callbacks - setappdata (hlegend, "nocallbacks", false); + if (isempty (hl)) + + hl = axes ("handlevisibility", "off", ... + "tag", "legend", "ydir", "reverse", ... + "position", [.5 .5 .3 .3], "xtick", [], "ytick", [], ... + "tag", "legend"); + + ## FIXME: Use the axes appdata to store its peer legend handle + ## rather that adding a public property and change all uses. + try + addproperty ("__legend_handle__", opts.axes_handles, "handle", hl); + catch + set (opts.axes_handles, "__legend_handle__", hl); + end_try_catch + + ## Add and update legend specific properties + addproperties (hl); + try + set (hl, pval{:}); + catch ee + delete (hl); + set (opts.axes_handles, "__legend_handle__", []); + rethrow (ee); + end_try_catch + + + ## Update legend layout + setappdata (hl, "__axes_handle__", opts.axes_handles, ... + "__next_label_index__", opts.next_idx, ... + "__peer_objects__", opts.obj_handles); + update_location_cb (hl, [], false); + update_layout_cb (hl, [], true); + update_numchild_cb (hl); + + ## Dummy invisible object that deletes the legend when "newplot" is called + ht = __go_text__ (opts.axes_handles(1), "visible", "off", ... + "handlevisibility", "off", ... + "tag", "__legend_watcher__", ... + "deletefcn", {@reset_cb, hl}); + + ## Listeners to foreign objects properties are stored for later + ## deletion in "delfunction" + setappdata (hl, "__listeners__", {}); + add_safe_listener (hl, ancestor (opts.axes_handles(1), "figure") , ... + "position", {@maybe_update_layout_cb, hl}); + add_safe_listener (hl, opts.axes_handles(1), ... + "position", {@maybe_update_layout_cb, hl}); + add_safe_listener (hl, opts.axes_handles(1), ... + "tightinset", ... + @(h) update_layout_cb (get (h, "__legend_handle__"))); + add_safe_listener (hl, opts.axes_handles(1), ... + "colormap", ... + @(hax) set (hl, "colormap", get (hax, "colormap"))); + add_safe_listener (hl, opts.axes_handles(1), ... + "fontsize", ... + @(hax) set (hl, "fontsize", 0.9*get (hax, "fontsize"))); + add_safe_listener (hl, opts.axes_handles(1), ... + "children", {@legend_autoupdate_cb, hl}); + + ## Listeners to legend properties + props = {"fontsize", "fontweight", "fontname", "interpreter", ... + "textposition", "numcolumnsmode", "numcolumns", "orientation"}; + + for ii = 1:numel (props) + addlistener (hl, props{ii}, {@update_layout_cb, true}); + endfor + + addlistener (hl, "autoupdate", @update_numchild_cb); + + addlistener (hl, "beingdeleted", @delete_legend_cb); + + addlistener (hl, "box", @update_box_cb); + + addlistener (hl, "edgecolor", ... + @(h) set (hl, "xcolor", get (hl, "edgecolor"), ... + "ycolor", get (hl, "edgecolor"))); + + addlistener (hl, "location", @update_location_cb); + + addlistener (hl, "position", @update_position_cb); + + addlistener (hl, "string", @update_string_cb); + + addlistener (hl, "textcolor", ... + @(h) set (findobj (h, "type", "text"), ... + "color", get (hl, "textcolor"))); + + else + + ## FIXME: this will trigger the execution of update_layout_cb + ## for each watched property. Should we suspend its execution with + ## yet another appdata bool property? + + ## Update properties + setappdata (hl, "__peer_objects__", opts.obj_handles); + set (hl, pval{:}); + + endif if (nargout > 0) - hleg = hlegend; - hleg_obj = hobjects; - hplot = hplots; - labels = text_strings; + hleg = hl; + ## Those ones are needed for backward compatibility + hleg_obj = get (hl, "children"); + hplot = getappdata (hl, "__peer_objects__"); + labels = get (hl, "string"); + endif + + set (hl, "handlevisibility", "on"); + +endfunction + +function update_box_cb (hl) + + if (strcmp (get (hl, "box"), "on")) + if (strcmp (get (hl, "color"), "none")) + set (hl, "color", "w"); + endif + else + set (hl, "color", "none"); endif endfunction -## Colormap of the base axes has changed. -function cb_legend_colormap_update (cax, ~, hlegend) - set (hlegend, "colormap", get (cax, "colormap")); +function update_location_cb (hl, ~, do_layout = true) + + if (strcmp (get (hl, "location"), "best")) + warning ("Octave:legend:unimplemented-location", ... + ["legend: 'best' not yet implemented for location ", ... + "specifier, using 'northeast' instead\n"]); + endif + + if (do_layout) + update_layout_cb (hl); + endif + +endfunction + +function update_position_cb (hl) + + updating = getappdata (hl, "__updating_layout__"); + if (isempty (updating) || ! updating) + set (hl, "location", "none") + endif + endfunction -## A non-text property of legend has changed requiring an update. -function cb_legend_update (hleg, ~) - persistent recursive = false; +function update_string_cb (hl) - if (! recursive) - recursive = true; + ## Check adequation between the number of legend item and the number + ## of labels before calling update_layout_cb + persistent updating = false; + if (! updating) + updating = true; unwind_protect - hax = getappdata (hleg, "__axes_handle__"); - ## Hack. Maybe store this somewhere else such as appdata. - hplots = get (hleg, "deletefcn"){6}; - text_strings = get (hleg, "string"); - position = get (hleg, "unmodified_axes_position"); - outerposition = get (hleg, "unmodified_axes_outerposition"); - units = get (hax, "units"); - set (hax, "units", "points"); - switch (get (hax, "activepositionproperty")) - case "position" - set (hax, "outerposition", outerposition, "position", position); - case "outerposition" - set (hax, "position", position, "outerposition", outerposition); - endswitch - if (isscalar (hax)) - set (hax, "units", units); - else - set (hax, {"units"}, units); + str = get (hl, "string"); + nstr = numel (str); + + obj = getappdata (hl, "__peer_objects__"); + nobj = numel (obj); + + if (ischar (str) && nobj != 1) + setappdata (hl, "__peer_objects__", obj(1)) + elseif (iscellstr (str) && nobj != nstr) + if (nobj > nstr) + setappdata (hl, "__peer_objects__", obj(1:nstr)) + elseif (nobj == 1) + set (hl, "string", str{1}); + else + set (hl, "string", str(1:nobj)); + endif endif - - hleg = legend (hax(1), hplots, text_strings); + update_layout_cb (hl, [], true); unwind_protect_cleanup - recursive = false; + updating = false; end_unwind_protect endif endfunction -## A text property of legend, such as fontname, has changed. -function cb_legend_text_update (hleg, ~) - - kids = get (hleg, "children"); - htext = kids(strcmp (get (kids, "type"), "text")); - - tprops = {"fontangle", "fontname", "fontweight", "color"}; - lprops = {"fontangle", "fontname", "fontweight", "textcolor"}; - set (htext, tprops, get (hleg, lprops)); - - ec = get (hleg, "edgecolor"); - set (hleg, "xcolor", ec, "ycolor", ec); - -endfunction - -## The legend "visible" property has changed. -function cb_legend_hideshow (hleg, ~, ca, orig_pos, new_pos) - - isvisible = strcmp (get (hleg, "visible"), "on"); +function reset_cb (ht, evt, hl, deletelegend = true) - ## FIXME: Can't use a single set() call because of linked axes and - ## listeners on plotyy graphs. - ca = ca(isaxes (ca)); - for cax = ca(:).' - units = get (cax, "units"); - unwind_protect - set (cax, "units", "points"); - if (isvisible) - set (cax, "position", new_pos); - else - set (cax, "position", orig_pos); - endif - unwind_protect_cleanup - set (cax, "units", units); - end_unwind_protect - endfor + if (ishghandle (hl)) + listeners = getappdata (hl, "__listeners__"); + for ii = 1:numel (listeners) + dellistener (listeners{ii}{:}) + endfor -endfunction - -## The legend "location" property has changed. -function cb_legend_location (hleg, d) - - ## If it isn't "none", which means manual positioning, then rebuild . - if (! strcmp (get (hleg, "location"), "none")) - cb_legend_update (hleg, d); + if (deletelegend) + delete (hl); + endif endif endfunction -## Axes to which legend was attached is being deleted/reset. Delete legend. -function cb_axes_deleted (~, ~, ca, hlegend) - if (isaxes (hlegend)) - if (strcmp (get (ca(1), "beingdeleted"), "on")) - ## Axes are being deleted. Disable call to cb_restore_axes. - set (hlegend, "deletefcn", []); - endif - delete (hlegend); - endif +function delete_legend_cb (hl) + + reset_cb ([], [], hl, false); + + hax = getappdata (hl, "__axes_handle__")(1); + units = get (hax, "units"); + set (hax, "units", getappdata (hl, "__original_units__"), ... + "looseinset", getappdata (hl, "__original_looseinset__"), ... + "units", units, "__legend_handle__", []); + endfunction -## Restore position of axes object when legend is deleted. -function cb_restore_axes (~, ~, ca, pos, outpos, htdel, hplots) +function add_safe_listener (hl, varargin) - hf = ancestor (ca(1), "figure"); - if (strcmp (get (hf, "beingdeleted"), "on") - || strcmp (get (ca(1), "beingdeleted"), "on")) - ## Skip restoring axes if entire figure or axes is being destroyed. - return; - endif - - ## Remove text object used to trigger legend delete when axes is deleted - if (ishghandle (htdel)) - set (htdel, "deletefcn", []); - delete (htdel); - endif + addlistener (varargin{:}); - ## Restore original axes positions - if (! isempty (pos)) - ## FIXME: can't use single call to set() because of weirdness w/plotyy - for cax = ca(:).' - if (isaxes (cax)) - units = get (cax, "units"); - unwind_protect - set (cax, "units", "points", "position", pos); - unwind_protect_cleanup - set (cax, "units", units); - end_unwind_protect - endif - endfor - endif - - ## Remove listeners from plot objects - for i = 1 : numel (hplots) - if (isgraphics (hplots(i), "line")) - dellistener (hplots(i), "color"); - dellistener (hplots(i), "linestyle"); - dellistener (hplots(i), "linewidth"); - dellistener (hplots(i), "marker"); - dellistener (hplots(i), "markeredgecolor"); - dellistener (hplots(i), "markerfacecolor"); - dellistener (hplots(i), "markersize"); - dellistener (hplots(i), "displayname"); - endif - endfor - - ## Nullify legend link (can't delete properties yet) - set (ca(1), "__legend_handle__", []); + lsn = getappdata (hl, "__listeners__"); + lsn = [lsn {varargin}]; + setappdata (hl, "__listeners__", lsn); endfunction -## Update legend item because underlying plot line object has changed. -function cb_line_listener (h, ~, hlegend, linelength, update_name) +function addproperties (hl) - ## Don't execute callbacks when legend is under construction - legdata = getappdata (hlegend); - if (legdata.nocallbacks) - return; - endif + persistent default = {"north", "northoutside", ... + "south", "southoutside", ... + "east", "eastoutside", ... + "west", "westoutside", ... + "{northeast}", "northeastoutside", ... + "northwest", "northwestoutside", ... + "southeast", "southeastoutside", ... + "southwest", "southwestoutside", ... + "best", "bestoutside", ... + "none"}; + + addproperty ("location", hl, "radio", strjoin (default(:), "|")); - if (update_name) - ## When string changes, have to rebuild legend completely - [hplots, text_strings] = __getlegenddata__ (hlegend); - if (isempty (hplots)) - delete (hlegend); - else - legend (legdata.handle(1), hplots, text_strings); - endif - else - kids = get (hlegend, "children"); - kids = kids([getappdata(kids, "handle"){:}] == h); - kids = kids(strcmp (get (kids, "type"), "line")); - idx = strcmp (get (kids, "marker"), "none"); - ll = kids (idx); - lm = kids (! idx); + addproperty ("orientation", hl, "radio", "{vertical}|horizontal"); + + addproperty ("numcolumns", hl, "double", 1); + + addproperty ("numcolumnsmode", hl, "radio", "{auto}|manual"); + + addlistener (hl, "numcolumns", @(h) set (h, "numcolumnsmode", "manual")); - [linestyle, marker, displayname] = ... - get (h, {"linestyle", "marker", "displayname"}){:}; + addproperty ("autoupdate", hl, "radio", "{on}|off"); + + addproperty ("string", hl, "textstring", {}); + + addproperty ("interpreter", hl, "textinterpreter"); + + addproperty ("edgecolor", hl, "color", [.15 .15 .15]); + + addproperty ("textcolor", hl, "color", "k"); - if (! isempty (ll)) - [xpos1, ypos1] = get (ll, {"xdata", "ydata"}){:}; - xpos2 = sum (xpos1) / 2; - ypos2 = ypos1(1); - delete (ll); - if (! isempty (lm)) - delete (lm); - endif - else - [xpos2, ypos2] = get (lm, {"xdata", "ydata"}){:}; - xpos1 = xpos2 + [-0.5, 0.5] * linelength; - ypos1 = [ypos2, ypos2]; - delete (lm); - endif + addproperty ("textposition", hl, "radio", "left|{right}"); + + addproperty ("itemhitfcn", hl, "axesbuttondownfcn"); + +endfunction + +function pos = get_position_points (h) + + units = get (h, "units"); + unwind_protect + set (h, "units", "points"); + pos = get (h, "position"); + unwind_protect_cleanup + set (h, "units", units); + end_unwind_protect - if (! strcmp (linestyle, "none")) - hl = __go_line__ (hlegend, "xdata", xpos1, "ydata", ypos1, - "color", get (h, "color"), - "linestyle", get (h, "linestyle"), - "linewidth", min (get (h, "linewidth"), 5), - "marker", "none"); - setappdata (hl, "handle", h); +endfunction + +function maybe_update_layout_cb (h, d, hl) + + if (isaxes (h)) + pos = get_position_points (h); + old_pos = getappdata (hl, "__peer_axes_position__"); + if (! all (pos == old_pos)) + update_layout_cb (hl); + setappdata (hl, "__peer_axes_position__", pos); endif - if (! strcmp (marker, "none")) - hl = __go_line__ (hlegend, "xdata", xpos2, "ydata", ypos2, ... - "color", get (h, "color"), ... - "marker", marker, "markeredgecolor", get (h, "markeredgecolor"), ... - "markerfacecolor", get (h, "markerfacecolor"), ... - "markersize", min (get (h, "markersize"), 10), ... - "linestyle", "none", ... - "linewidth", min (get (h, "linewidth"), 5)); - setappdata (hl, "handle", h); + elseif (isfigure (h)) + pos = get_position_points (h)(3:4); + old_pos = getappdata (hl, "__peer_figure_position__"); + if (isempty (old_pos) || ! all (pos == old_pos)) + update_layout_cb (hl); + setappdata (hl, "__peer_figure_position__", pos); endif endif endfunction +function update_numchild_cb (hl) + + if (strcmp (get (hl, "autoupdate"), "on")) + + hax = getappdata (hl, "__axes_handle__"); + kids = get (hax, "children"); + if (iscell (kids)) + nkids = numel (cell2mat (get (hax, "children"))); + else + nkids = numel (get (hax, "children")); + endif + + setappdata (hl, "__total_num_children__", nkids); + + endif + +endfunction + +function legend_autoupdate_cb (hax, d, hl) + + ## Get all current children including eventual peer plotyy axes + ## children + try + hax = get (hax, "__plotyy_axes__"); + kids = cell2mat (get (hax, "children")); + catch + kids = get (hax, "children"); + end_try_catch + + is_deletion = getappdata (hl, "__total_num_children__") > numel (kids); + setappdata (hl, "__total_num_children__", numel (kids)); + + ## Remove item for deleted object + current_obj = getappdata (hl, "__peer_objects__"); + [~, iold, inew] = setxor (current_obj, kids); + kids = kids(inew); + current_obj(iold) = []; + + if (isempty (current_obj)) + delete (hl); + return; + endif + + if (! is_deletion && strcmp (get (hl, "autoupdate"), "on")) + + ## Add item for the latest created object + persistent valid_types = {"line", "patch", "surface", "hggroup"}; + valid = arrayfun (@(h) any (strcmp (get (h, "type"), valid_types)), kids); + kids(! valid) = []; + + ## FIXME: if the latest child is an hggroup, we cannot label it + ## since this function is called before the hggroup has been properly + ## populated + if (numel (kids) > 0 && strcmp (get (kids(1), "type"), "hggroup")) + kids = []; + elseif (numel (kids) > 1) + kids = kids(1); + endif + + else + kids = []; + endif + + if (any (iold) || any (kids)) + setappdata (hl, "__peer_objects__", [current_obj; kids]); + set (hl, "string", displayname_or_default ([current_obj; kids], hl)); + endif + +endfunction + +function opts = parse_opts (varargin) + + action = ""; + legend_handle = []; + axes_handles = []; + obj_handles = []; + obj_labels = {}; + + nargs = numel (varargin); + + ## Find peer axes + if (nargs > 0 + && (! ishghandle (varargin{1}) + || (strcmp (get (varargin{1}, "type"), "axes") + && ! strcmp (get (varargin{1}, "tag"), "legend")))) + [axes_handles, varargin, nargs] = __plt_get_axis_arg__ ("legend", ... + varargin{:}); + if (isempty (axes_handles)) + axes_handles = gca (); + endif + else + axes_handles = gca (); + endif + + ## Special handling for plotyy which has two axes objects + if (isprop (axes_handles, "__plotyy_axes__")) + axes_handles = [axes_handles get(axes_handles, "__plotyy_axes__").']; + ## Remove duplicates while preserving order + [~, n] = unique (axes_handles, "first"); + axes_handles = axes_handles(sort (n)); + endif + + ## Find any existing legend object associated with axes + try + legend_handle = get (axes_handles, "__legend_handle__"); + if (iscell (legend_handle)) + legend_handle = unique (cell2mat (legend_handle)); + endif + catch + end_try_catch + + ## Legend actions + actions = {"show", "hide", "toggle", "boxon", ... + "boxoff", "right", "left", "off"}; + if (nargs > 0 && ischar (varargin{1}) + && any (strcmp (varargin{1}, actions))) + action = varargin{1}; + if (nargs > 1) + warning ("Octave:legend:ignoring-extra-argument", ... + "legend: ignoring extra arguments after \"%s\"", action); + endif + nargs = 0; + endif + + ## Now remove property-value pairs for compatibility. + propval = {}; + warn_propval = ""; + persistent legend_props = {"location", "orientation", "numcolumns", ... + "numcolumnsmode", "textposition", ... + "position", "units", "autoupdate", ... + "string", "title", "interpreter", ... + "fontname", "fontsize", "fontweight", ... + "fontangle", "textcolor", "color", ... + "edgecolor", "box", "linewidth", ... + "visible", "uicontextmenu", "selected", ... + "selectionhighlight", "itemhitfcn", ... + "buttondownfcn", "createfcn", "deletefcn" ... + "interruptible", "busyaction", ... + "pickableparts", "hittest", ... + "beingdeleted", "parent", "children", ... + "handlevisibility", "tag", "type", ... + "userdata"}; + isprp = @(prop) (ischar (prop) && any (strcmpi (legend_props, prop))); + idx = find (cellfun (isprp, varargin)); + if (! isempty (idx)) + idx = idx(1); + propval = varargin(idx:end); + warn_propval = varargin{idx}; + varargin(idx:end) = []; + nargs = idx-1; + endif + + ## List plot objects that can be handled + warn_extra_obj = false; + persistent valid_types = {"line", "patch", "surface", "hggroup"}; + + if (nargs > 0 && all (ishghandle (varargin{1}))) + + ## List of plot objects to label given as first argument + obj_handles = varargin{1}; + types = get (obj_handles, "type"); + if (! iscell (types)) + types = {types}; + endif + + idx = cellfun (@(s) any (strcmp (s, valid_types)), types); + if (! all (idx)) + error ("Octave:legend:bad-object", ... + "legend: objects of type \"%s\" can't be labeled", ... + types(! idx){1}); + endif + varargin(1) = []; + nargs--; + warn_extra_obj = true; + + elseif (isempty (legend_handle)) + + ## Find list of plot objects from axes "children" + if (isscalar (axes_handles)) + obj_handles = flipud (get (axes_handles, "children")(:)); + else + obj_handles = vertcat (flipud (get (flipud (axes_handles(:)), ... + "children")){:}); + endif + + if (isempty (obj_handles)) + error ("Octave:legend:no-object", "legend: no valid object to label"); + endif + + idx = arrayfun (@(h) any (strcmp (get (h, "type"), valid_types)), ... + obj_handles); + obj_handles(! idx) = []; + + if (isempty (obj_handles)) + error ("Octave:legend:no-object", "legend: no valid object to label"); + endif + + else + obj_handles = getappdata (legend_handle, "__peer_objects__"); + endif + + nobj = numel (obj_handles); + + ## List labels + next_idx = 1; + if (nargs > 0) + + if (iscellstr (varargin{1})) + obj_labels = varargin{1}; + varargin(1) = []; + nargs--; + elseif (ischar (varargin{1}) && ! isvector (varargin{1})) + obj_labels = cellstr (varargin{1}); + varargin(1) = []; + nargs--; + elseif (all (cellfun (@ischar, varargin))) + obj_labels = varargin; + varargin = {}; + nargs = 0; + endif + + if (nargs > 0) + print_usage ("legend"); + endif + + nlab = numel (obj_labels); + if (nlab != nobj) + if (nobj > nlab) + obj_handles = obj_handles(1:nlab); + + msg = "legend: ignoring extra objects."; + if (! isempty (warn_propval)) + msg = [msg " \"" warn_propval "\" interpreted as a property " , ... + "name. Use a cell array of strings to specify labels ", ... + "that match a legend property name."]; + endif + if (warn_extra_obj) + warning ("Octave:legend:object-label-mismatch", msg); + endif + else + obj_labels = obj_labels(1:nobj); + warning ("Octave:legend:object-label-mismatch", ... + "legend: ignoring extra labels."); + endif + endif + else + [obj_labels, next_idx] = displayname_or_default (obj_handles); + endif + + opts.action = action; + opts.axes_handles = axes_handles; + opts.obj_handles = obj_handles; + opts.obj_labels = obj_labels; + opts.legend_handle = legend_handle; + opts.propval = propval; + opts.next_idx = next_idx; + +endfunction + +function [labels, next_idx] = displayname_or_default (hplots, hl = []) + + next_idx = 1; + if (! isempty (hl)) + next_idx = getappdata (hl, "__next_label_index__"); + endif + + labels = get (hplots, "displayname"); + if (! iscell (labels)) + labels = {labels}; + endif + + idx = cellfun (@isempty, labels); + if (any (idx)) + default = arrayfun (@(ii) sprintf ("data%d", ii), ... + [next_idx:(next_idx + sum (idx) - 1)], ... + "uniformoutput", false)(:); + labels(idx) = default; + endif + + next_idx += sum (idx); + + if (! isempty (hl)) + setappdata (hl, "__next_label_index__", next_idx); + endif + +endfunction + +function update_layout_cb (hl, ~, update_item = false) + updating = getappdata (hl, "__updating_layout__"); + if (! isempty (updating) && updating) + return; + endif + + setappdata(hl, "__updating_layout__", true); + + ## Scale limits so that item positions are expressed in points, from + ## top to bottom and from left to right or reverse depending on textposition + units = get (hl, "units"); + set (hl, "units", "points"); + + unwind_protect + + if (update_item) + pos = get (hl, "position")(3:4); + set (hl, "xlim", [0 pos(1)], "ylim", [0 pos(2)]); + + textright = strcmp (get (hl, "textposition"), "right"); + set (hl, "ydir", "reverse", ... + "xdir", ifelse (textright, "normal", "reverse")); + + ## Create or reuse text and item graphics objects + objlist = textitem_objects (hl, textright); + nitem = rows (objlist); + + ## Prepare the array of text/item pairs and update their position + [sz, txtdata, itemdata] = textitem_data (hl, objlist); + for ii = 1:nitem + set (objlist(ii,1), "position", txtdata(ii,:)); + if (strcmp (get (objlist(ii,2), "type"), "line")) + set (objlist(ii,2), "xdata", itemdata(ii,1:2), ... + "ydata", itemdata(ii,3:4)); + else + set (objlist(ii,2), "xdata", [itemdata(ii,1:2) itemdata(ii,[2 1])], + "ydata", [itemdata(ii,3), itemdata(ii,3), ... + itemdata(ii,4), itemdata(ii,4)]); + endif + endfor + else + sz = [diff(get (hl, "xlim")) diff(get (hl, "ylim"))]; + endif + + ## Place the legend + update_legend_position (hl, sz) + + unwind_protect_cleanup + set (hl, "units", units); + setappdata(hl, "__updating_layout__", false); + end_unwind_protect + +endfunction + +function objlist = textitem_objects (hl, textright) + + ## Delete or set invisible obsolete or unused text/item objects. + old_kids = get (hl, "children")(:).'; + old_peer_objects = cell2mat (get (old_kids, "peer_object"))(:).'; + unused = ! ishghandle (old_peer_objects); + delete (old_kids(unused)); + old_kids(unused) = []; + old_peer_objects(unused) = []; + + new_peer_objects = getappdata (hl, "__peer_objects__")(:).'; + + unused = arrayfun (@(h) ! any (h == new_peer_objects), ... + old_peer_objects); + set (old_kids(unused), "visible", "off"); + + ## Text properties + string = get (hl , "string"); + if (! iscell (string)) + string = {string}; + endif + + txtprops = {"textcolor", "fontsize", "fontweight", "fontname", ... + "interpreter"}; + txtvals = get (hl, txtprops); + txtprops{1} = "color"; + txtprops = [txtprops, "horizontalalignment"]; + txtvals = [txtvals, ifelse(textright, "left", "right")]; + + ## Create or reuse text/item objects as needed + nitem = numel (new_peer_objects); + objlist = nan (nitem, 2); + + for ii = 1:nitem + + str = string{ii}; + hplt = new_peer_objects(ii); + + idx = (old_peer_objects == hplt); + typ = get (hplt, "type"); + + if (any (idx)) + tmp = old_kids(idx); + idx = strcmp (get (tmp, "type"), "text"); + + htxt = tmp(idx); + hitem = tmp(! idx); + + set (htxt, "visible", "on", "string", str, ... + [txtprops(:)'; txtvals(:)']{:}); + set (hitem, "visible", "on"); + set (hplt, "displayname", str); + + else + ## For hggroups use the first child that can be labeled + base_hplt = hplt; + if (strcmp (typ, "hggroup")) + kids = get (hplt, "children"); + types = get (kids, "type"); + if (! iscell (types)) + types = {types}; + endif + + idx = cellfun (@(s) any (strcmp (s, {"line", "patch", "surface"})), ... + types); + hplt = kids(idx)(1); + typ = types(idx){1}; + endif + + hmarker = []; + + switch (typ) + case "line" + persistent lprops = {"color", "linestyle", "linewidth"}; + persistent mprops = {"color", "marker", "markeredgecolor", ... + "markerfacecolor", "markersize"}; + + ## Main line + vals = get (hplt, lprops); + hitem = __go_line__ (hl, [lprops; vals]{:}); + + ## Additional line for the marker + vals = get (hplt, mprops); + hmarker = __go_line__ (hl, "handlevisibility", "off", ... + "xdata", 0, "ydata", 0, [mprops; vals]{:}); + update_marker_cb (hmarker); + + case {"patch", "surface"} + persistent pprops = {"edgecolor", "facecolor", "cdata", ... + "linestyle", "linewidth", ... + "marker", "markeredgecolor", ... + "markerfacecolor", "markersize"}; + + vals = get (hplt, pprops); + + hitem = __go_patch__ (hl, [pprops; vals]{:}); + + endswitch + + htxt = __go_text__ (hl, "string", str, ... + [txtprops(:)'; txtvals(:)']{:}); + set (base_hplt, "displayname", str); + + addproperty ("peer_object", htxt, "double", base_hplt); + addproperty ("peer_object", hitem, "double", base_hplt); + + if (isempty (hmarker)) + setappdata (hplt, "__item_link__", + linkprop ([hplt hitem], pprops)); + else + setappdata (hplt, "__item_link__", linkprop ([hplt hitem], lprops)); + setappdata (hplt, "__marker_link__", linkprop ([hplt hmarker], mprops)); + addlistener (hitem, "ydata", ... + @(h) set (hmarker, "ydata", mean (get (h, "ydata")))); + addlistener (hitem, "xdata", ... + @(h) set (hmarker, "xdata", mean (get (h, "xdata")))); + addlistener (hmarker, "markersize", @update_marker_cb); + endif + endif + + objlist(ii,:) = [htxt hitem]; + endfor + +endfunction + +function update_marker_cb (h) + if (get (h, "markersize") > 6) + set (h, "markersize", 6) + endif +endfunction + +function [sz, txtdata, itemdata] = textitem_data (hl, objlist) + + ## margins in points + persistent hmargin = 3; + persistent vmargin = 3; + persistent item_width = 15; + + ext = get (objlist(:,1), "extent"); + markers = get (objlist(:,2), "marker"); + markersz = get (objlist(:,2), "markersize"); + types = get (objlist(:,2), "type"); + + ## Simple case of 1 text/item pair + nitem = rows (objlist); + txtitem = zeros (nitem, 4); + if (nitem == 1) + ext = abs (ext(:,3:4)); + types = {types}; + markers = {markers}; + markersz = {markersz}; + else + ext = abs (cell2mat (ext)(:,3:4)); + endif + + ## Maximum allowable size for the legend + hax = getappdata (hl, "__axes_handle__")(1); + units = get (hax, "units"); + unwind_protect + set (hax, "units", "points"); + max_size = get (hax, "position")(3:4); + unwind_protect_cleanup + set (hax, "units", units); + end_unwind_protect + + location = get (hl, "location"); + outside = strcmp (location(end:-1:end-3), "edis"); + if (! outside) + max_size *= .9; + endif + + autolayout = strcmp (get (hl, "numcolumnsmode"), "auto"); + itemdata = nan (nitem, 4); + txtdata = nan (nitem, 3); + xmax = ymax = 0; + iter = 1; + + if (strcmp (get (hl, "orientation"), "vertical")) + + if (autolayout) + if (any (strcmpi (location, {"north", "northoutside", + "south", "southoutside"}))) + ##FIXME: handle autolayout for those better + nrow = 1; + else + nrow = max (find ((cumsum (ext(:,2) + vmargin) + vmargin) ... + < max_size(2))); + endif + ncol = ceil (nitem / nrow); + else + ncol = min (nitem, get (hl, "numcolumns")); + nrow = ceil (nitem / ncol); + endif + + rowheighs = arrayfun (@(idx) max(ext(idx:nrow:end, 2)), + 1:nrow); + x = hmargin; + for ii = 1:ncol + y = vmargin; + for jj = 1:nrow + if (iter > nitem) + continue + endif + + hg = rowheighs(jj); + dx = 0; + if (! strcmp (markers{iter}, "none")) + dx = markersz{iter}/2; + endif + + y0 = y1 = y + hg/2; + if (! strcmp (types{jj}, "line")) + y0 = y + dx; + y1 = y + hg - dx; + endif + + ## [x0 x1 y0 y1] + itemdata(iter,:) = [x+dx x+item_width-dx y0 y1]; + + ## [x y z] + txtdata(iter,:) = [x+item_width+hmargin y+hg/2 0]; + + xmax = max ([xmax x+item_width+2*hmargin+ext(iter,1)]); + y += (vmargin + hg); + iter++; + endfor + ymax = max ([ymax y]); + x = xmax + 2*hmargin; + endfor + + else + + if (autolayout) + if (any (strcmpi (location, {"north", "northoutside", + "south", "southoutside"}))) + ##FIXME: handle autolayout for those better + ncol = nitem; + else + ncol = max (find ((cumsum (ext(:,1) + 2*hmargin ... + + item_width) + hmargin) ... + < max_size(1))); + endif + else + ncol = min (nitem, get (hl, "numcolumns")); + nrow = ceil (nitem / ncol); + endif + + nrow = ceil (nitem / ncol); + + colwidth = arrayfun (@(idx) max(ext(idx:ncol:end, 1)), + 1:ncol); + y = vmargin; + for ii = 1:nrow + x = hmargin; + for jj = 1:ncol + if (iter > nitem) + continue + endif + + wd = colwidth(jj); + + dx = 0; + if (! strcmp (markers, "none")) + dx = markersz{iter}/2; + endif + + ybase = y + ext(iter,2) / 2; + y0 = y1 = ybase; + if (! strcmp (types{jj}, "line")) + y0 = y + dx; + y1 = y + hg - dx; + endif + + ## [x0 x1 y0 y1] + itemdata(iter,:) = [x+dx x+item_width-dx y0 y1]; + ## [x y z] + txtdata(iter,:) = [x+item_width+hmargin ybase 0]; + + ymax = max ([ymax ybase+ext(iter,2)/2+vmargin]); + x += (3*hmargin + item_width + wd); + iter++; + endfor + xmax = max ([xmax x-hmargin]); + y = ymax + vmargin; + endfor + + endif + + sz = [xmax ymax]; + +endfunction + +function update_legend_position (hl, sz) + + persistent hmargin = 6; + persistent vmargin = 6; + + location = get (hl, "location"); + outside = strcmp (location(end:-1:end-3), "edis"); + if (outside) + location = location(1:end-7); + endif + + if (strcmp (location, "best")) + orientation = get (hl, "orientation"); + if (outside && strcmp (orientation, "vertical")) + location = "northeast"; + elseif (outside) + location = "south"; + else + ## FIXME: implement this properly + location = "northeast"; + endif + endif + + haxes = getappdata (hl, "__axes_handle__"); + hax = haxes(end); + units = get (hax, "units"); + + unwind_protect + ## Restore the original looseinset first and set units to points. + li = getappdata (hl, "__original_looseinset__"); + if (isempty (li)) + li = get (hax, "looseinset"); + setappdata (hl, "__original_looseinset__", li); + setappdata (hl, "__original_units__", units); + endif + + set (hax, "units", getappdata (hl, "__original_units__"), ... + "looseinset", li, "units", "points"); + + [li, axpos] = get (hax, {"looseinset", "position"}){:}; + lpos = [get(hl, "position")(1:2) sz]; + + if (! outside) + + switch (location) + case "southwest" + lpos(1) = axpos(1) + hmargin; + lpos(2) = axpos(2) + vmargin; + case "west" + lpos(1) = axpos(1) + hmargin; + lpos(2) = axpos(2) + axpos(4)/2 - lpos(4)/2; + case "northwest" + lpos(1) = axpos(1) + hmargin; + lpos(2) = axpos(2) + axpos(4) - lpos(4) - vmargin; + case "north" + lpos(1) = axpos(1) + axpos(3)/2 - lpos(3)/2; + lpos(2) = axpos(2) + axpos(4) - lpos(4) - vmargin; + case "northeast" + lpos(1) = axpos(1) + axpos(3) - lpos(3) - hmargin; + lpos(2) = axpos(2) + axpos(4) - lpos(4) - vmargin; + case "east" + lpos(1) = axpos(1) + axpos(3) - lpos(3) - hmargin; + lpos(2) = axpos(2) + axpos(4)/2 - lpos(4)/2; + case "southeast" + lpos(1) = axpos(1) + axpos(3) - lpos(3) - hmargin; + lpos(2) = axpos(2) + vmargin; + case "south" + lpos(1) = axpos(1) + axpos(3)/2 - lpos(3)/2; + lpos(2) = axpos(2) + vmargin; + endswitch + + else + + ti = get (haxes, "tightinset"); + if (iscell (ti)) + ti = max (cell2mat (ti)); + endif + + switch (location) + case "southwest" + dx = lpos(3) + hmargin + ti(1); + if (axpos(1) < (dx + hmargin)) + li(1) = dx + hmargin; + set (hax, "looseinset", li); + axpos = get (hax, "position"); + endif + lpos(1) = axpos(1) - dx; + lpos(2) = axpos(2); + case "west" + dx = lpos(3) + hmargin + ti(1); + if (axpos(1) < (dx + hmargin)) + li(1) = dx + hmargin; + set (hax, "looseinset", li); + axpos = get (hax, "position"); + endif + lpos(1) = axpos(1) - dx; + lpos(2) = axpos(2) + axpos(4)/2 - lpos(4)/2; + case "northwest" + dx = lpos(3) + hmargin + ti(1); + if (axpos(1) < (dx + hmargin)) + li(1) = dx + hmargin; + set (hax, "looseinset", li); + axpos = get (hax, "position"); + endif + lpos(1) = axpos(1) - dx; + lpos(2) = axpos(2) + axpos(4) - lpos(4); + case "north" + dy = lpos(4) + vmargin + ti(4); + if (li(4) < (dy + vmargin)) + li(4) = dy + vmargin; + set (hax, "looseinset", li); + axpos = get (hax, "position"); + endif + lpos(1) = axpos(1) + axpos(3)/2 - lpos(3)/2; + lpos(2) = axpos(2) + axpos(4) + vmargin + ti(4); + case "northeast" + dx = lpos(3) + hmargin + ti(3); + if (li(3) < (dx + hmargin)) + li(3) = dx + hmargin; + set (hax, "looseinset", li); + axpos = get (hax, "position"); + endif + lpos(1) = axpos(1) + axpos(3) + hmargin + ti(3); + lpos(2) = axpos(2) + axpos(4) - lpos(4); + case "east" + dx = lpos(3) + hmargin + ti(3); + if (li(3) < (dx + hmargin)) + li(3) = dx + hmargin; + set (hax, "looseinset", li); + axpos = get (hax, "position"); + endif + lpos(1) = axpos(1) + axpos(3) + hmargin + ti(3); + lpos(2) = axpos(2) + axpos(4)/2 - lpos(4)/2; + case "southeast" + dx = lpos(3) + hmargin + ti(3); + if (li(3) < (dx + hmargin)) + li(3) = dx + hmargin; + set (hax, "looseinset", li); + axpos = get (hax, "position"); + endif + lpos(1) = axpos(1) + axpos(3) + hmargin + ti(3); + lpos(2) = axpos(2); + case "south" + dy = lpos(4) + vmargin + ti(2); + if (li(2) < (dy + vmargin)) + li(2) = dy + vmargin; + set (hax, "looseinset", li); + axpos = get (hax, "position"); + endif + lpos(1) = axpos(1) + axpos(3)/2 - lpos(3)/2; + lpos(2) = axpos(2) - lpos(4) - vmargin - ti(2); + endswitch + endif + + set (hl, "xlim", [0 sz(1)], "ylim", [0 sz(2)], ... + "position", lpos); + + setappdata (hl, "__peer_axes_position__", axpos); + + unwind_protect_cleanup + set (hax, "units", units); + end_unwind_protect + +endfunction %!demo %! clf; %! plot (rand (2)); %! title ("legend called with string inputs for labels"); %! h = legend ("foo", "bar"); -%! legend (h, "location", "northeastoutside"); -%! set (h, "fontsize", 20); +%! set (h, "fontsize", 20, "location", "northeastoutside"); %!demo %! clf; %! plot (rand (2)); %! title ("legend called with cell array of strings"); %! h = legend ({"cellfoo", "cellbar"}); -%! legend (h, "location", "northeast"); -%! set (h, "fontsize", 20); +%! set (h, "fontsize", 20, "location", "northeast"); %!demo %! clf; @@ -1393,10 +1291,11 @@ %!demo %! clf; %! x = 0:1; -%! plot (x,x,";I am Blue;", x,2*x, x,3*x,";I am yellow;"); -%! h = legend ("location", "northeastoutside"); +%! hline = plot (x,x,";I am Blue;", x,2*x, x,3*x,";I am yellow;"); +%! h = legend (); +%! set (h, "location", "northeastoutside"); %! ## Placing legend inside returns axes to original size -%! legend (h, "location", "northeast"); +%! set (h, "location", "northeast"); %! title ("Blue and Yellow keys, with Orange missing"); %!demo @@ -1603,7 +1502,7 @@ %! clf; %! plot (rand (2)); %! title ("legend() will warn if extra labels are specified"); -%! legend ("Hello", "World", "interpreter", "foobar"); +%! legend ("Hello", "World", "foo", "bar"); %!demo %! clf; @@ -1645,47 +1544,38 @@ %!demo # bug 36408 %! clf; -%! option = "right"; %! subplot (3,1,1); %! plot (rand (1,4)); %! xlabel xlabel; %! ylabel ylabel; %! title ("Subplots adjust to the legend placed outside"); %! legend ({"1"}, "location", "northeastoutside"); -%! legend (option); %! subplot (3,1,2); %! plot (rand (1,4)); %! xlabel xlabel; %! ylabel ylabel; %! legend ({"1234567890"}, "location", "eastoutside"); -%! legend (option); %! subplot (3,1,3); %! plot (rand (1,4)); %! xlabel xlabel; %! ylabel ylabel; %! legend ({"12345678901234567890"}, "location", "southeastoutside"); -%! legend (option); %!demo # bug 36408 %! clf; -%! option = "right"; %! subplot (3,1,1); %! plot (rand (1,4)); %! title ("Subplots adjust to the legend placed outside"); %! legend ({"1"}, "location", "northwestoutside"); -%! legend (option); %! subplot (3,1,2); %! plot (rand (1,4)); %! legend ({"1234567890"}, "location", "westoutside"); -%! legend (option); %! subplot (3,1,3); %! plot (rand (1,4)); %! legend ({"12345678901234567890"}, "location", "southwestoutside"); -%! legend (option); %!demo # bug 36408 %! clf; -%! option = "right"; %! subplot (3,1,1); %! plot (rand (1,4)); %! set (gca (), "yaxislocation", "right"); @@ -1693,25 +1583,21 @@ %! ylabel ("ylabel"); %! title ("Subplots adjust to the legend placed outside"); %! legend ({"1"}, "location", "northeastoutside"); -%! legend (option); %! subplot (3,1,2); %! plot (rand (1,4)); %! set (gca (), "yaxislocation", "right"); %! xlabel ("xlabel"); %! ylabel ("ylabel"); %! legend ({"1234567890"}, "location", "eastoutside"); -%! legend (option); %! subplot (3,1,3); %! plot (rand (1,4)); %! set (gca (), "yaxislocation", "right"); %! xlabel ("xlabel"); %! ylabel ("ylabel"); %! legend ({"12345678901234567890"}, "location", "southeastoutside"); -%! legend (option); %!demo # bug 36408 %! clf; -%! option = "right"; %! subplot (3,1,1); %! plot (rand (1,4)); %! set (gca (), "yaxislocation", "right"); @@ -1719,25 +1605,21 @@ %! ylabel ("ylabel"); %! title ("Subplots adjust to the legend placed outside"); %! legend ({"1"}, "location", "northwestoutside"); -%! legend (option); %! subplot (3,1,2); %! plot (rand (1,4)); %! set (gca (), "yaxislocation", "right"); %! xlabel ("xlabel"); %! ylabel ("ylabel"); %! legend ({"1234567890"}, "location", "westoutside"); -%! legend (option); %! subplot (3,1,3); %! plot (rand (1,4)); %! set (gca (), "yaxislocation", "right"); %! xlabel ("xlabel"); %! ylabel ("ylabel"); %! legend ({"12345678901234567890"}, "location", "southwestoutside"); -%! legend (option); %!demo # bug 36408; %! clf; -%! option = "right"; %! subplot (3,1,1); %! plot (rand (1,4)); %! set (gca (), "xaxislocation", "top"); @@ -1745,103 +1627,140 @@ %! ylabel ("ylabel"); %! title ("Subplots adjust to the legend placed outside"); %! legend ({"1"}, "location", "northwestoutside"); -%! legend (option); %! subplot (3,1,2); %! plot (rand (1,4)); %! set (gca (), "xaxislocation", "top"); %! xlabel ("xlabel"); %! ylabel ("ylabel"); %! legend ({"1234567890"}, "location", "westoutside"); -%! legend (option); %! subplot (3,1,3); %! plot (rand (1,4)); %! set (gca (), "xaxislocation", "top"); %! xlabel ("xlabel"); %! ylabel ("ylabel"); %! legend ({"12345678901234567890"}, "location", "southwestoutside"); -%! legend (option); -%!demo # bug 39697 +%!demo # bug 36408; %! clf; %! plot (1:10); %! legend ("Legend Text"); %! title ({"Multi-line", "titles", "are a", "problem", "See bug #39697"}); -%!testif ; any (strcmp ("gnuplot", available_graphics_toolkits ())) -%! toolkit = graphics_toolkit ("gnuplot"); -%! h = figure ("visible", "off"); +## Test input validation +%!test +%! hf = figure ("visible", "off"); %! unwind_protect -%! position = get (h, "position"); -%! plot (rand (3)); -%! legend (); -%! filename = sprintf ("%s.eps", tempname ()); -%! print (filename); -%! unlink (filename); -%! assert (get (h, "position"), position); +%! try +%! legend (); +%! catch +%! [~, id] = lasterr (); +%! assert (id, "Octave:legend:no-object"); +%! end_try_catch %! unwind_protect_cleanup -%! close (h); -%! graphics_toolkit (toolkit); -%! end_unwind_protect - -%!test <*42035> -%! h = figure ("visible", "off"); -%! unwind_protect -%! hax1 = subplot (1,2,1); -%! plot (1:10); -%! hax2 = subplot (1,2,2); -%! plot (1:10); -%! hleg1 = legend (hax1, "foo"); -%! assert (getappdata (hleg1, "__axes_handle__"), hax1); -%! assert (gca (), hax2); -%! hleg2 = legend ("bar"); -%! assert (getappdata (hleg2, "__axes_handle__"), gca ()); -%! unwind_protect_cleanup -%! close (h); +%! close (hf); %! end_unwind_protect %!test -%! ## Difficult example from plotyy demo #1 +%! hf = figure ("visible", "off"); +%! unwind_protect +%! axes (); +%! try +%! legend (); +%! catch +%! [~, id] = lasterr (); +%! assert (id, "Octave:legend:no-object"); +%! end_try_catch +%! unwind_protect_cleanup +%! close (hf); +%! end_unwind_protect + +%!test %! hf = figure ("visible", "off"); %! unwind_protect -%! x = 0:0.1:2*pi; -%! y1 = sin (x); -%! y2 = exp (x - 1); -%! hax = plotyy (x,y1, x-1,y2, @plot, @semilogy); -%! text (0.5, 0.5, "Left Axis", "parent", hax(1)); -%! text (4.5, 80, "Right Axis", "parent", hax(2)); -%! hleg = legend ("show"); -%! assert (get (hleg, "string"), {"data1", "data2"}); -%! fail ("legend ('foo', 'bar', 'baz')", "warning", "ignoring extra labels"); +%! axes (); +%! light (); +%! try +%! legend (); +%! catch +%! [~, id] = lasterr (); +%! assert (id, "Octave:legend:no-object"); +%! end_try_catch +%! unwind_protect_cleanup +%! close (hf); +%! end_unwind_protect + +%!test +%! hf = figure ("visible", "off"); +%! unwind_protect +%! axes (); +%! hli = light (); +%! try +%! legend (hli); +%! catch +%! [~, id] = lasterr (); +%! assert (id, "Octave:legend:bad-object"); +%! end_try_catch %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test -%! ## Test warnings about objects to label %! hf = figure ("visible", "off"); %! unwind_protect -%! hax = gca (); -%! fail ("legend ('foobar')", "warning", "plot data is empty"); -%! ht = text (0.5, 0.5, "Hello World"); -%! fail ("legend ('foobar')", "warning", "plot data is empty"); -%! lastwarn (""); # clear warning -%! hleg = legend (); -%! assert (isempty (hleg) && isempty (lastwarn ())); -%! fail ("legend ('foobar')", "warning", "plot data is empty"); -%! hln = line ([0 1], [0 1]); -%! fail ("legend ('foo', 'bar')", "warning", "ignoring extra labels"); -%! plot (rand (2, 21)); -%! fail ("legend ()", "warning", "labeling only first 20 data objects"); +%! axes (); +%! hplot = plot (rand (3)); +%! try +%! legend (hplot, struct); +%! catch +%! [~, id] = lasterr (); +%! assert (id, "Octave:invalid-fun-call"); +%! end_try_catch %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect %!test -%! ## Test warnings about unsupported features %! hf = figure ("visible", "off"); %! unwind_protect -%! plot (1:10); -%! fail ("legend ('location','best')", "warning", "'best' not yet implemented"); +%! axes (); +%! hplot = plot (rand (3)); +%! try +%! legend ("a", "b", "c", hplot); +%! catch +%! [~, id] = lasterr (); +%! assert (id, "Octave:invalid-fun-call"); +%! end_try_catch %! unwind_protect_cleanup %! close (hf); %! end_unwind_protect + +## Test bugs in previous implementation +%!test <39697> +%! hf = figure ("visible", "off"); +%! unwind_protect +%! axes ("units", "normalized"); +%! plot (1:10); +%! hl = legend ("Legend Text", "units", "normalized"); +%! title ({'Multi-line', 'titles', 'are a', 'problem'}); +%! pos = get (gca, 'position'); +%! axtop = sum (pos(2:2:4)); +%! pos = get (hl, 'position'); +%! legtop = sum (pos(2:2:4)); +%! assert (legtop < axtop); +%! unwind_protect_cleanup +%! close (hf); +%! end_unwind_protect + +%!test <40333> +%! hf = figure ("visible", "off"); +%! unwind_protect +%! axes ("units", "normalized"); +%! plot (1:10); +%! hl = legend ("Legend Text", "units", "normalized"); +%! pos = get (gca, 'position'); +%! set (hf, 'position', [0 0 200 200]); +%! set (hl, 'fontsize', 20); +%! assert (get (gca, 'position'), pos, 2*eps); +%! unwind_protect_cleanup +%! close (hf); +%! end_unwind_protect
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scripts/plot/appearance/private/__gnuplot_legend__.m Wed Nov 13 09:53:07 2019 +0100 @@ -0,0 +1,1847 @@ +## Copyright (C) 2010-2019 David Bateman +## +## 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 {} {} legend () +## @deftypefnx {} {} legend (@var{str1}, @var{str2}, @dots{}) +## @deftypefnx {} {} legend (@var{charmat}) +## @deftypefnx {} {} legend (@{@var{cellstr}@}) +## @deftypefnx {} {} legend (@dots{}, "location", @var{pos}) +## @deftypefnx {} {} legend (@dots{}, "orientation", @var{orient}) +## @deftypefnx {} {} legend (@var{hax}, @dots{}) +## @deftypefnx {} {} legend (@var{hobjs}, @dots{}) +## @deftypefnx {} {} legend (@var{hax}, @var{hobjs}, @dots{}) +## @deftypefnx {} {} legend ("@var{option}") +## @deftypefnx {} {} legend (@dots{}, @{@var{cellstr}@}, @var{property}, @var{value}, @dots{}) +## @deftypefnx {} {[@var{hleg}, @var{hleg_obj}, @var{hplot}, @var{labels}] =} legend (@dots{}) +## +## Display a legend for the current axes using the specified strings as labels. +## +## Legend entries may be specified as individual character string arguments, +## a character array, or a cell array of character strings. When label names +## might be confused with options to @code{legend}, the labels should be +## protected by specifying them as a cell array of strings. +## +## If the first argument @var{hax} is an axes handle, then add a legend to this +## axes, rather than the current axes returned by @code{gca}. +## +## Legend labels are associated with the axes' children; The first label is +## assigned to the first object that was plotted in the axes, the second label +## to the next object plotted, etc. To label specific data objects, without +## labeling all objects, provide their graphic handles in the input +## @var{hobjs}. +## +## The optional parameter @var{pos} specifies the location of the legend as +## follows: +## +## @multitable @columnfractions 0.06 0.14 0.80 +## @headitem @tab pos @tab location of the legend +## @item @tab north @tab center top +## @item @tab south @tab center bottom +## @item @tab east @tab right center +## @item @tab west @tab left center +## @item @tab northeast @tab right top (default) +## @item @tab northwest @tab left top +## @item @tab southeast @tab right bottom +## @item @tab southwest @tab left bottom +## @sp 1 +## @item @tab outside @tab can be appended to any location string @* +## @item @tab @tab which will place the legend outside the axes +## @end multitable +## +## The optional parameter @var{orient} determines if the legend elements are +## placed vertically or horizontally. The allowed values are +## @qcode{"vertical"} (default) or @qcode{"horizontal"}. +## +## The following customizations are available using @var{option}: +## +## @table @asis +## @item @qcode{"show"} +## Show legend on the plot +## +## @item @qcode{"hide"} +## Hide legend on the plot +## +## @item @qcode{"toggle"} +## Toggle between @qcode{"hide"} and @qcode{"show"} +## +## @item @qcode{"boxon"} +## Show a box around legend (default) +## +## @item @qcode{"boxoff"} +## Hide the box around legend +## +## @item @qcode{"right"} +## Place label text to the right of the keys (default) +## +## @item @qcode{"left"} +## Place label text to the left of the keys +## +## @item @qcode{"off"} +## Delete the legend object +## @end table +## +## The @code{legend} function creates a graphics object which has various +## properties that can be manipulated with @code{get}/@code{set}. +## Alternatively, properties can be set directly when calling @code{legend} by +## including @var{property}/@var{value} pairs. If using this calling form, the +## labels must be specified as a cell array of strings. +## +## The optional output values are +## +## @table @var +## @item hleg +## The graphics handle of the legend object. +## +## @item hleg_obj +## Graphics handles to the text, patch, and line objects which form the +## legend. +## +## @item hplot +## Graphics handles to the plot objects which were used in making the legend. +## +## @item labels +## A cell array of strings of the labels in the legend. +## @end table +## +## Implementation Note: The legend label text is either provided in the call to +## @code{legend} or is taken from the @code{DisplayName} property of the +## graphics objects. Only data objects, such as line, patch, and surface, have +## this property whereas axes, figures, etc.@: do not so they are never present +## in a legend. If no labels or @code{DisplayName} properties are available, +## then the label text is simply @qcode{"data1"}, @qcode{"data2"}, @dots{}, +## @nospell{@qcode{"dataN"}}. No more than 20 data labels will be +## automatically generated. To label more, call @code{legend} explicitly and +## provide all labels. +## +## The legend @code{FontSize} property is initially set to 90% of the axes +## @code{FontSize} to which it is attached. Use @code{set} to override this +## if necessary. +## +## A legend is implemented as an additional axes object with the @code{tag} +## property set to @qcode{"legend"}. Properties of the legend object may be +## manipulated directly by using @code{set}. +## @end deftypefn + +function [hleg, hleg_obj, hplot, labels] = __gnuplot_legend__ (varargin) + + if (nargin > 0 + && (! ishghandle (varargin{1}) + || (strcmp (get (varargin{1}, "type"), "axes") + && ! strcmp (get (varargin{1}, "tag"), "legend")))) + [ca, varargin, nargin] = __plt_get_axis_arg__ ("legend", varargin{:}); + if (isempty (ca)) + ca = gca (); + endif + hfig = ancestor (ca, "figure"); + else + hfig = get (0, "currentfigure"); + if (isempty (hfig)) + hfig = gcf (); + endif + ca = gca (); + endif + + ## Special handling for plotyy which has two axes objects + if (isprop (ca, "__plotyy_axes__")) + plty = get (ca, "__plotyy_axes__"); + ca = [ca, plty.']; + ## Remove duplicates while preserving order + [~, n] = unique (ca, "first"); + ca = ca(sort (n)); + endif + + if (nargin > 0 && all (ishghandle (varargin{1}))) + ## List of plot objects to label given as first argument + kids = flipud (varargin{1}(:)); + varargin(1) = []; + else + ## Find list of plot objects from axes "children" + kids = ca; + kids(strcmp (get (ca, "tag"), "legend")) = []; + if (isscalar (kids)) + kids = get (kids, "children")(:); + else + kids = vertcat (flipud (get (kids, "children")){:}); + endif + endif + nargs = numel (varargin); + nkids = numel (kids); + + ## Find any existing legend object associated with axes + hlegend = []; + for hax = ca + try + hlegend = get (hax, "__legend_handle__"); + if (! isempty (hlegend)) + break; + endif + end_try_catch + endfor + + orientation = "default"; + location = "default"; + show = "create"; + textpos = "default"; + box = "default"; + delete_leg = false; + find_leg_hdl = (nargs == 0); # possibly overridden + propvals = {}; + + ## Find "location", "orientation", "textposition" property/value pairs + foundpos = foundorient = foundtextpos = false; + i = nargs - 1; + while (i > 0) + pos = varargin{i}; + str = varargin{i+1}; + if (strcmpi (pos, "location") && ischar (str)) + if (! foundpos) + location = lower (str); + foundpos = true; + endif + varargin(i:i+1) = []; + nargs -= 2; + elseif (strcmpi (pos, "orientation") && ischar (str)) + if (! foundorient) + orientation = lower (str); + foundorient = true; + endif + varargin(i:i+1) = []; + nargs -= 2; + elseif (strcmpi (pos, "textposition") && ischar (str)) + if (! foundtextpos) + textpos = lower (str); + foundtextpos = true; + endif + varargin(i:i+1) = []; + nargs -= 2; + endif + i -= 2; + endwhile + + ## Validate the orientation + if (! any (strcmp (orientation, {"vertical", "horizontal", "default"}))) + error ("legend: unrecognized legend orientation"); + endif + + ## Validate the texposition + if (! any (strcmp (textpos, {"left", "right", "default"}))) + error ("legend: unrecognized legend textposition"); + endif + + ## Validate the location type + outside = false; + inout = strfind (location, "outside"); + if (! isempty (inout)) + outside = true; + location = location(1:inout-1); + else + outside = false; + endif + + switch (location) + case {"north", "south", "east", "west", "northeast", "northwest", ... + "southeast", "southwest", "default"} + ## These are all valid locations, do nothing. + + case "best" + if (outside) + if (strcmp (orientation, "horizontal")) + location = "south"; + else + location = "northeast"; + endif + else + warning ("legend: 'best' not yet implemented for location specifier, using 'northeast' instead\n"); + location = "northeast"; + endif + + case "none" + ## FIXME: Should there be any more error checking on this? + + otherwise + error ("legend: unrecognized legend location"); + endswitch + + ## Finish input processing based on number of inputs + if (nargs == 0) + ## No labels given, create a new legend or return existing one + if (isempty (hlegend)) + show = "create"; + textpos = "right"; + find_leg_hdl = false; + endif + + elseif (nargs == 1) + ## Either OPTION value, single string label, or cellstr of labels. + arg = varargin{1}; + if (ischar (arg)) + if (rows (arg) == 1) + str = lower (strtrim (arg)); + switch (str) + case "off" + delete_leg = true; + case "hide" + show = "off"; + nargs -= 1; + case "show" + if (! isempty (hlegend)) + show = "on"; + else + show = "create"; + textpos = "right"; + endif + nargs -= 1; + case "toggle" + if (isempty (hlegend)) + show = "create"; + textpos = "right"; + elseif (strcmp (get (hlegend, "visible"), "off")) + show = "on"; + else + show = "off"; + endif + nargs -= 1; + case "boxon" + box = "on"; + nargs -= 1; + case "boxoff" + box = "off"; + nargs -= 1; + case "left" + textpos = "left"; + nargs -= 1; + case "right" + textpos = "right"; + nargs -= 1; + endswitch + else + ## Character matrix of labels + varargin = cellstr (arg); + nargs = numel (varargin); + endif + elseif (iscellstr (arg)) + ## Cell array of labels + varargin = arg; + nargs = numel (varargin); + else + error ("legend: single argument must be a string or cellstr"); + endif + + elseif (nargs > 1 && iscellstr (varargin{1})) + ## Cell array of labels followed by property/value pairs + propvals = varargin(2:end); + if (rem (numel (propvals), 2) != 0) + error ("legend: PROPERTY/VALUE arguments must occur in pairs"); + endif + varargin = {varargin{1}{:}}; + nargs = numel (varargin); + endif + + have_labels = (nargs > 0); + hobjects = []; + hplots = []; + text_strings = {}; + + if (delete_leg) + delete (hlegend); + hlegend = []; + elseif (find_leg_hdl) + ## Don't change anything about legend. + ## hleg output will be assigned hlegend value at end of function. + elseif (strcmp (show, "off")) + if (! isempty (hlegend)) + set (hlegend, "visible", "off"); + hlegend = []; + endif + elseif (strcmp (show, "on")) + if (! isempty (hlegend)) + set (hlegend, "visible", "on"); + ## NOTE: Matlab sets both "visible" and "box" to "on" for "show on" + ## set (hlegend, "box", "on"); + endif + elseif (strcmp (box, "on")) + if (! isempty (hlegend)) + set (hlegend, "box", "on"); + endif + elseif (strcmp (box, "off")) + if (! isempty (hlegend)) + set (hlegend, "box", "off"); + endif + elseif (! have_labels && ! isempty (hlegend) + && ! (strcmp (location, "default") + && strcmp (orientation, "default"))) + ## Changing location or orientation of existing legend + if (strcmp (location, "default")) + set (hlegend, "orientation", orientation); + elseif (strcmp (orientation, "default")) + if (outside) + set (hlegend, "location", [location "outside"]); + else + set (hlegend, "location", location); + endif + else + if (outside) + set (hlegend, "location", [location "outside"], + "orientation", orientation); + else + set (hlegend, "location", location, + "orientation", orientation); + endif + endif + else + ## Create or modify legend object + + if (! isempty (hlegend)) + ## Disable callbacks while modifying an existing legend + setappdata (hlegend, "nocallbacks", true); + endif + + if (have_labels) + ## Check for valid data that can be labeled. + have_data = false; + have_dname = false; + for hkid = kids.' + typ = get (hkid, "type"); + if (any (strcmp (typ, {"line", "patch", "surface", "hggroup"}))) + have_data = true; + break; + endif + endfor + + if (! have_data) + warning ("legend: plot data is empty; setting key labels has no effect"); + endif + else + ## No labels. Search for DisplayName property. + have_dname = false; + n_dname = 0; + for hkid = kids.' + typ = get (hkid, "type"); + if (any (strcmp (typ, {"line", "patch", "surface", "hggroup"}))) + n_dname += 1; # count of objects which could be labeled + if (! isempty (get (hkid, "displayname"))) + have_dname = true; + break; + endif + endif + endfor + have_data = n_dname > 0; + endif + + if (have_labels || ! have_dname) + k = nkids; + if (! have_labels) + ## No labels or DisplayName. Create set of "dataX" labels. + if (n_dname > 20) + warning ("legend: labeling only first 20 data objects"); + n_dname = 20; + endif + nargs = n_dname; + varargin = arrayfun (@(x) sprintf ("data%d", x), [1:nargs]', + "uniformoutput", false); + have_labels = true; + endif + for i = 1 : nargs + label = varargin{i}; + if (! ischar (label)) + error ("legend: expecting label to be a string"); + endif + ## Locate an object which can be labeled + while (k > 0) + typ = get (kids(k), "type"); + if (any (strcmp (typ, {"line","patch","surface","hggroup"}))) + break; + endif + k--; + endwhile + if (k > 0) + set (kids(k), "displayname", label); + hplots(end+1) = kids(k); + text_strings(end+1) = label; + k--; + else + if (have_data) + warning ("legend: ignoring extra labels"); + endif + break; # k = 0, no further handles to process + endif + endfor + + else + ## No labels specified but objects have DisplayName property set. + k = nkids; + while (k > 0) + ## Locate object to label + while (k > 0) + typ = get (kids(k), "type"); + if (any (strcmp (typ, {"line","patch","surface","hggroup"}))) + break; + endif + k--; + endwhile + if (k > 0) + dname = get (kids(k), "displayname"); + if (! isempty (dname)) + hplots(end+1) = kids(k); + text_strings(end+1) = dname; + endif + k--; + endif + endwhile + endif + + if (isempty (hplots)) + ## Nothing to label + if (! isempty (hlegend)) + delete (hlegend); + hlegend = []; + hobjects = []; + hplots = []; + text_strings = {}; + endif + else + ## Preserve the old legend if it exists + if (! isempty (hlegend)) + if (strcmp (textpos, "default")) + textpos = get (hlegend, "textposition"); + endif + if (strcmp (location, "default")) + location = get (hlegend, "location"); + inout = strfind (location, "outside"); + if (! isempty (inout)) + outside = true; + location = location(1:inout-1); + else + outside = false; + endif + endif + if (strcmp (orientation, "default")) + orientation = get (hlegend, "orientation"); + endif + box = get (hlegend, "box"); + else + if (strcmp (textpos, "default")) + textpos = "right"; + endif + if (strcmp (location, "default")) + location = "northeast"; + endif + if (strcmp (orientation, "default")) + orientation = "vertical"; + endif + box = "on"; + endif + + ## Use axis which is appropriate for legend location. + ## This is only necessary for plotyy figures where there are two axes. + if (numel (ca) == 1) + cax = ca(1); + elseif (strfind (location, "east")) + cax = ca(2); + else + cax = ca(1); + endif + ## Get axis size and fontsize in points. + ## Rely on listener to handle conversion. + units = get (cax, "units"); + unwind_protect + set (cax, "units", "points", "fontunits", "points"); + if (isempty (hlegend) || ! isprop (hlegend, "unmodified_axes_position")) + unmodified_axes_position = get (cax, "position"); + unmodified_axes_outerposition = get (cax, "outerposition"); + else + unmodified_axes_position = get (hlegend, "unmodified_axes_position"); + unmodified_axes_outerposition = get (hlegend, ... + "unmodified_axes_outerposition"); + endif + ca_pos = unmodified_axes_position; + ca_outpos = unmodified_axes_outerposition; + tightinset = get (ca(1), "tightinset"); + for i = 2 : numel (ca) + tightinset = max (tightinset, get (ca(i), "tightinset")); + endfor + unwind_protect_cleanup + set (cax, "units", units); + end_unwind_protect + + ## Padding between legend entries horizontally and vertically + ## measured in points. + ## FIXME: 3*xpad must be integer or strange off-by-1 pixel issues + ## with lines in OpenGL. + xpad = 2 + 1/3; + ypad = 4; + + bpad = 8; # padding of legend box from surrounding axes + + linelength = 15; + + ## Preamble code to restore figure and axes after legend creation + origfig = get (0, "currentfigure"); + if (origfig != hfig) + set (0, "currentfigure", hfig); + else + origfig = []; + endif + origaxes = get (hfig, "currentaxes"); + unwind_protect + ud = ancestor (hplots, "axes"); + if (! isscalar (ud)) + ud = unique ([ud{:}]); + endif + hpar = get (ud(1), "parent"); + + if (isempty (hlegend)) + ## Create a legend object (axes + new properties) + addprops = true; + hlegend = axes ("parent", hpar, "tag", "legend", + "box", box, + "xtick", [], "ytick", [], + "xlim", [0, 1], "ylim", [0, 1], + "activepositionproperty", "position"); + setappdata (hlegend, "__axes_handle__", ud); + try + addproperty ("__legend_handle__", ud(1), "handle", hlegend); + catch + set (ud(1), "__legend_handle__", hlegend); + end_try_catch + + ## Inherit fontsize from current axis + ## "fontunits" should be first because it affects interpretation + ## of "fontsize" property. + [fontunits, fontsz] = get (ca(1), {"fontunits", "fontsize"}){:}; + fontsz *= 0.90; # Reduce legend fontsize to 90% of axes fontsize + set (hlegend, {"fontunits", "fontsize"}, {fontunits, fontsz}); + set (hlegend, "fontunits", "points"); # legend always works in pts. + ## Also inherit colormap from axes if it is different than figure + cax_cmap = get (cax, "colormap"); + if (! isequal (cax_cmap, get (hpar, "colormap"))) + set (hlegend, "colormap", cax_cmap); + endif + old_hplots = []; + else + ## Re-use existing legend. + addprops = false; + axes (hlegend); + delete (get (hlegend, "children")); + ## Hack: get list of hplots for which addlistener has been called. + old_hplots = get (hlegend, "deletefcn"){6}; + endif + + if (addprops) + ## Only required for a newly created legend object + ## FIXME: "autoupdate" is not implemented. + addproperty ("autoupdate", hlegend, "radio", "{on}|off"); + addproperty ("edgecolor", hlegend, "color", [0.15, 0.15, 0.15]); + addproperty ("textcolor", hlegend, "color", [0, 0, 0]); + locations = {"north", "south", "east", "west", ... + "{northeast}", "southeast", "northwest", "southwest", ... + "northoutside", "southoutside", ... + "eastoutside", "westoutside", ... + "northeastoutside", "southeastoutside", ... + "northwestoutside", "southwestoutside", "best", ... + "bestoutside", "none"}; + addproperty ("location", hlegend, "radio", strjoin (locations, "|")); + addproperty ("orientation", hlegend, "radio", + "{vertical}|horizontal"); + addproperty ("string", hlegend, "any", text_strings); + addproperty ("interpreter", hlegend, "textinterpreter"); + addproperty ("textposition", hlegend, "radio", "left|{right}"); + endif + + ## Apply any PROPERTY/VALUE pairs given as arguments + if (! isempty (propvals)) + set (hlegend, propvals{:}); + endif + + ## Special case of PROPERTY "edgecolor" (bug #56968) + ec_idx = find (strcmpi (propvals, "edgecolor"), 1, "last"); + if (! isempty (ec_idx)) + ec_color = propvals{ec_idx + 1}; + set (hlegend, "xcolor", ec_color, "ycolor", ec_color); + endif + + ## Text objects in key inherit visual properties from legend object + legprops = { "fontunits", "fontangle", "fontname", "fontsize", ... + "fontweight", "interpreter", "textcolor" }; + + txtprops = { "fontunits", [], "fontangle", [] "fontname", [], ... + "fontsize", [], "fontweight", [] "interpreter", [], ... + "color", [] }; + propvals = get (hlegend, legprops); + txtprops(2:2:end) = propvals; + + ## Add text labels to the axes first and check their extents + nentries = numel (hplots); + texthandle = []; + maxwidth = maxheight = 0; + for k = 1 : nentries + halign = ifelse (strcmp (textpos, "right"), "left", "right"); + texthandle(k) = text (0, 0, text_strings{k}, + "units", "points", + "horizontalalignment", halign, + txtprops{:}); + setappdata (texthandle(k), "handle", hplots(k)); + extents = get (texthandle(k), "extent"); + maxwidth = max (maxwidth, extents(3)); + maxheight = max (maxheight, extents(4)); + endfor + ## Restore units which were forced to points + set (texthandle, "units", get (0, "DefaultTextUnits")); + + num1 = nentries; + if (strcmp (orientation, "vertical")) + height = nentries * (ypad + maxheight); + if (outside) + if (height > ca_pos(4)) + ## Avoid shrinking the height of the axis to zero if outside + num1 = ca_pos(4) / (maxheight + ypad) / 2; + endif + else + if (height > 0.9 * ca_pos(4)) + num1 = 0.9 * ca_pos(4) / (maxheight + ypad); + endif + endif + else + width = nentries * (ypad + maxwidth); + if (outside) + if (width > ca_pos(3)) + ## Avoid shrinking the width of the axis to zero if outside + num1 = ca_pos(3) / (maxwidth + ypad) / 2; + endif + else + if (width > 0.9 * ca_pos(3)) + num1 = 0.9 * ca_pos(3) / (maxwidth + ypad); + endif + endif + endif + num2 = ceil (nentries / num1); + + ## Layout is [xpad, linelength, xpad, maxwidth, xpad] + xstep = 3 * xpad + (maxwidth + linelength); + if (strcmp (textpos, "right")) + xoffset = xpad; + txoffset = 2 * xpad + linelength; + else + xoffset = 2 * xpad + maxwidth; + txoffset = xpad + maxwidth; + endif + ystep = (ypad + maxheight); + yoffset = ystep / 2; + + ## Place the legend in the desired location + if (strcmp (orientation, "vertical")) + lpos = [0, 0, num2 * xstep, num1 * ystep]; + else + lpos = [0, 0, num1 * xstep, num2 * ystep]; + endif + + gnuplot = strcmp (get (hfig, "__graphics_toolkit__"), "gnuplot"); + if (gnuplot) + ## gnuplot places the key (legend) at edge of the figure window. + ## OpenGL places the legend box at edge of the unmodified axes + ## position. + if (isempty (strfind (location, "east"))) + gnuplot_offset = unmodified_axes_outerposition(1) ... + + unmodified_axes_outerposition(3) ... + - unmodified_axes_position(1) ... + - unmodified_axes_position(3); + else + gnuplot_offset = unmodified_axes_position(1) ... + - unmodified_axes_outerposition(1); + endif + ## FIXME: The "fontsize" is added to match the behavior of OpenGL. + ## This implies that a change in fontsize should trigger a listener + ## to update the legend. The "2" was determined using a long legend + ## key in the absence of any subplots. + gnuplot_offset -= 2 * get (hlegend, "fontsize"); + else + gnuplot_offset = 0; + endif + + ## For legend's outside the associated axes position, + ## align their edge to the unmodified_axes_outerposition, + ## and adjust the axes position accordingly. + switch (location) + case "north" + if (outside) + lpos = [ca_pos(1) + (ca_pos(3) - lpos(3)) / 2, ... + ca_outpos(2) + ca_outpos(4) - lpos(4) - bpad/2, ... + lpos(3), lpos(4)]; + + new_pos = [ca_pos(1), ca_pos(2), ca_pos(3), ca_pos(4) - lpos(4)]; + else + lpos = [ca_pos(1) + (ca_pos(3) - lpos(3)) / 2, ... + ca_pos(2) + ca_pos(4) - lpos(4) - bpad, ... + lpos(3), lpos(4)]; + endif + case "south" + if (outside) + lpos = [ca_pos(1) + (ca_pos(3) - lpos(3)) / 2, ... + ca_outpos(2) + bpad/2, lpos(3), lpos(4)]; + new_pos = [ca_pos(1), ... + lpos(2) + lpos(4) + bpad/2 + tightinset(2), ... + ca_pos(3), ca_pos(4) - lpos(4)]; + else + lpos = [ca_pos(1) + (ca_pos(3) - lpos(3)) / 2, ... + ca_pos(2) + bpad, lpos(3), lpos(4)]; + endif + case "east" + if (outside) + lpos = [ca_outpos(1) + ca_outpos(3) - lpos(3) - bpad/2, ... + ca_pos(2) + (ca_pos(4) - lpos(4)) / 2, ... + lpos(3), lpos(4)]; + new_pos = [ca_pos(1), ca_pos(2), ... + lpos(1) - bpad - tightinset(3) - ca_pos(1), ... + ca_pos(4)]; + new_pos(3) += gnuplot_offset; + else + lpos = [ca_pos(1) + ca_pos(3) - lpos(3) - bpad, ... + ca_pos(2) + (ca_pos(4) - lpos(4)) / 2, lpos(3), lpos(4)]; + endif + case "west" + if (outside) + lpos = [ca_outpos(1) + bpad/2, ... + ca_pos(2) + (ca_pos(4) - lpos(4)) / 2, ... + lpos(3), lpos(4)]; + new_pos = [lpos(1) + lpos(3) + bpad/2 + tightinset(1), ... + ca_pos(2), ca_pos(3) - lpos(3) - bpad/2, ca_pos(4)]; + new_pos([1, 3]) += [-gnuplot_offset, gnuplot_offset]; + else + lpos = [ca_pos(1) + bpad, ... + ca_pos(2) + (ca_pos(4) - lpos(4)) / 2, lpos(3), lpos(4)]; + endif + case "northeast" + if (outside) + lpos = [ca_outpos(1) + ca_outpos(3) - lpos(3) - bpad/2, ... + ca_pos(2) + ca_pos(4) - lpos(4), ... + lpos(3), lpos(4)]; + new_pos = [ca_pos(1), ca_pos(2), ... + lpos(1) - bpad - tightinset(3) - ca_pos(1), ... + ca_pos(4)]; + new_pos(3) += gnuplot_offset; + else + lpos = [ca_pos(1) + ca_pos(3) - lpos(3) - bpad, ... + ca_pos(2) + ca_pos(4) - lpos(4) - bpad, ... + lpos(3), lpos(4)]; + endif + case "northwest" + if (outside) + lpos = [ca_outpos(1) + bpad/2, ... + ca_pos(2) + ca_pos(4) - lpos(4), ... + lpos(3), lpos(4)]; + new_pos = [lpos(1) + lpos(3) + bpad/2 + tightinset(1), ... + ca_pos(2), ca_pos(3) - lpos(3) - bpad/2, ca_pos(4)]; + new_pos([1, 3]) += [-gnuplot_offset, gnuplot_offset]; + else + lpos = [ca_pos(1) + bpad, ... + ca_pos(2) + ca_pos(4) - lpos(4) - bpad, ... + lpos(3), lpos(4)]; + endif + case "southeast" + if (outside) + lpos = [ca_outpos(1) + ca_outpos(3) - lpos(3) - bpad/2, ... + ca_pos(2), lpos(3), lpos(4)]; + new_pos = [ca_pos(1), ca_pos(2), ... + lpos(1) - bpad - ca_pos(1) - tightinset(3), ... + ca_pos(4)]; + new_pos(3) += gnuplot_offset; + else + lpos = [ca_pos(1) + ca_pos(3) - lpos(3) - bpad, ... + ca_pos(2) + bpad, lpos(3), lpos(4)]; + endif + case "southwest" + if (outside) + lpos = [ca_outpos(1) + bpad/2, ca_pos(2), lpos(3), lpos(4)]; + new_pos = [lpos(1) + lpos(3) + bpad/2 + tightinset(1), ... + ca_pos(2), ca_pos(3) - lpos(3) - bpad/2, ca_pos(4)]; + new_pos([1, 3]) += [-gnuplot_offset, gnuplot_offset]; + else + lpos = [ca_pos(1) + bpad, ca_pos(2) + bpad, lpos(3), lpos(4)]; + endif + endswitch + + units = get (hlegend, "units"); + unwind_protect + set (hlegend, "units", "points", "position", lpos); + unwind_protect_cleanup + set (hlegend, "units", units); + end_unwind_protect + + ## Now write the line segments and place the text objects correctly + xk = yk = 0; + for k = 1 : numel (hplots) + hobjects(end+1) = texthandle(k); + hplt = hplots(k); + typ = get (hplt, "type"); + ## For an hggroup, find an underlying primitive object + if (strcmp (typ, "hggroup")) + for hgkid = get (hplt, "children").' + hgkid_type = get (hgkid, "type"); + if (any (strcmp (hgkid_type, {"line","patch","surface"}))) + typ = hgkid_type; + hplt = hgkid; + break; + endif + endfor + endif + + switch (typ) + + case "line" + color = get (hplt, "color"); + style = get (hplt, "linestyle"); + lwidth = min (get (hplt, "linewidth"), 5); + if (! strcmp (style, "none")) + l1 = __go_line__ (hlegend, ... + "xdata", ([xoffset, xoffset + linelength] + xk * xstep) / lpos(3), ... + "ydata", [1, 1] .* (lpos(4) - yoffset - yk * ystep) / lpos(4), ... + "color", color, "linestyle", style, ... + "linewidth", lwidth, "marker", "none"); + setappdata (l1, "handle", hplt); + hobjects(end+1) = l1; + endif + marker = get (hplt, "marker"); + if (! strcmp (marker, "none")) + l1 = __go_line__ (hlegend, ... + "xdata", (xoffset + 0.5 * linelength + xk * xstep) / lpos(3), ... + "ydata", (lpos(4) - yoffset - yk * ystep) / lpos(4), ... + "color", color, "linestyle", "none", ... + "linewidth", lwidth, "marker", marker, ... + "markeredgecolor", get (hplt, "markeredgecolor"), ... + "markerfacecolor", get (hplt, "markerfacecolor"), ... + "markersize", min (get (hplt, "markersize"),10)); + setappdata (l1, "handle", hplt); + hobjects(end+1) = l1; + endif + + ## Newly labeled objects have listeners added + if (! any (hplt == old_hplots)) + addlistener (hplt, "color", + {@cb_line_listener, hlegend, linelength, false}); + addlistener (hplt, "linestyle", + {@cb_line_listener, hlegend, linelength, false}); + addlistener (hplt, "linewidth", + {@cb_line_listener, hlegend, linelength, false}); + addlistener (hplt, "marker", + {@cb_line_listener, hlegend, linelength, false}); + addlistener (hplt, "markeredgecolor", + {@cb_line_listener, hlegend, linelength, false}); + addlistener (hplt, "markerfacecolor", + {@cb_line_listener, hlegend, linelength, false}); + addlistener (hplt, "markersize", + {@cb_line_listener, hlegend, linelength, false}); + addlistener (hplt, "displayname", + {@cb_line_listener, hlegend, linelength, true}); + endif + + case "patch" + facecolor = get (hplt, "facecolor"); + edgecolor = get (hplt, "edgecolor"); + cdata = get (hplt, "cdata"); + if (! strcmp (facecolor, "none") || ! strcmp (edgecolor, "none")) + p1 = patch ("xdata", ([0, linelength, linelength, 0] + + xoffset + xk * xstep) / lpos(3), + "ydata", (lpos(4) - yoffset - + [yk-0.3, yk-0.3, yk+0.3, yk+0.3] .* ystep) / lpos(4), + "facecolor", facecolor, "edgecolor", edgecolor, + "cdata", cdata); + setappdata (p1, "handle", hplt); + else + ## non-standard patch only making use of marker styles + ## such as scatter plot. + p1 = patch ("xdata", (xoffset + 0.5 * linelength + xk * xstep) / lpos(3), + "ydata", (lpos(4) - yoffset - yk * ystep) / lpos(4), + "marker", get (hplt, "marker"), + "markeredgecolor",get (hplt,"markeredgecolor"), + "markerfacecolor",get (hplt,"markerfacecolor"), + "markersize", min (get (hplt,"markersize"),10), + "cdata", cdata); + setappdata (p1, "handle", hplt); + endif + hobjects(end+1) = p1; + ## Copy clim from axes so that colors work out. + set (hlegend, "clim", get (ca(1), "clim")); + + ## FIXME: Need listeners, as for line objects. + ## Changing clim, for example, won't update colors + + case "surface" + facecolor = get (hplt, "facecolor"); + edgecolor = get (hplt, "edgecolor"); + cdata = sum (get (ca(1), "clim")) / 2; + if (! strcmp (facecolor, "none") || ! strcmp (edgecolor, "none")) + p1 = patch ("xdata", ([0, linelength, linelength, 0] + + xoffset + xk * xstep) / lpos(3), + "ydata", (lpos(4) - yoffset - + [yk-0.3, yk-0.3, yk+0.3, yk+0.3] .* ystep) / lpos(4), + "facecolor", facecolor, "edgecolor", edgecolor, + "cdata", cdata); + setappdata (p1, "handle", hplt); + hobjects(end+1) = p1; + endif + ## FIXME: Need listeners, as for line objects. + + endswitch + + set (texthandle(k), "position", + [(txoffset + xk * xstep) / lpos(3), ... + (lpos(4) - yoffset - yk * ystep) / lpos(4)]); + if (strcmp (orientation, "vertical")) + yk += 1; + if (yk > num1) + yk = 0; + xk += 1; + endif + else + xk += 1; + if (xk > num1) + xk = 0; + yk += 1; + endif + endif + endfor + + ## Add an invisible text object to original axis + ## that, when it is destroyed, will remove the legend. + htdel = findall (ca(1), "-depth", 1, "tag", "deletelegend", + "type", "text"); + if (isempty (htdel)) + htdel = text (0, 0, "", "parent", ca(1), "tag", "deletelegend", + "visible", "off", "handlevisibility", "off", + "xliminclude", "off", "yliminclude", "off", + "zliminclude", "off"); + set (htdel, "deletefcn", {@cb_axes_deleted, ca, hlegend}); + endif + if (isprop (hlegend, "unmodified_axes_position")) + set (hlegend, "unmodified_axes_position", + unmodified_axes_position, + "unmodified_axes_outerposition", + unmodified_axes_outerposition); + else + addproperty ("unmodified_axes_position", hlegend, + "data", unmodified_axes_position); + addproperty ("unmodified_axes_outerposition", hlegend, + "data", unmodified_axes_outerposition); + endif + + ## Resize the axis that the legend is attached to if the legend is + ## "outside" the plot and create a listener to resize axis to original + ## size if the legend is deleted, hidden, or shown. + if (outside) + for i = 1 : numel (ca) + units = get (ca(i), "units"); + unwind_protect + set (ca(i), "units", "points"); + if (gnuplot && numel (ca) == 1) + ## Let gnuplot handle the positioning of the keybox. + ## This violates strict Matlab compatibility, but reliably + ## renders an aesthetic result. + set (ca(i), "position", unmodified_axes_position, + "activepositionproperty", "outerposition"); + else + ## numel (ca) > 1 for axes overlays (like plotyy) + set (ca(i), "position", new_pos); + endif + unwind_protect_cleanup + set (ca(i), "units", units); + end_unwind_protect + endfor + + set (hlegend, "deletefcn", {@cb_restore_axes, ca, ... + unmodified_axes_position, ... + unmodified_axes_outerposition, ... + htdel, hplots}); + addlistener (hlegend, "visible", {@cb_legend_hideshow, ca, ... + unmodified_axes_position, ... + new_pos}); + else + set (hlegend, "deletefcn", {@cb_restore_axes, ca, [], [], ... + htdel, hplots}); + endif + + if (! addprops) + ## Remove listeners on existing legend temporarily to stop recursion. + dellistener (hlegend, "location"); + dellistener (hlegend, "orientation"); + dellistener (hlegend, "string"); + dellistener (hlegend, "textposition"); + endif + + if (! addprops) + set (hlegend, "string", text_strings); + endif + + if (outside) + set (hlegend, "location", [location "outside"], + "orientation", orientation, "textposition", textpos); + else + set (hlegend, "location", location, "orientation", orientation, + "textposition", textpos); + endif + + if (addprops) + addlistener (cax, "colormap", {@cb_legend_colormap_update, hlegend}); + addlistener (hlegend, "edgecolor", @cb_legend_text_update); + addlistener (hlegend, "fontangle", @cb_legend_text_update); + addlistener (hlegend, "fontname", @cb_legend_text_update); + addlistener (hlegend, "fontweight", @cb_legend_text_update); + addlistener (hlegend, "textcolor", @cb_legend_text_update); + ## Properties which could change size of box, such as fontsize, + ## require legend to be redrawn. + ## FIXME: fontsize is changed by print.m function during the + ## production of a plot for output. This screws things up + ## because legend tries to return the axes size to what it + ## was when the figure was created, versus what it is now + ## when the figure is being printed. Temporary hack is + ## good enough for generating the Octave manual which still + ## relies on gnuplot for generating images. See bug #40333. + if (! gnuplot) + addlistener (hlegend, "fontsize", @cb_legend_update); + endif + addlistener (hlegend, "fontunits", @cb_legend_update); + addlistener (hlegend, "interpreter", @cb_legend_update); + addlistener (hlegend, "location", @cb_legend_location); + addlistener (hlegend, "orientation", @cb_legend_update); + addlistener (hlegend, "string", @cb_legend_update); + addlistener (hlegend, "textposition", @cb_legend_update); + ## FIXME: need to add listeners for tightinset and position + ## addlistener (ca, "tightinset", @update????); + ## addlistener (ca, "position", @update????); + else + ## Restore listeners temporarily disabled during reconstruction. + addlistener (hlegend, "location", @cb_legend_update); + addlistener (hlegend, "orientation", @cb_legend_update); + addlistener (hlegend, "string", @cb_legend_update); + addlistener (hlegend, "textposition", @cb_legend_update); + endif + + unwind_protect_cleanup + set (hfig, "currentaxes", origaxes); + if (! isempty (origfig)) + set (0, "currentfigure", origfig); + endif + end_unwind_protect + endif + endif + + ## Restore operation of callbacks + setappdata (hlegend, "nocallbacks", false); + + if (nargout > 0) + hleg = hlegend; + hleg_obj = hobjects; + hplot = hplots; + labels = text_strings; + endif + +endfunction + +## Colormap of the base axes has changed. +function cb_legend_colormap_update (cax, ~, hlegend) + set (hlegend, "colormap", get (cax, "colormap")); +endfunction + +## A non-text property of legend has changed requiring an update. +function cb_legend_update (hleg, ~) + persistent recursive = false; + + if (! recursive) + recursive = true; + unwind_protect + hax = getappdata (hleg, "__axes_handle__"); + ## Hack. Maybe store this somewhere else such as appdata. + hplots = get (hleg, "deletefcn"){6}; + text_strings = get (hleg, "string"); + position = get (hleg, "unmodified_axes_position"); + outerposition = get (hleg, "unmodified_axes_outerposition"); + units = get (hax, "units"); + set (hax, "units", "points"); + switch (get (hax, "activepositionproperty")) + case "position" + set (hax, "outerposition", outerposition, "position", position); + case "outerposition" + set (hax, "position", position, "outerposition", outerposition); + endswitch + if (isscalar (hax)) + set (hax, "units", units); + else + set (hax, {"units"}, units); + endif + + hleg = legend (hax(1), hplots, text_strings); + unwind_protect_cleanup + recursive = false; + end_unwind_protect + endif + +endfunction + +## A text property of legend, such as fontname, has changed. +function cb_legend_text_update (hleg, ~) + + kids = get (hleg, "children"); + htext = kids(strcmp (get (kids, "type"), "text")); + + tprops = {"fontangle", "fontname", "fontweight", "color"}; + lprops = {"fontangle", "fontname", "fontweight", "textcolor"}; + set (htext, tprops, get (hleg, lprops)); + + ec = get (hleg, "edgecolor"); + set (hleg, "xcolor", ec, "ycolor", ec); + +endfunction + +## The legend "visible" property has changed. +function cb_legend_hideshow (hleg, ~, ca, orig_pos, new_pos) + + isvisible = strcmp (get (hleg, "visible"), "on"); + + ## FIXME: Can't use a single set() call because of linked axes and + ## listeners on plotyy graphs. + ca = ca(isaxes (ca)); + for cax = ca(:).' + units = get (cax, "units"); + unwind_protect + set (cax, "units", "points"); + if (isvisible) + set (cax, "position", new_pos); + else + set (cax, "position", orig_pos); + endif + unwind_protect_cleanup + set (cax, "units", units); + end_unwind_protect + endfor + +endfunction + +## The legend "location" property has changed. +function cb_legend_location (hleg, d) + + ## If it isn't "none", which means manual positioning, then rebuild . + if (! strcmp (get (hleg, "location"), "none")) + cb_legend_update (hleg, d); + endif + +endfunction + +## Axes to which legend was attached is being deleted/reset. Delete legend. +function cb_axes_deleted (~, ~, ca, hlegend) + if (isaxes (hlegend)) + if (strcmp (get (ca(1), "beingdeleted"), "on")) + ## Axes are being deleted. Disable call to cb_restore_axes. + set (hlegend, "deletefcn", []); + endif + delete (hlegend); + endif +endfunction + +## Restore position of axes object when legend is deleted. +function cb_restore_axes (~, ~, ca, pos, outpos, htdel, hplots) + + hf = ancestor (ca(1), "figure"); + if (strcmp (get (hf, "beingdeleted"), "on") + || strcmp (get (ca(1), "beingdeleted"), "on")) + ## Skip restoring axes if entire figure or axes is being destroyed. + return; + endif + + ## Remove text object used to trigger legend delete when axes is deleted + if (ishghandle (htdel)) + set (htdel, "deletefcn", []); + delete (htdel); + endif + + ## Restore original axes positions + if (! isempty (pos)) + ## FIXME: can't use single call to set() because of weirdness w/plotyy + for cax = ca(:).' + if (isaxes (cax)) + units = get (cax, "units"); + unwind_protect + set (cax, "units", "points", "position", pos); + unwind_protect_cleanup + set (cax, "units", units); + end_unwind_protect + endif + endfor + endif + + ## Remove listeners from plot objects + for i = 1 : numel (hplots) + if (isgraphics (hplots(i), "line")) + dellistener (hplots(i), "color"); + dellistener (hplots(i), "linestyle"); + dellistener (hplots(i), "linewidth"); + dellistener (hplots(i), "marker"); + dellistener (hplots(i), "markeredgecolor"); + dellistener (hplots(i), "markerfacecolor"); + dellistener (hplots(i), "markersize"); + dellistener (hplots(i), "displayname"); + endif + endfor + + ## Nullify legend link (can't delete properties yet) + set (ca(1), "__legend_handle__", []); + +endfunction + +## Update legend item because underlying plot line object has changed. +function cb_line_listener (h, ~, hlegend, linelength, update_name) + + ## Don't execute callbacks when legend is under construction + legdata = getappdata (hlegend); + if (legdata.nocallbacks) + return; + endif + + if (update_name) + ## When string changes, have to rebuild legend completely + [hplots, text_strings] = __getlegenddata__ (hlegend); + if (isempty (hplots)) + delete (hlegend); + else + legend (legdata.handle(1), hplots, text_strings); + endif + else + kids = get (hlegend, "children"); + kids = kids([getappdata(kids, "handle"){:}] == h); + kids = kids(strcmp (get (kids, "type"), "line")); + idx = strcmp (get (kids, "marker"), "none"); + ll = kids (idx); + lm = kids (! idx); + + [linestyle, marker, displayname] = ... + get (h, {"linestyle", "marker", "displayname"}){:}; + + if (! isempty (ll)) + [xpos1, ypos1] = get (ll, {"xdata", "ydata"}){:}; + xpos2 = sum (xpos1) / 2; + ypos2 = ypos1(1); + delete (ll); + if (! isempty (lm)) + delete (lm); + endif + else + [xpos2, ypos2] = get (lm, {"xdata", "ydata"}){:}; + xpos1 = xpos2 + [-0.5, 0.5] * linelength; + ypos1 = [ypos2, ypos2]; + delete (lm); + endif + + if (! strcmp (linestyle, "none")) + hl = __go_line__ (hlegend, "xdata", xpos1, "ydata", ypos1, + "color", get (h, "color"), + "linestyle", get (h, "linestyle"), + "linewidth", min (get (h, "linewidth"), 5), + "marker", "none"); + setappdata (hl, "handle", h); + endif + if (! strcmp (marker, "none")) + hl = __go_line__ (hlegend, "xdata", xpos2, "ydata", ypos2, ... + "color", get (h, "color"), ... + "marker", marker, "markeredgecolor", get (h, "markeredgecolor"), ... + "markerfacecolor", get (h, "markerfacecolor"), ... + "markersize", min (get (h, "markersize"), 10), ... + "linestyle", "none", ... + "linewidth", min (get (h, "linewidth"), 5)); + setappdata (hl, "handle", h); + endif + endif + +endfunction + + +%!demo +%! clf; +%! plot (rand (2)); +%! title ("legend called with string inputs for labels"); +%! h = legend ("foo", "bar"); +%! legend (h, "location", "northeastoutside"); +%! set (h, "fontsize", 20); + +%!demo +%! clf; +%! plot (rand (2)); +%! title ("legend called with cell array of strings"); +%! h = legend ({"cellfoo", "cellbar"}); +%! legend (h, "location", "northeast"); +%! set (h, "fontsize", 20); + +%!demo +%! clf; +%! plot (rand (3)); +%! title ("legend () without inputs creates default labels"); +%! h = legend (); + +%!demo +%! clf; +%! x = 0:1; +%! plot (x,x,";I am Blue;", x,2*x, x,3*x,";I am yellow;"); +%! h = legend ("location", "northeastoutside"); +%! ## Placing legend inside returns axes to original size +%! legend (h, "location", "northeast"); +%! title ("Blue and Yellow keys, with Orange missing"); + +%!demo +%! clf; +%! plot (1:10, 1:10, 1:10, fliplr (1:10)); +%! title ("incline is blue and decline is orange"); +%! legend ({"I am blue", "I am orange"}, "location", "east"); +%! legend hide +%! legend show + +%!demo +%! clf; +%! plot (1:10, 1:10, 1:10, fliplr (1:10)); +%! title ("Legend with keys in horizontal orientation"); +%! legend ({"I am blue", "I am orange"}, ... +%! "location", "east", "orientation", "horizontal"); +%! legend boxoff +%! legend boxon + +%!demo +%! clf; +%! plot (1:10, 1:10, 1:10, fliplr (1:10)); +%! title ("Legend with box off"); +%! legend ({"I am blue", "I am orange"}, "location", "east"); +%! legend boxoff + +%!demo +%! clf; +%! plot (1:10, 1:10, 1:10, fliplr (1:10)); +%! title ("Legend with text to the left of key"); +%! legend ({"I am blue", "I am orange"}, "location", "east"); +%! legend left + +%!demo +%! clf; +%! plot (1:10, 1:10, 1:10, fliplr (1:10)); +%! title ({"Use properties to place legend text to the left of key", ... +%! "Legend text color is magenta"}); +%! h = legend ({"I am blue", "I am orange"}, "location", "east"); +%! legend ("right"); +%! set (h, "textposition", "left"); +%! set (h, "textcolor", [1 0 1]); + +%!demo +%! clf; +%! plot (1:10, 1:10, 1:10, fliplr (1:10)); +%! title ("Legend is hidden"); +%! legend ({"I am blue", "I am orange"}, "location", "east"); +%! legend hide + +%!demo +%! clf; +%! x = 0:1; +%! plot (x,x,";I am Blue;", x,2*x,";I am Orange;", x,3*x,";I am Yellow;"); +%! title ({"Labels are embedded in call to plot", ... +%! "Legend is hidden and then shown"}); +%! legend boxon +%! legend hide +%! legend show + +%!demo +%! clf; +%! x = 0:1; +%! plot (x,x, x,2*x, x,3*x); +%! title ("Labels with interpreted Greek text"); +%! h = legend ('\alpha', '\beta=2\alpha', '\gamma=3\alpha'); +%! set (h, "interpreter", "tex"); + +%!demo +%! clf; +%! plot (rand (2)); +%! title ("Labels with TeX interpreter turned off"); +%! h = legend ("Hello_World", "foo^bar"); +%! set (h, "interpreter", "none"); + +%!demo +%! clf; +%! labels = {}; +%! colororder = get (gca, "colororder"); +%! for i = 1:5 +%! h = plot (1:100, i + rand (100,1)); hold on; +%! set (h, "color", colororder(i,:)); +%! labels = {labels{:}, ["Signal ", num2str(i)]}; +%! endfor +%! hold off; +%! title ({"Signals with random offset and uniform noise"; +%! "Legend shown below and outside of plot"}); +%! xlabel ("Sample Nr [k]"); ylabel ("Amplitude [V]"); +%! legend (labels, "location", "southoutside"); + +%!demo +%! clf; +%! x = linspace (0, 10); +%! plot (x, x); +%! hold on; +%! stem (x, x.^2, "g"); +%! title ("First created object gets first label"); +%! legend ("linear"); +%! hold off; + +%!demo +%! clf; +%! x = linspace (0, 10); +%! plot (x, x, x, x.^2); +%! title ("First created object gets first label"); +%! legend ("linear"); + +%!demo +%! clf; +%! x = linspace (0, 10); +%! plot (x, x, x, x.^2); +%! title ("Labels are applied in order of object creation"); +%! legend ("linear", "quadratic"); + +%!demo +%! clf; +%! subplot (2,1,1); +%! rand_2x3_data1 = [0.341447, 0.171220, 0.284370; 0.039773, 0.731725, 0.779382]; +%! bar (rand_2x3_data1); +%! ylim ([0 1.0]); +%! title ("legend() works for bar graphs (hggroups)"); +%! legend ({"1st Bar", "2nd Bar", "3rd Bar"}, "location", "northwest"); +%! subplot (2,1,2); +%! x = linspace (0, 10, 20); +%! stem (x, 0.5+x.*rand (size (x))/max (x), "markeredgecolor", [0 0.7 0]); +%! hold on; +%! stem (x+10/(2*20), x.*(1.0+rand (size (x)))/max (x)); +%! xlim ([0 10+10/(2*20)]); +%! title ("legend() works for stem plots (hggroups)"); +%! legend ({"Multicolor", "Unicolor"}, "location", "northwest"); + +%!demo +%! clf; +%! colormap (cool (64)); +%! surf (peaks ()); +%! legend ("peaks()"); +%! title ("legend() works for surface objects too"); + +%!demo +%! clf reset; # needed to undo colormap assignment in previous demo +%! rand_2x3_data2 = [0.44804, 0.84368, 0.23012; 0.72311, 0.58335, 0.90531]; +%! bar (rand_2x3_data2); +%! ylim ([0 1.2]); +%! title ('"left" option places colors to the left of text label'); +%! legend ("1st Bar", "2nd Bar", "3rd Bar"); +%! legend left; + +%!demo +%! clf; +%! x = 0:0.1:7; +%! h = plot (x,sin(x), x,cos(x), x,sin(x.^2/10), x,cos(x.^2/10)); +%! title ("Only the sin() objects have keylabels"); +%! legend (h([1, 3]), {"sin (x)", "sin (x^2/10)"}, "location", "southwest"); + +%!demo +%! clf; +%! x = 0:0.1:10; +%! plot (x, sin (x), ";sin (x);"); +%! hold on; +%! plot (x, cos (x), ";cos (x);"); +%! hold off; +%! title ("legend constructed from multiple plot calls"); + +%!demo +%! clf; +%! x = 0:0.1:10; +%! plot (x, sin (x), ";sin (x);"); +%! hold on; +%! plot (x, cos (x), ";cos (x);"); +%! hold off; +%! title ("Specified label text overrides previous labels"); +%! legend ({"Sine", "Cosine"}, "location", "northeastoutside"); + +%!demo +%! clf; +%! x = 0:10; +%! plot (x, rand (11)); +%! axis ([0, 10, 0, 1]); +%! xlabel ("Indices"); +%! ylabel ("Random Values"); +%! title ('Legend "off" deletes the legend'); +%! legend (cellstr (num2str ((0:10)')), "location", "northeastoutside"); +%! pause (1); +%! legend off; + +%!demo +%! clf; +%! x = (1:5)'; +%! subplot (2,2,1); +%! plot (x, rand (numel (x))); +%! legend (cellstr (num2str (x)), "location", "northwestoutside"); +%! subplot (2,2,2); +%! plot (x, rand (numel (x))); +%! legend (cellstr (num2str (x)), "location", "northeastoutside"); +%! subplot (2,2,3); +%! plot (x, rand (numel (x))); +%! legend (cellstr (num2str (x)), "location", "southwestoutside"); +%! subplot (2,2,4); +%! plot (x, rand (numel (x))); +%! legend (cellstr (num2str (x)), "location", "southeastoutside"); +%! ## Legend works on a per axes basis for each subplot + +%!demo +%! clf; +%! plot (rand (2)); +%! title ("legend() will warn if extra labels are specified"); +%! legend ("Hello", "World", "interpreter", "foobar"); + +%!demo +%! clf; +%! x = 0:10; +%! y1 = rand (size (x)); +%! y2 = rand (size (x)); +%! [ax, h1, h2] = plotyy (x, y1, x, y2); +%! title ({"plotyy legend test #1", "Blue label to left axis, Orange label to right axis"}); +%! drawnow (); +%! legend ("Blue", "Orange", "location", "south"); + +%!demo +%! clf; +%! x = 0:10; +%! y1 = rand (size (x)); +%! y2 = rand (size (x)); +%! [ax, h1, h2] = plotyy (x, y1, x, y2); +%! ylabel (ax(1), {"Blue", "Y", "Axis"}); +%! title ('plotyy legend test #2: "westoutside" adjusts to ylabel'); +%! drawnow (); +%! legend ([h1, h2], {"Blue", "Orange"}, "location", "westoutside"); + +%!demo +%! clf; +%! x = 0:10; +%! y1 = rand (size (x)); +%! y2 = rand (size (x)); +%! [ax, h1, h2] = plotyy (x, y1, x, y2); +%! ylabel (ax(2), {"Orange", "Y", "Axis"}); +%! title ('plotyy legend test #3: "eastoutside" adjusts to ylabel'); +%! drawnow (); +%! legend ([h1, h2], {"Blue", "Orange"}, "location", "eastoutside"); + +%!demo +%! clf; +%! plot (1:10, 1:10); +%! title ("a very long label can sometimes cause problems"); +%! legend ("hello very big world", "location", "northeastoutside"); + +%!demo # bug 36408 +%! clf; +%! option = "right"; +%! subplot (3,1,1); +%! plot (rand (1,4)); +%! xlabel xlabel; +%! ylabel ylabel; +%! title ("Subplots adjust to the legend placed outside"); +%! legend ({"1"}, "location", "northeastoutside"); +%! legend (option); +%! subplot (3,1,2); +%! plot (rand (1,4)); +%! xlabel xlabel; +%! ylabel ylabel; +%! legend ({"1234567890"}, "location", "eastoutside"); +%! legend (option); +%! subplot (3,1,3); +%! plot (rand (1,4)); +%! xlabel xlabel; +%! ylabel ylabel; +%! legend ({"12345678901234567890"}, "location", "southeastoutside"); +%! legend (option); + +%!demo # bug 36408 +%! clf; +%! option = "right"; +%! subplot (3,1,1); +%! plot (rand (1,4)); +%! title ("Subplots adjust to the legend placed outside"); +%! legend ({"1"}, "location", "northwestoutside"); +%! legend (option); +%! subplot (3,1,2); +%! plot (rand (1,4)); +%! legend ({"1234567890"}, "location", "westoutside"); +%! legend (option); +%! subplot (3,1,3); +%! plot (rand (1,4)); +%! legend ({"12345678901234567890"}, "location", "southwestoutside"); +%! legend (option); + +%!demo # bug 36408 +%! clf; +%! option = "right"; +%! subplot (3,1,1); +%! plot (rand (1,4)); +%! set (gca (), "yaxislocation", "right"); +%! xlabel ("xlabel"); +%! ylabel ("ylabel"); +%! title ("Subplots adjust to the legend placed outside"); +%! legend ({"1"}, "location", "northeastoutside"); +%! legend (option); +%! subplot (3,1,2); +%! plot (rand (1,4)); +%! set (gca (), "yaxislocation", "right"); +%! xlabel ("xlabel"); +%! ylabel ("ylabel"); +%! legend ({"1234567890"}, "location", "eastoutside"); +%! legend (option); +%! subplot (3,1,3); +%! plot (rand (1,4)); +%! set (gca (), "yaxislocation", "right"); +%! xlabel ("xlabel"); +%! ylabel ("ylabel"); +%! legend ({"12345678901234567890"}, "location", "southeastoutside"); +%! legend (option); + +%!demo # bug 36408 +%! clf; +%! option = "right"; +%! subplot (3,1,1); +%! plot (rand (1,4)); +%! set (gca (), "yaxislocation", "right"); +%! xlabel ("xlabel"); +%! ylabel ("ylabel"); +%! title ("Subplots adjust to the legend placed outside"); +%! legend ({"1"}, "location", "northwestoutside"); +%! legend (option); +%! subplot (3,1,2); +%! plot (rand (1,4)); +%! set (gca (), "yaxislocation", "right"); +%! xlabel ("xlabel"); +%! ylabel ("ylabel"); +%! legend ({"1234567890"}, "location", "westoutside"); +%! legend (option); +%! subplot (3,1,3); +%! plot (rand (1,4)); +%! set (gca (), "yaxislocation", "right"); +%! xlabel ("xlabel"); +%! ylabel ("ylabel"); +%! legend ({"12345678901234567890"}, "location", "southwestoutside"); +%! legend (option); + +%!demo # bug 36408; +%! clf; +%! option = "right"; +%! subplot (3,1,1); +%! plot (rand (1,4)); +%! set (gca (), "xaxislocation", "top"); +%! xlabel ("xlabel"); +%! ylabel ("ylabel"); +%! title ("Subplots adjust to the legend placed outside"); +%! legend ({"1"}, "location", "northwestoutside"); +%! legend (option); +%! subplot (3,1,2); +%! plot (rand (1,4)); +%! set (gca (), "xaxislocation", "top"); +%! xlabel ("xlabel"); +%! ylabel ("ylabel"); +%! legend ({"1234567890"}, "location", "westoutside"); +%! legend (option); +%! subplot (3,1,3); +%! plot (rand (1,4)); +%! set (gca (), "xaxislocation", "top"); +%! xlabel ("xlabel"); +%! ylabel ("ylabel"); +%! legend ({"12345678901234567890"}, "location", "southwestoutside"); +%! legend (option); + +%!demo # bug 39697 +%! clf; +%! plot (1:10); +%! legend ("Legend Text"); +%! title ({"Multi-line", "titles", "are a", "problem", "See bug #39697"}); + +%!testif ; any (strcmp ("gnuplot", available_graphics_toolkits ())) +%! toolkit = graphics_toolkit ("gnuplot"); +%! h = figure ("visible", "off"); +%! unwind_protect +%! position = get (h, "position"); +%! plot (rand (3)); +%! legend (); +%! filename = sprintf ("%s.eps", tempname ()); +%! print (filename); +%! unlink (filename); +%! assert (get (h, "position"), position); +%! unwind_protect_cleanup +%! close (h); +%! graphics_toolkit (toolkit); +%! end_unwind_protect + +%!test <*42035> +%! h = figure ("visible", "off"); +%! unwind_protect +%! hax1 = subplot (1,2,1); +%! plot (1:10); +%! hax2 = subplot (1,2,2); +%! plot (1:10); +%! hleg1 = legend (hax1, "foo"); +%! assert (getappdata (hleg1, "__axes_handle__"), hax1); +%! assert (gca (), hax2); +%! hleg2 = legend ("bar"); +%! assert (getappdata (hleg2, "__axes_handle__"), gca ()); +%! unwind_protect_cleanup +%! close (h); +%! end_unwind_protect + +%!test +%! ## Difficult example from plotyy demo #1 +%! hf = figure ("visible", "off"); +%! unwind_protect +%! x = 0:0.1:2*pi; +%! y1 = sin (x); +%! y2 = exp (x - 1); +%! hax = plotyy (x,y1, x-1,y2, @plot, @semilogy); +%! text (0.5, 0.5, "Left Axis", "parent", hax(1)); +%! text (4.5, 80, "Right Axis", "parent", hax(2)); +%! hleg = legend ("show"); +%! assert (get (hleg, "string"), {"data1", "data2"}); +%! fail ("legend ('foo', 'bar', 'baz')", "warning", "ignoring extra labels"); +%! unwind_protect_cleanup +%! close (hf); +%! end_unwind_protect + +%!test +%! ## Test warnings about objects to label +%! hf = figure ("visible", "off"); +%! unwind_protect +%! hax = gca (); +%! fail ("legend ('foobar')", "warning", "plot data is empty"); +%! ht = text (0.5, 0.5, "Hello World"); +%! fail ("legend ('foobar')", "warning", "plot data is empty"); +%! lastwarn (""); # clear warning +%! hleg = legend (); +%! assert (isempty (hleg) && isempty (lastwarn ())); +%! fail ("legend ('foobar')", "warning", "plot data is empty"); +%! hln = line ([0 1], [0 1]); +%! fail ("legend ('foo', 'bar')", "warning", "ignoring extra labels"); +%! plot (rand (2, 21)); +%! fail ("legend ()", "warning", "labeling only first 20 data objects"); +%! unwind_protect_cleanup +%! close (hf); +%! end_unwind_protect + +%!test +%! ## Test warnings about unsupported features +%! hf = figure ("visible", "off"); +%! unwind_protect +%! plot (1:10); +%! fail ("legend ('location','best')", "warning", "'best' not yet implemented"); +%! unwind_protect_cleanup +%! close (hf); +%! end_unwind_protect
--- a/scripts/plot/draw/plotyy.m Sat Nov 16 23:32:42 2019 +0100 +++ b/scripts/plot/draw/plotyy.m Wed Nov 13 09:53:07 2019 +0100 @@ -127,8 +127,10 @@ if (strcmp (get (ax(1), "__autopos_tag__"), "subplot")) set (ax(2), "__autopos_tag__", "subplot"); + elseif (strcmp (graphics_toolkit (), "gnuplot")) + set (ax, "activepositionproperty", "position"); else - set (ax, "activepositionproperty", "position"); + set (ax, "activepositionproperty", "outerposition"); endif ## Don't replace axis which has colororder property already modified @@ -170,20 +172,13 @@ set (t2, "deletefcn", {@deleteplotyy, ax(1), t1}); ## Add cross-listeners so a change in one axes' attributes updates the other. - addlistener (ax(1), "position", {@update_position, ax(2)}); - addlistener (ax(2), "position", {@update_position, ax(1)}); - addlistener (ax(1), "outerposition", {@update_position, ax(2)}); - addlistener (ax(2), "outerposition", {@update_position, ax(1)}); - addlistener (ax(1), "looseinset", {@update_position, ax(2)}); - addlistener (ax(2), "looseinset", {@update_position, ax(1)}); - addlistener (ax(1), "view", {@update_position, ax(2)}); - addlistener (ax(2), "view", {@update_position, ax(1)}); - addlistener (ax(1), "plotboxaspectratio", {@update_position, ax(2)}); - addlistener (ax(2), "plotboxaspectratio", {@update_position, ax(1)}); - addlistener (ax(1), "plotboxaspectratiomode", {@update_position, ax(2)}); - addlistener (ax(2), "plotboxaspectratiomode", {@update_position, ax(1)}); - addlistener (ax(1), "nextplot", {@update_nextplot, ax(2)}); - addlistener (ax(2), "nextplot", {@update_nextplot, ax(1)}); + props = {"units", "looseinset", "position", "xlim", "view", ... + "plotboxaspectratio", "plotboxaspectratiomode", "nextplot"}; + + for ii = 1:numel (props) + addlistener (ax(1), props{ii}, {@update_prop, ax(2), props{ii}}); + addlistener (ax(2), props{ii}, {@update_prop, ax(1), props{ii}}); + endfor ## Store the axes handles for the sister axes. if (! isprop (ax(1), "__plotyy_axes__")) @@ -222,40 +217,14 @@ endfunction -function update_position (h, ~, ax2) +function update_prop (h, ~, ax2, prop) persistent recursion = false; - ## Don't allow recursion - if (! recursion) + if (! recursion && all (ishghandle ([h, ax2]))) unwind_protect recursion = true; - view = get (h, "view"); - oldview = get (ax2, "view"); - plotboxaspectratio = get (h, "plotboxaspectratio"); - oldplotboxaspectratio = get (ax2, "plotboxaspectratio"); - plotboxaspectratiomode = get (h, "plotboxaspectratiomode"); - oldplotboxaspectratiomode = get (ax2, "plotboxaspectratiomode"); - - if (strcmp (get (h, "activepositionproperty"), "position")) - position = get (h, "position"); - oldposition = get (ax2, "position"); - if (! (isequal (position, oldposition) && isequal (view, oldview))) - set (ax2, "position", position, "view", view); - endif - else - outerposition = get (h, "outerposition"); - oldouterposition = get (ax2, "outerposition"); - if (! (isequal (outerposition, oldouterposition) - && isequal (view, oldview))) - set (ax2, "outerposition", outerposition, "view", view); - endif - endif - - if (! (isequal (plotboxaspectratio, oldplotboxaspectratio) - && isequal (plotboxaspectratiomode, oldplotboxaspectratiomode))) - set (ax2, "plotboxaspectratio", plotboxaspectratio, - "plotboxaspectratiomode", plotboxaspectratiomode); - endif + val = get (h, prop); + set (ax2, prop, get (h, prop)); unwind_protect_cleanup recursion = false; end_unwind_protect