# HG changeset patch # User Pantxo Diribarne # Date 1637946367 -3600 # Node ID 4c0c02102ba9146d4f1f7fcae6b7d9fe31d494f1 # Parent 2434363ba3368d2658001c17bcb75fb01fde1b55 Allow for sticky "tight" option for auto axes limits (bug #61526) * graphics.in.h (axes::properties::m_xlimitmethod, m_ylimitmethod, m_zlimitmethod): New properties with updaters. (axes::properties::update_xlimitmethod, update_ylimitmethod, update_zlimitmethod): New updater methods that call corresponding update_*lim. (axes::properties::calc_ticks_and_lims): Change signature to include two new bool arguments that indicate whether limitmethod is tight or padded. Change all uses. (axes::properties::get_axis_limits): Change signature to include limitmethod as a string. * graphics.in.h (axes::properties::calc_ticks_and_lims): Don't adjust limits to closest ticks for "tight" and "padded" methods. (axes::properties::get_axis_limits): Leave limits unchanged for "tigh" method or add a 7% padding for "padded" method. * axis.m: Make use of the new properties and remove custom limits calculation routines, __get_tight_lims__ and __do_tight_option__. Add new options "padded" and "tickaligned". Add two demos to show the three methods in linear and log coordinates. Add test for stickiness of the "tight" option. * genpropdoc.m: Document new properties. diff -r 2434363ba336 -r 4c0c02102ba9 doc/interpreter/genpropdoc.m --- a/doc/interpreter/genpropdoc.m Fri Jan 21 15:23:38 2022 +0100 +++ b/doc/interpreter/genpropdoc.m Fri Nov 26 18:06:07 2021 +0100 @@ -930,6 +930,15 @@ for the x-axis. __modemsg__. @xref{XREFxlim, , @w{xlim function}}."; s.valid = valid_2elvec; + case "xlimitmethod" + s.doc = "Method used to determine the x axis limits when the \ +@code{xlimmode} property is @qcode{\"auto\"}. The default value, \ +@qcode{\"tickaligned\"} makes limits align with the closest ticks. With \ +value @qcode{\"tight\"} the limits are adjusted to enclose all the graphics \ +objects in the axes, while with value @qcode{\"padded\"}, an additionnal \ +margin of about 7%% of the data extent is added around the objects. \ +@xref{XREFaxis, , @w{axis function}}."; + case "xlimmode" case "xminorgrid" s.doc = "Control whether minor x grid lines are displayed."; @@ -975,6 +984,15 @@ for the y-axis. __modemsg__. @xref{XREFylim, , @w{ylim function}}."; s.valid = valid_2elvec; + case "xlimitmethod" + s.doc = "Method used to determine the y axis limits when the \ +@code{xlimmode} property is @qcode{\"auto\"}. The default value, \ +@qcode{\"tickaligned\"} makes limits align with the closest ticks. With \ +value @qcode{\"tight\"} the limits are adjusted to enclose all the graphics \ +objects in the axes, while with value @qcode{\"padded\"}, an additionnal \ +margin of about 7%% of the data extent is added around the objects. \ +@xref{XREFaxis, , @w{axis function}}."; + case "ylimmode" case "yminorgrid" s.doc = "Control whether minor y grid lines are displayed."; @@ -1013,6 +1031,15 @@ for the z-axis. __modemsg__. @xref{XREFzlim, , @w{zlim function}}."; s.valid = valid_2elvec; + case "xlimitmethod" + s.doc = "Method used to determine the z axis limits when the \ +@code{xlimmode} property is @qcode{\"auto\"}. The default value, \ +@qcode{\"tickaligned\"} makes limits align with the closest ticks. With \ +value @qcode{\"tight\"} the limits are adjusted to enclose all the graphics \ +objects in the axes, while with value @qcode{\"padded\"}, an additionnal \ +margin of about 7%% of the data extent is added around the objects. \ +@xref{XREFaxis, , @w{axis function}}."; + case "zlimmode" case "zminorgrid" s.doc = "Control whether minor z grid lines are displayed."; diff -r 2434363ba336 -r 4c0c02102ba9 libinterp/corefcn/graphics.cc --- a/libinterp/corefcn/graphics.cc Fri Jan 21 15:23:38 2022 +0100 +++ b/libinterp/corefcn/graphics.cc Fri Nov 26 18:06:07 2021 +0100 @@ -7786,7 +7786,8 @@ Matrix axes::properties::get_axis_limits (double xmin, double xmax, double min_pos, double max_neg, - const bool logscale) + const bool logscale, + const std::string& method) { Matrix retval; @@ -7838,17 +7839,38 @@ max_val *= 0.9; } } - if (min_val > 0) - { - // Log plots with all positive data - min_val = std::pow (10, std::floor (log10 (min_val))); - max_val = std::pow (10, std::ceil (log10 (max_val))); - } - else - { - // Log plots with all negative data - min_val = -std::pow (10, std::ceil (log10 (-min_val))); - max_val = -std::pow (10, std::floor (log10 (-max_val))); + + if (method == "tickaligned") + { + if (min_val > 0) + { + // Log plots with all positive data + min_val = std::pow (10, std::floor (log10 (min_val))); + max_val = std::pow (10, std::ceil (log10 (max_val))); + } + else + { + // Log plots with all negative data + min_val = -std::pow (10, std::ceil (log10 (-min_val))); + max_val = -std::pow (10, std::floor (log10 (-max_val))); + } + } + else if (method == "padded") + { + if (min_val > 0) + { + // Log plots with all positive data + double pad = (log10 (max_val) - log10 (min_val)) * 0.07; + min_val = std::pow (10, log10 (min_val) - pad); + max_val = std::pow (10, log10 (max_val) + pad); + } + else + { + // Log plots with all negative data + double pad = (log10 (-min_val) - log10 (-max_val)) * 0.07; + min_val = -std::pow (10, log10 (-min_val) + pad); + max_val = -std::pow (10, log10 (-max_val) - pad); + } } } else @@ -7866,12 +7888,21 @@ max_val += 0.1 * std::abs (max_val); } - double tick_sep = calc_tick_sep (min_val, max_val); - double min_tick = std::floor (min_val / tick_sep); - double max_tick = std::ceil (max_val / tick_sep); - // Prevent round-off from cropping ticks - min_val = std::min (min_val, tick_sep * min_tick); - max_val = std::max (max_val, tick_sep * max_tick); + if (method == "tickaligned") + { + double tick_sep = calc_tick_sep (min_val, max_val); + double min_tick = std::floor (min_val / tick_sep); + double max_tick = std::ceil (max_val / tick_sep); + // Prevent round-off from cropping ticks + min_val = std::min (min_val, tick_sep * min_tick); + max_val = std::max (max_val, tick_sep * max_tick); + } + else if (method == "padded") + { + double pad = 0.07 * (max_val - min_val); + min_val -= pad; + max_val += pad; + } } } @@ -8086,13 +8117,16 @@ array_property& mticks, bool limmode_is_auto, bool tickmode_is_auto, - bool is_logscale) + bool is_logscale, + bool method_is_padded, + bool method_is_tight) { if (lims.get ().isempty ()) return; double lo = (lims.get ().matrix_value ())(0); double hi = (lims.get ().matrix_value ())(1); + double lo_lim = lo; double hi_lim = hi; bool is_negative = lo < 0 && hi < 0; @@ -8136,17 +8170,28 @@ if (limmode_is_auto) { - // Adjust limits to include min and max ticks Matrix tmp_lims (1, 2); - tmp_lims(0) = std::min (tick_sep * i1, lo); - tmp_lims(1) = std::max (tick_sep * i2, hi); + + if (! method_is_padded && ! method_is_tight) + { + // Adjust limits to include min and max ticks + tmp_lims(0) = std::min (tick_sep * i1, lo); + tmp_lims(1) = std::max (tick_sep * i2, hi); + } + else + { + tmp_lims(0) = lo; + tmp_lims(1) = hi; + } if (is_logscale) { tmp_lims(0) = std::pow (10., tmp_lims(0)); tmp_lims(1) = std::pow (10., tmp_lims(1)); + if (tmp_lims(0) <= 0) tmp_lims(0) = std::pow (10., lo); + if (is_negative) { double tmp = tmp_lims(0); @@ -8154,6 +8199,7 @@ tmp_lims(1) = -tmp; } } + lims = tmp_lims; } else @@ -8522,9 +8568,11 @@ { get_children_limits (min_val, max_val, min_pos, max_neg, kids, 'x'); + std::string method = m_properties.get_xlimitmethod (); limits = m_properties.get_axis_limits (min_val, max_val, min_pos, max_neg, - m_properties.xscale_is ("log")); + m_properties.xscale_is ("log"), + method); } else m_properties.check_axis_limits (limits, kids, @@ -8543,9 +8591,11 @@ { get_children_limits (min_val, max_val, min_pos, max_neg, kids, 'y'); + std::string method = m_properties.get_ylimitmethod (); limits = m_properties.get_axis_limits (min_val, max_val, min_pos, max_neg, - m_properties.yscale_is ("log")); + m_properties.yscale_is ("log"), + method); } else m_properties.check_axis_limits (limits, kids, @@ -8567,9 +8617,11 @@ m_properties.set_has3Dkids ((max_val - min_val) > std::numeric_limits::epsilon ()); + std::string method = m_properties.get_zlimitmethod (); limits = m_properties.get_axis_limits (min_val, max_val, min_pos, max_neg, - m_properties.zscale_is ("log")); + m_properties.zscale_is ("log"), + method); } else { @@ -8722,9 +8774,11 @@ { get_children_limits (min_val, max_val, min_pos, max_neg, kids, 'x'); + std::string method = m_properties.get_xlimitmethod (); limits = m_properties.get_axis_limits (min_val, max_val, min_pos, max_neg, - m_properties.xscale_is ("log")); + m_properties.xscale_is ("log"), + method); } else { @@ -8745,9 +8799,11 @@ { get_children_limits (min_val, max_val, min_pos, max_neg, kids, 'y'); + std::string method = m_properties.get_ylimitmethod (); limits = m_properties.get_axis_limits (min_val, max_val, min_pos, max_neg, - m_properties.yscale_is ("log")); + m_properties.yscale_is ("log"), + method); } else { @@ -8777,9 +8833,11 @@ && ! m_properties.zscale_is ("log")) min_val = max_val = 0.; + std::string method = m_properties.get_zlimitmethod (); limits = m_properties.get_axis_limits (min_val, max_val, min_pos, max_neg, - m_properties.zscale_is ("log")); + m_properties.zscale_is ("log"), + method); } else { diff -r 2434363ba336 -r 4c0c02102ba9 libinterp/corefcn/graphics.in.h --- a/libinterp/corefcn/graphics.in.h Fri Jan 21 15:23:38 2022 +0100 +++ b/libinterp/corefcn/graphics.in.h Fri Nov 26 18:06:07 2021 +0100 @@ -3828,6 +3828,7 @@ bool_property xgrid , "off" handle_property xlabel SOf , make_graphics_handle ("text", m___myhandle__, false, false, false) row_vector_property xlim mu , default_lim () + radio_property xlimitmethod u , "{tickaligned}|tight|padded" radio_property xlimmode al , "{auto}|manual" bool_property xminorgrid , "off" bool_property xminortick , "off" @@ -3847,6 +3848,7 @@ bool_property ygrid , "off" handle_property ylabel SOf , make_graphics_handle ("text", m___myhandle__, false, false, false) row_vector_property ylim mu , default_lim () + radio_property ylimitmethod u , "{tickaligned}|tight|padded" radio_property ylimmode al , "{auto}|manual" bool_property yminorgrid , "off" bool_property yminortick , "off" @@ -3864,6 +3866,7 @@ bool_property zgrid , "off" handle_property zlabel SOf , make_graphics_handle ("text", m___myhandle__, false, false, false) row_vector_property zlim mu , default_lim () + radio_property zlimitmethod u , "{tickaligned}|tight|padded" radio_property zlimmode al , "{auto}|manual" bool_property zminorgrid , "off" bool_property zminortick , "off" @@ -4043,7 +4046,8 @@ { calc_ticks_and_lims (m_xlim, m_xtick, m_xminortickvalues, m_xlimmode.is ("auto"), m_xtickmode.is ("auto"), - m_xscale.is ("log")); + m_xscale.is ("log"), m_xlimitmethod.is ("padded"), + m_xlimitmethod.is ("tight")); if (m_xticklabelmode.is ("auto")) calc_ticklabels (m_xtick, m_xticklabel, m_xscale.is ("log"), xaxislocation_is ("origin"), @@ -4059,7 +4063,8 @@ { calc_ticks_and_lims (m_ylim, m_ytick, m_yminortickvalues, m_ylimmode.is ("auto"), m_ytickmode.is ("auto"), - m_yscale.is ("log")); + m_yscale.is ("log"), m_ylimitmethod.is ("padded"), + m_ylimitmethod.is ("tight")); if (m_yticklabelmode.is ("auto")) calc_ticklabels (m_ytick, m_yticklabel, m_yscale.is ("log"), yaxislocation_is ("origin"), @@ -4075,7 +4080,8 @@ { calc_ticks_and_lims (m_zlim, m_ztick, m_zminortickvalues, m_zlimmode.is ("auto"), m_ztickmode.is ("auto"), - m_zscale.is ("log")); + m_zscale.is ("log"), m_zlimitmethod.is ("padded"), + m_zlimitmethod.is ("tight")); if (m_zticklabelmode.is ("auto")) calc_ticklabels (m_ztick, m_zticklabel, m_zscale.is ("log"), false, 2, m_zlim); @@ -4180,7 +4186,8 @@ OCTINTERP_API void calc_ticks_and_lims (array_property& lims, array_property& ticks, array_property& mticks, bool limmode_is_auto, - bool tickmode_is_auto, bool is_logscale); + bool tickmode_is_auto, bool is_logscale, + bool method_is_padded, bool method_is_tight); OCTINTERP_API void calc_ticklabels (const array_property& ticks, any_property& labels, bool is_logscale, const bool is_origin, @@ -4229,7 +4236,7 @@ OCTINTERP_API Matrix get_axis_limits (double xmin, double xmax, double min_pos, double max_neg, - const bool logscale); + const bool logscale, const std::string& method); OCTINTERP_API void check_axis_limits (Matrix& limits, const Matrix kids, @@ -4241,7 +4248,8 @@ calc_ticks_and_lims (m_xlim, m_xtick, m_xminortickvalues, m_xlimmode.is ("auto"), m_xtickmode.is ("auto"), - m_xscale.is ("log")); + m_xscale.is ("log"), m_xlimitmethod.is ("padded"), + m_xlimitmethod.is ("tight")); if (m_xticklabelmode.is ("auto")) calc_ticklabels (m_xtick, m_xticklabel, m_xscale.is ("log"), m_xaxislocation.is ("origin"), @@ -4257,13 +4265,19 @@ update_axes_layout (); } + void update_xlimitmethod () + { + update_xlim (); + } + void update_ylim (void) { update_axis_limits ("ylim"); calc_ticks_and_lims (m_ylim, m_ytick, m_yminortickvalues, m_ylimmode.is ("auto"), m_ytickmode.is ("auto"), - m_yscale.is ("log")); + m_yscale.is ("log"), m_ylimitmethod.is ("padded"), + m_ylimitmethod.is ("tight")); if (m_yticklabelmode.is ("auto")) calc_ticklabels (m_ytick, m_yticklabel, m_yscale.is ("log"), yaxislocation_is ("origin"), @@ -4279,13 +4293,19 @@ update_axes_layout (); } + void update_ylimitmethod () + { + update_ylim (); + } + void update_zlim (void) { update_axis_limits ("zlim"); calc_ticks_and_lims (m_zlim, m_ztick, m_zminortickvalues, m_zlimmode.is ("auto"), m_ztickmode.is ("auto"), - m_zscale.is ("log")); + m_zscale.is ("log"), m_zlimitmethod.is ("padded"), + m_zlimitmethod.is ("tight")); if (m_zticklabelmode.is ("auto")) calc_ticklabels (m_ztick, m_zticklabel, m_zscale.is ("log"), false, 2, m_zlim); @@ -4297,6 +4317,11 @@ update_axes_layout (); } + void update_zlimitmethod () + { + update_zlim (); + } + void trigger_normals_calc (void); }; diff -r 2434363ba336 -r 4c0c02102ba9 scripts/plot/appearance/axis.m --- a/scripts/plot/appearance/axis.m Fri Jan 21 15:23:38 2022 +0100 +++ b/scripts/plot/appearance/axis.m Fri Nov 26 18:06:07 2021 +0100 @@ -83,9 +83,16 @@ ## @item @qcode{"manual"} ## Fix the current axes limits. ## +## @item @qcode{"tickaligned"} +## Fix axes to the limits of the closest ticks. +## ## @item @qcode{"tight"} ## Fix axes to the limits of the data. ## +## @item @qcode{"padded"} +## Fix axes to the limits of the data plus margins of about 7% of the +## data extent. +## ## @item @qcode{"image"} ## Equivalent to @qcode{"tight"} and @qcode{"equal"}. ## @@ -213,8 +220,10 @@ ## aspect ratio elseif (strcmpi (opt, "image")) __axis__ (ca, "equal"); - set (ca, "plotboxaspectratiomode", "auto"); - __do_tight_option__ (ca); + set (ca, "plotboxaspectratiomode", "auto", ... + "xlimmode", "auto", "ylimmode", "auto", ... + "zlimmode", "auto", ... + "xlimitmethod", "tight", "ylimitmethod", "tight"); elseif (strcmpi (opt, "square")) set (ca, "dataaspectratiomode", "auto", "plotboxaspectratio", [1, 1, 1]); @@ -280,10 +289,26 @@ ## fixes the axis limits set (ca, "xlimmode", "manual", "ylimmode", "manual", "zlimmode", "manual"); + elseif (strcmpi (opt, "tickaligned")) + ## sets the axis limits to closest ticks. + set (ca, "xlimmode", "auto", "ylimmode", "auto", ... + "zlimmode", "auto", ... + "xlimitmethod", "tickaligned", ... + "ylimitmethod", "tickaligned", ... + "zlimitmethod", "tickaligned"); elseif (strcmpi (opt, "tight")) ## sets the axis limits to the min and max of all data. - __do_tight_option__ (ca); - + set (ca, "xlimmode", "auto", "ylimmode", "auto", ... + "zlimmode", "auto", ... + "xlimitmethod", "tight", "ylimitmethod", "tight", + "zlimitmethod", "tight"); + elseif (strcmpi (opt, "padded")) + ## sets the axis limits to the min and max of all data plus padding. + set (ca, "xlimmode", "auto", "ylimmode", "auto", ... + "zlimmode", "auto", ... + "xlimitmethod", "padded", ... + "ylimitmethod", "padded", ... + "zlimitmethod", "padded"); ## visibility elseif (strcmpi (opt, "on")) set (ca, "visible", "on"); @@ -376,87 +401,6 @@ endfunction -## Find the limits for axis ("tight"). -## AX should be one of "x", "y", or "z". -function lims = __get_tight_lims__ (ca, ax) - - kids = findobj (ca, "-property", [ax "data"]); - ## The data properties for hggroups mirror their children. - ## Exclude the redundant hggroup values. - hg_kids = findobj (kids, "type", "hggroup"); - kids = setdiff (kids, hg_kids); - if (isempty (kids)) - ## Return the current limits. - ## FIXME: Is this the correct thing to do? - lims = get (ca, [ax "lim"]); - else - data = get (kids, [ax "data"]); - types = get (kids, "type"); - - scale = get (ca, [ax "scale"]); - if (! iscell (data)) - data = {data}; - endif - - ## Extend image data one pixel - idx = strcmp (types, "image"); - if (any (idx) && (ax == "x" || ax == "y")) - imdata = data(idx); - px = arrayfun (@__image_pixel_size__, kids(idx), "uniformoutput", false); - ipx = ifelse (ax == "x", 1, 2); - imdata = cellfun (@(x,dx) [(min (x) - dx(ipx)), (max (x) + dx(ipx))], - imdata, px, "uniformoutput", false); - data(idx) = imdata; - endif - - if (strcmp (scale, "log")) - tmp = data; - data = cellfun (@(x) x(x>0), tmp, "uniformoutput", false); - n = cellfun ("isempty", data); - data(n) = cellfun (@(x) x(x<0), tmp(n), "uniformoutput", false); - endif - data = cellfun (@(x) x(isfinite (x)), data, "uniformoutput", false); - data = data(! cellfun ("isempty", data)); - if (! isempty (data)) - ## Change data from cell array of various sizes to a single column vector - data = cat (1, cellindexmat (data, ":"){:}); - lims = [min(data), max(data)]; - else - lims = [0, 1]; - endif - endif - -endfunction - -function __do_tight_option__ (ca) - - xlim = __get_tight_lims__ (ca, "x"); - if (all (xlim == 0)) - xlim = [-eps, +eps]; - elseif (diff (xlim == 0)) - xlim .*= [1-eps, 1+eps]; - endif - ylim = __get_tight_lims__ (ca, "y"); - if (all (ylim == 0)) - ylim = [-eps, +eps]; - elseif (diff (ylim == 0)) - ylim .*= [1-eps, 1+eps]; - endif - set (ca, "xlim", xlim, "ylim", ylim); - nd = __calc_dimensions__ (ca); - is3dview = (get (ca, "view")(2) != 90); - if (nd > 2 && is3dview) - zlim = __get_tight_lims__ (ca, "z"); - if (all (zlim == 0)) - zlim = [-eps, +eps]; - elseif (diff (zlim == 0)) - zlim .*= [1-eps, 1+eps]; - endif - set (ca, "zlim", zlim); - endif - -endfunction - %!demo %! clf; @@ -618,6 +562,38 @@ %! axis tight; %! title ('"tight" on loglog plot'); +%!demo +%! clf; +%! x = y = 0.5:0.5:12; +%! subplot (3,1,1); +%! plot (x, y, "-s"); +%! axis tickaligned +%! title ("tickaligned"); +%! subplot (3,1,2); +%! plot (x, y, "-s"); +%! axis padded +%! title ("padded"); +%! subplot (3,1,3); +%! plot (x, y, "-s"); +%! axis tight +%! title ("tight"); + +%!demo +%! clf; +%! x = y = 0.5:0.5:12; +%! subplot (3,1,1); +%! loglog (x, y, "-s"); +%! axis tickaligned +%! title ("tickaligned"); +%! subplot (3,1,2); +%! loglog (x, y, "-s"); +%! axis padded +%! title ("padded"); +%! subplot (3,1,3); +%! loglog (x, y, "-s"); +%! axis tight +%! title ("tight"); + %!test %! hf = figure ("visible", "off"); %! unwind_protect @@ -655,6 +631,19 @@ %! close (hf); %! end_unwind_protect +## Test 'axis tight' remains after addition of new data +%!test +%! hf = figure ("visible", "off"); +%! unwind_protect +%! plot (1:10) +%! axis tight; +%! assert (axis (), [1 10 1 10]); +%! plot (1:11) +%! assert (axis (), [1 11 1 11]); +%! unwind_protect_cleanup +%! close (hf); +%! end_unwind_protect + ## Even on errors, axis can display a figure. %!error %! hf = figure ("visible", "off");