Ticket #4529: trac_4529-add-log-scale.patch

File trac_4529-add-log-scale.patch, 22.5 KB (added by kcrisman, 9 years ago)
  • sage/plot/graphics.py

    # HG changeset patch
    # User Punarbasu Purkayastha <ppurka@gmail.com>
    # Date 1338111511 -28800
    # Node ID 50485c4d4dd2d50839e508b720ba0121053e528f
    # Parent  2e78a1d2e09596753afb4ed9223be6c78c5c9e36
    add logarithmic scale to Graphics(). Also fix a problem with user specified xmin,xmax,ymin,ymax
    
    diff --git a/sage/plot/graphics.py b/sage/plot/graphics.py
    a b  
    99AUTHORS:
    1010
    1111- Jeroen Demeyer (2012-04-19): split off this file from plot.py (:trac:`12857`)
     12- Punarbasu Purkayastha (2012-05-20): Add logarithmic scale (:trac:`4529`)
    1213
    1314"""
    1415
     
    117118        ...        G+=line(l,color=hue(c + p*(x/h)))
    118119        sage: G.show(figsize=[5,5])
    119120
     121    We can change the scale of the axes in the graphics before displaying.::
     122
     123        sage: G = plot(exp, 1, 10)
     124        sage: G.show(scale='semilogy')
     125
    120126    TESTS:
    121127
    122128    From :trac:`4604`, ensure Graphics can handle 3d objects::
     
    843849
    844850            sage: show_default(True)
    845851        """
    846         pr, i = '', 0
    847         for x in self:
    848             pr += '\n\t%s -- %s'%(i, x)
    849             i += 1
    850852        s = "Graphics object consisting of %s graphics primitives"%(len(self))
    851853        if len(self) == 1:
    852854            s = s[:-1]
     
    10011003        elif other.aspect_ratio()=='automatic':
    10021004            g.set_aspect_ratio(self.aspect_ratio())
    10031005        else:
    1004             g.set_aspect_ratio( max(self.aspect_ratio(), other.aspect_ratio()))
     1006            g.set_aspect_ratio(max(self.aspect_ratio(), other.aspect_ratio()))
    10051007        return g
    10061008
    10071009    def add_primitive(self, primitive):
     
    10981100        """
    10991101        self._extra_kwds = kwds
    11001102
     1103    def _set_scale(self, figure, scale=None, base=None):
     1104        """
     1105        Set the scale of the axes in the current figure. This function is
     1106        only for internal use.
     1107
     1108        INPUT:
     1109        - ``figure`` -- the matplotlib figure instance.
     1110        - ``scale`` -- the scale of the figure. Values it can take are
     1111          `"linear"`, `"loglog"`, `"semilogx"`, `"semilogy"`. See
     1112          :meth:`show` for other options it can take.
     1113        - ``base`` -- the base of the logarithm if a logarithmic scale is
     1114          set. See :meth:`show` for the options it can take.
     1115
     1116        OUTPUT:
     1117        The scale in the form of a tuple: (xscale, yscale, basex, basey)
     1118
     1119        EXAMPLES::
     1120
     1121            sage: p = plot(x,1,10)
     1122            sage: fig = p.matplotlib()
     1123            sage: p._set_scale(fig, scale='linear', base=2)
     1124            ('linear', 'linear', 10, 10)
     1125            sage: p._set_scale(fig, scale='semilogy', base=2)
     1126            ('linear', 'log', 10, 2)
     1127            sage: p._set_scale(fig, scale=('loglog', 2, 3))
     1128            ('log', 'log', 2, 3)
     1129            sage: p._set_scale(fig, scale=['semilogx', 2])
     1130            ('log', 'linear', 2, 10)
     1131
     1132        TESTS::
     1133
     1134            sage: p._set_scale(fig, 'log')
     1135            Traceback (most recent call last):
     1136            ...
     1137            ValueError: The scale must be one of 'linear', 'loglog', 'semilogx' or 'semilogy' -- got 'log'
     1138            sage: p._set_scale(fig, ('loglog', 1))
     1139            Traceback (most recent call last):
     1140            ...
     1141            ValueError: The base of the logarithm must be greater than 1
     1142        """
     1143        if scale is None:
     1144            return ('linear', 'linear', 10, 10)
     1145        if isinstance(scale, (list, tuple)):
     1146            if len(scale) != 2 and len(scale) != 3:
     1147                raise ValueError("If the input is a tuple, it must be of "
     1148                    "the form (scale, base) or (scale, basex, basey)")
     1149            if len(scale) == 2:
     1150                base = scale[1]
     1151            else:
     1152                base = scale[1:]
     1153            scale = scale[0]
     1154
     1155        if scale not in ('linear', 'loglog', 'semilogx', 'semilogy'):
     1156            raise ValueError("The scale must be one of 'linear', 'loglog',"
     1157                    " 'semilogx' or 'semilogy' -- got '{0}'".format(scale))
     1158
     1159        if isinstance(base, (list, tuple)):
     1160            basex, basey = base
     1161        elif base is None:
     1162            basex = basey = 10
     1163        else:
     1164            basex = basey = base
     1165
     1166        if basex <= 1 or basey <= 1:
     1167            raise ValueError("The base of the logarithm must be greater "
     1168                             "than 1")
     1169
     1170        ax = figure.get_axes()[0]
     1171        xscale = yscale = 'linear'
     1172        if scale == 'linear':
     1173            basex = basey = 10
     1174        elif scale == 'loglog':
     1175            ax.set_xscale('log', basex=basex)
     1176            ax.set_yscale('log', basey=basey)
     1177            xscale = yscale = 'log'
     1178        elif scale == 'semilogx':
     1179            ax.set_xscale('log', basex=basex)
     1180            basey = 10
     1181            xscale = 'log'
     1182        elif scale == 'semilogy':
     1183            ax.set_yscale('log', basey=basey)
     1184            basex = 10
     1185            yscale = 'log'
     1186
     1187        return (xscale, yscale, basex, basey)
     1188
     1189
    11011190    # This dictionary has the default values for the keywords to show(). When
    11021191    # show is invoked with keyword arguments, those arguments are merged with
    11031192    # this dictionary to create a set of keywords with the defaults filled in.
     
    11091198    SHOW_OPTIONS = dict(filename=None,
    11101199                        # axes options
    11111200                        axes=None, axes_labels=None, axes_pad=.02,
     1201                        base=None, scale=None,
    11121202                        xmin=None, xmax=None, ymin=None, ymax=None,
    11131203                        # Figure options
    11141204                        aspect_ratio=None, dpi=DEFAULT_DPI, fig_tight=True,
     
    12741364
    12751365        - ``legend_*`` - all the options valid for :meth:`set_legend_options` prefixed with ``legend_``
    12761366
     1367        - ``base`` - (default: 10) the base of the logarithm if
     1368          a logarithmic scale is set. This must be greater than 1. The base
     1369          can be also given as a list or tuple ``(basex, basey)``.
     1370          ``basex`` sets the base of the logarithm along the horizontal
     1371          axis and ``basey`` sets the base along the vertical axis.
     1372
     1373        - ``scale`` -- (default: `"linear"`) string. The scale of the axes.
     1374          Possible values are
     1375
     1376          - `"linear"` -- linear scaling of both the axes
     1377          - `"loglog"` -- sets both the horizontal and vertical axes to
     1378            logarithmic scale
     1379          - `"semilogx"` -- sets only the horizontal axis to logarithmic
     1380            scale.
     1381          - `"semilogy"` -- sets only the vertical axis to logarithmic
     1382            scale.
     1383
     1384          The scale can be also be given as single argument that is a list
     1385          or tuple ``(scale, base)`` or ``(scale, basex, basey)``.
     1386
     1387          Note: If the ``scale`` is `"linear"`, then irrespective of what
     1388          ``base`` is set to, it will default to 10 and will remain unused.
     1389
    12771390        EXAMPLES::
    12781391
    12791392            sage: c = circle((1,1), 1, color='red')
     
    13001413
    13011414            sage: plot(sin(x), (x, -4, 4), transparent=True)
    13021415
     1416        We can change the scale of the axes in the graphics before
     1417        displaying::
     1418
     1419            sage: G = plot(exp, 1, 10)
     1420            sage: G.show(scale='semilogy')
     1421
     1422        We can change the base of the logarithm too. The following changes
     1423        the vertical axis to be on log scale, and with base 2. Note that
     1424        the ``base`` argument will ignore any changes to the axis which is
     1425        in linear scale.::
     1426
     1427            sage: G.show(scale='semilogy', base=2) # y axis as powers of 2
     1428
     1429            sage: G.show(scale='semilogy', base=(3,2)) # base ignored for x-axis
     1430
     1431        The scale can be also given as a 2-tuple or a 3-tuple.::
     1432
     1433            sage: G.show(scale=('loglog', 2)) # both x and y axes in base 2
     1434
     1435            sage: G.show(scale=('loglog', 2, 3)) # x in base 2, y in base 3
     1436
     1437        The base need not be an integer.::
     1438
     1439            sage: G.show(scale='semilogy', base=float(e)) # base is e
     1440
     1441        Logarithmic scale can be used for various kinds of plots. Here are
     1442        some examples.::
     1443
     1444            sage: G = list_plot(map(lambda i: 10**i, range(10)))
     1445            sage: G.show(scale='semilogy')
     1446
     1447            sage: G = parametric_plot((x, x**2), (x, 1, 10))
     1448            sage: G.show(scale='loglog')
     1449
     1450            sage: G = arc((2,3), 2, 1, angle=pi/2, sector=(0,pi/2))
     1451            sage: G.show(scale=('loglog', 2))
     1452
     1453            sage: disk((0.1,0.1), 1, (pi/3, pi/2)).show(scale='loglog')
     1454
     1455            sage: polygon([(1,1), (10,10), (10,1)]).show(scale='loglog')
     1456
     1457            sage: x, y = var('x, y')
     1458            sage: G =  plot_vector_field((sin(x),cos(y)),(x,0.1,3),(y,0.1,3))
     1459            sage: G.show(scale='loglog')
     1460
    13031461        Add grid lines at the major ticks of the axes.
    13041462
    13051463        ::
     
    16101768            ymax += 1
    16111769        return {'xmin':xmin, 'xmax':xmax, 'ymin':ymin, 'ymax':ymax}
    16121770
    1613     def _matplotlib_tick_formatter(self, subplot, locator_options={},
     1771    def _matplotlib_tick_formatter(self, subplot, base=(10, 10),
     1772                            locator_options={}, scale=('linear', 'linear'),
    16141773                            tick_formatter=(None, None), ticks=(None, None),
    16151774                            xmax=None, xmin=None, ymax=None, ymin=None):
    16161775        r"""
     
    16351794        """
    16361795        # This function is created to refactor some code that is repeated
    16371796        # in the matplotlib function
    1638         from matplotlib.ticker import (FixedLocator, Locator, MaxNLocator,
     1797        from matplotlib.ticker import (FixedLocator, Locator,
     1798                LogFormatterMathtext, LogLocator, MaxNLocator,
    16391799                MultipleLocator, NullLocator, OldScalarFormatter)
    16401800
    16411801        x_locator, y_locator = ticks
    16421802        #---------------------- Location of x-ticks ---------------------#
     1803
    16431804        if x_locator is None:
    1644             x_locator = MaxNLocator(**locator_options)
     1805            if scale[0] == 'log':
     1806                x_locator = LogLocator(base=base[0])
     1807            else:
     1808                x_locator = MaxNLocator(**locator_options)
    16451809        elif isinstance(x_locator,Locator):
    16461810            pass
    16471811        elif x_locator == []:
     
    16591823
    16601824        #---------------------- Location of y-ticks ---------------------#
    16611825        if y_locator is None:
    1662             y_locator = MaxNLocator(**locator_options)
     1826            if scale[1] == 'log':
     1827                y_locator = LogLocator(base=base[1])
     1828            else:
     1829                y_locator = MaxNLocator(**locator_options)
    16631830        elif isinstance(y_locator,Locator):
    16641831            pass
    16651832        elif y_locator == []:
     
    16811848        from sage.symbolic.ring import SR
    16821849        #---------------------- Formatting x-ticks ----------------------#
    16831850        if x_formatter is None:
    1684             x_formatter = OldScalarFormatter()
     1851            if scale[0] == 'log':
     1852                x_formatter = LogFormatterMathtext(base=base[0])
     1853            else:
     1854                x_formatter = OldScalarFormatter()
    16851855        elif x_formatter in SR:
    16861856            from misc import _multiple_of_constant
    16871857            x_const = x_formatter
    16881858            x_formatter = FuncFormatter(lambda n,pos:
    16891859                                        _multiple_of_constant(n,pos,x_const))
    16901860        elif x_formatter == "latex":
    1691             x_formatter = FuncFormatter(lambda n,pos: '$%s$'%latex(n))
     1861            if scale[0] == 'log':
     1862                # We need to strip out '\\mathdefault' from the string
     1863                x_formatter = FuncFormatter(lambda n,pos:
     1864                    LogFormatterMathtext(base=base[0])(n,pos).replace(
     1865                                                        "\\mathdefault",""))
     1866            else:
     1867                x_formatter = FuncFormatter(lambda n,pos: '$%s$'%latex(n))
    16921868        #---------------------- Formatting y-ticks ----------------------#
    16931869        if y_formatter is None:
    1694             y_formatter = OldScalarFormatter()
     1870            if scale[1] == 'log':
     1871                y_formatter = LogFormatterMathtext(base=base[1])
     1872            else:
     1873                y_formatter = OldScalarFormatter()
    16951874        elif y_formatter in SR:
    16961875            from misc import _multiple_of_constant
    16971876            y_const = y_formatter
    16981877            y_formatter = FuncFormatter(lambda n,pos:
    16991878                                        _multiple_of_constant(n,pos,y_const))
    17001879        elif y_formatter == "latex":
    1701             y_formatter = FuncFormatter(lambda n,pos: '$%s$'%latex(n))
     1880            if scale[1] == 'log':
     1881                # We need to strip out '\\mathdefault' from the string
     1882                y_formatter = FuncFormatter(lambda n,pos:
     1883                    LogFormatterMathtext(base=base[1])(n,pos).replace(
     1884                                                        "\\mathdefault",""))
     1885            else:
     1886                y_formatter = FuncFormatter(lambda n,pos: '$%s$'%latex(n))
    17021887
    17031888        subplot.xaxis.set_major_locator(x_locator)
    17041889        subplot.yaxis.set_major_locator(y_locator)
     
    17171902                   vgridlinesstyle=None, hgridlinesstyle=None,
    17181903                   show_legend=None, legend_options={},
    17191904                   axes_pad=0.02, ticks_integer=None,
    1720                    tick_formatter=None, ticks=None):
     1905                   tick_formatter=None, ticks=None,
     1906                   base=None, scale=None):
    17211907        r"""
    17221908        Return a matplotlib figure object representing the graphic
    17231909
     
    18111997            if hasattr(g, '_bbox_extra_artists'):
    18121998                self._bbox_extra_artists.extend(g._bbox_extra_artists)
    18131999
    1814         #add the legend if requested
     2000        #--------------------------- Set the scale -----------------------#
     2001        xscale, yscale, basex, basey = self._set_scale(figure, scale=scale,
     2002                                                       base=base)
     2003
     2004        #-------------------------- Set the legend -----------------------#
    18152005        if show_legend is None:
    18162006            show_legend = self._show_legend
    18172007
     
    18332023
    18342024
    18352025        subplot.set_xlim([xmin, xmax])
    1836         subplot.set_ylim([ymin,ymax])
     2026        subplot.set_ylim([ymin, ymax])
    18372027
    18382028        locator_options=dict(nbins=9,steps=[1,2,5,10],integer=ticks_integer)
    18392029
    1840 
    18412030        if axes is None:
    18422031            axes = self._show_axes
    18432032
     
    18522041            # the default one to see if we like it better.
    18532042
    18542043            (subplot, x_locator, y_locator,
    1855                     x_formatter, y_formatter) = self._matplotlib_tick_formatter(
    1856                             subplot, locator_options=locator_options,
     2044                x_formatter, y_formatter) = self._matplotlib_tick_formatter(
     2045                            subplot, base=(basex, basey),
     2046                            locator_options=locator_options,
     2047                            scale=(xscale, yscale),
    18572048                            tick_formatter=tick_formatter, ticks=ticks,
    18582049                            xmax=xmax, xmin=xmin, ymax=ymax, ymin=ymin)
    18592050
    18602051            subplot.set_frame_on(True)
    1861             if axes:
    1862                 if ymin<=0 and ymax>=0:
     2052            if axes and xscale == 'linear' and yscale == 'linear':
     2053                if (ymin<=0 and ymax>=0) or (ymax<=0 and ymin>=0):
    18632054                    subplot.axhline(color=self._axes_color,
    18642055                                    linewidth=self._axes_width)
    1865                 if xmin<=0 and xmax>=0:
     2056                if (xmin<=0 and xmax>=0) or (xmax<=0 and xmin>=0):
    18662057                    subplot.axvline(color=self._axes_color,
    18672058                                    linewidth=self._axes_width)
    18682059
    18692060        elif axes:
    18702061            ymiddle=False
    18712062            xmiddle=False
    1872             if xmin>0:
     2063            # Note that the user may specify a custom xmin and xmax which
     2064            # flips the axis horizontally. Hence we need to check for both
     2065            # the possibilities in the if statements below. Similar
     2066            # comments hold for ymin and ymax.
     2067            if xscale == 'log':
     2068                if xmax > xmin:
     2069                    subplot.spines['right'].set_visible(False)
     2070                    subplot.spines['left'].set_position(('outward',10))
     2071                    subplot.yaxis.set_ticks_position('left')
     2072                    subplot.yaxis.set_label_position('left')
     2073                    yaxis='left'
     2074                elif xmax < xmin:
     2075                    subplot.spines['left'].set_visible(False)
     2076                    subplot.spines['right'].set_position(('outward',10))
     2077                    subplot.yaxis.set_ticks_position('right')
     2078                    subplot.yaxis.set_label_position('right')
     2079                    yaxis='right'
     2080            elif (xmin > 0 and xmax > xmin) or (xmax > 0 and xmin > xmax):
    18732081                subplot.spines['right'].set_visible(False)
    18742082                subplot.spines['left'].set_position(('outward',10))
    18752083                subplot.yaxis.set_ticks_position('left')
    18762084                subplot.yaxis.set_label_position('left')
    18772085                yaxis='left'
    1878             elif xmax<0:
     2086            elif (xmax < 0 and xmax > xmin) or (xmin < 0 and xmin > xmax):
    18792087                subplot.spines['left'].set_visible(False)
    18802088                subplot.spines['right'].set_position(('outward',10))
    18812089                subplot.yaxis.set_ticks_position('right')
     
    18892097                ymiddle=True
    18902098                yaxis='left'
    18912099
    1892             if ymin>0:
     2100            if yscale == 'log':
     2101                if ymax > ymin:
     2102                    subplot.spines['top'].set_visible(False)
     2103                    subplot.spines['bottom'].set_position(('outward',10))
     2104                    subplot.xaxis.set_ticks_position('bottom')
     2105                    subplot.xaxis.set_label_position('bottom')
     2106                    xaxis='bottom'
     2107                elif ymax < ymin:
     2108                    subplot.spines['bottom'].set_visible(False)
     2109                    subplot.spines['top'].set_position(('outward',10))
     2110                    subplot.xaxis.set_ticks_position('top')
     2111                    subplot.xaxis.set_label_position('top')
     2112                    xaxis='top'
     2113            elif (ymin > 0 and ymax > ymin) or (ymax > 0 and ymin > ymax):
    18932114                subplot.spines['top'].set_visible(False)
    18942115                subplot.spines['bottom'].set_position(('outward',10))
    18952116                subplot.xaxis.set_ticks_position('bottom')
    18962117                subplot.xaxis.set_label_position('bottom')
    18972118                xaxis='bottom'
    1898             elif ymax<0:
     2119            elif (ymax < 0 and ymax > ymin) or (ymin < 0 and ymin > ymax):
    18992120                subplot.spines['bottom'].set_visible(False)
    19002121                subplot.spines['top'].set_position(('outward',10))
    19012122                subplot.xaxis.set_ticks_position('top')
     
    19142135            # the default one to see if we like it better.
    19152136
    19162137            (subplot, x_locator, y_locator,
    1917                     x_formatter, y_formatter) = self._matplotlib_tick_formatter(
    1918                             subplot, locator_options=locator_options,
     2138                x_formatter, y_formatter) = self._matplotlib_tick_formatter(
     2139                            subplot, base=(basex, basey),
     2140                            locator_options=locator_options,
     2141                            scale=(xscale, yscale),
    19192142                            tick_formatter=tick_formatter, ticks=ticks,
    19202143                            xmax=xmax, xmin=xmin, ymax=ymax, ymin=ymin)
    19212144
     
    19372160            #                     t.set_markersize(4)
    19382161
    19392162            # Make the zero tick labels disappear if the axes cross
    1940             # inside the picture
    1941             if xmiddle and ymiddle:
     2163            # inside the picture, but only if log scale is not used
     2164            if (xmiddle and ymiddle and xscale == 'linear' and
     2165                yscale == 'linear'):
    19422166                from sage.plot.plot import SelectiveFormatter
    19432167                subplot.yaxis.set_major_formatter(SelectiveFormatter(
    1944                     subplot.yaxis.get_major_formatter(),skip_values=[0]))
     2168                    subplot.yaxis.get_major_formatter(), skip_values=[0]))
    19452169                subplot.xaxis.set_major_formatter(SelectiveFormatter(
    1946                     subplot.xaxis.get_major_formatter(),skip_values=[0]))
     2170                    subplot.xaxis.get_major_formatter(), skip_values=[0]))
    19472171
    19482172        else:
    19492173            for spine in subplot.spines.values():
     
    19562180
    19572181        if frame or axes:
    19582182            # Make minor tickmarks, unless we specify fixed ticks or no ticks
    1959             from matplotlib.ticker import AutoMinorLocator, FixedLocator, NullLocator
     2183            # We do this change only on linear scale, otherwise matplotlib
     2184            # errors out with a memory error.
     2185            from matplotlib.ticker import (AutoMinorLocator, FixedLocator,
     2186                    LogLocator, NullLocator)
    19602187            if isinstance(x_locator, (NullLocator, FixedLocator)):
    19612188                subplot.xaxis.set_minor_locator(NullLocator())
    1962             else:
     2189            elif xscale == 'linear':
    19632190                subplot.xaxis.set_minor_locator(AutoMinorLocator())
     2191            else: # log scale
     2192                from sage.misc.misc import srange
     2193                base_inv = 1.0/basex
     2194                subs = map(float, srange(2*base_inv, 1, base_inv))
     2195                subplot.xaxis.set_minor_locator(LogLocator(base=basex,
     2196                                                           subs=subs))
    19642197            if isinstance(y_locator, (NullLocator, FixedLocator)):
    19652198                subplot.yaxis.set_minor_locator(NullLocator())
    1966             else:
     2199            elif yscale == 'linear':
    19672200                subplot.yaxis.set_minor_locator(AutoMinorLocator())
    1968 
    1969             ticklabels=subplot.xaxis.get_majorticklabels() + \
    1970                 subplot.xaxis.get_minorticklabels() + \
    1971                 subplot.yaxis.get_majorticklabels() + \
    1972                 subplot.yaxis.get_minorticklabels()
    1973             for ticklabel in ticklabels:
    1974                 ticklabel.set_fontsize(self._fontsize)
    1975                 ticklabel.set_color(self._tick_label_color)
    1976 
    1977             ticklines=subplot.xaxis.get_majorticklines() + \
    1978                 subplot.xaxis.get_minorticklines() + \
    1979                 subplot.yaxis.get_majorticklines() + \
    1980                 subplot.yaxis.get_minorticklines()
    1981             for tickline in ticklines:
    1982                 tickline.set_color(self._axes_color)
     2201            else: # log scale
     2202                from sage.misc.misc import srange
     2203                base_inv = 1.0/basey
     2204                subs = map(float, srange(2*base_inv, 1, base_inv))
     2205                subplot.yaxis.set_minor_locator(LogLocator(base=basey,
     2206                                                           subs=subs))
     2207
     2208            # Set the color and fontsize of ticks
     2209            figure.get_axes()[0].tick_params(color=self._axes_color,
     2210                    labelcolor=self._tick_label_color,
     2211                    labelsize=self._fontsize, which='both')
    19832212
    19842213
    19852214        if gridlines is not None: