From: <md...@us...> - 2007-12-14 20:08:47
|
Revision: 4733 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=4733&view=rev Author: mdboom Date: 2007-12-14 12:07:59 -0800 (Fri, 14 Dec 2007) Log Message: ----------- First pass at symmetrical log plots. Expose xscale() and yscale() through pyplot. Modified Paths: -------------- branches/transforms/lib/matplotlib/axes.py branches/transforms/lib/matplotlib/pyplot.py branches/transforms/lib/matplotlib/scale.py branches/transforms/lib/matplotlib/ticker.py Added Paths: ----------- branches/transforms/examples/symlog_demo.py Added: branches/transforms/examples/symlog_demo.py =================================================================== --- branches/transforms/examples/symlog_demo.py (rev 0) +++ branches/transforms/examples/symlog_demo.py 2007-12-14 20:07:59 UTC (rev 4733) @@ -0,0 +1,29 @@ +#!/usr/bin/env python +from pylab import * + +dt = 1.0 +x = arange(-50.0, 50.0, dt) +y = arange(0, 100.0, dt) + +subplot(311) +plot(x, y) +xscale('symlog') +ylabel('symlogx') +grid(True) +gca().xaxis.grid(True, which='minor') # minor grid on too + +subplot(312) +plot(y, x) +yscale('symlog') +ylabel('symlogy') + + +subplot(313) +plot(x, npy.sin(x / 3.0)) +xscale('symlog') +yscale('symlog') +grid(True) +ylabel('symlog both') + +savefig('log_demo') +show() Property changes on: branches/transforms/examples/symlog_demo.py ___________________________________________________________________ Name: svn:executable + * Modified: branches/transforms/lib/matplotlib/axes.py =================================================================== --- branches/transforms/lib/matplotlib/axes.py 2007-12-14 16:28:34 UTC (rev 4732) +++ branches/transforms/lib/matplotlib/axes.py 2007-12-14 20:07:59 UTC (rev 4733) @@ -741,8 +741,6 @@ self.xaxis.cla() self.yaxis.cla() - self.set_xscale('linear') - self.set_yscale('linear') self.ignore_existing_data_limits = True self.callbacks = cbook.CallbackRegistry(('xlim_changed', 'ylim_changed')) @@ -768,6 +766,8 @@ self.collections = [] # collection.Collection instances self._autoscaleon = True + self.set_xscale('linear') + self.set_yscale('linear') self.grid(self._gridOn) props = font_manager.FontProperties(size=rcParams['axes.titlesize']) @@ -1654,6 +1654,7 @@ ", ".join(mscale.get_scale_names())) return self.xaxis.get_scale() + # MGDTODO: Update docstring def set_xscale(self, value, **kwargs): """ SET_XSCALE(value) @@ -1673,6 +1674,7 @@ ACCEPTS: [%(scale)s] """ % {'scale': ' | '.join([repr(x) for x in mscale.get_scale_names()])} self.xaxis.set_scale(value, **kwargs) + self.autoscale_view() self._update_transScale() def get_xticks(self, minor=False): @@ -1833,6 +1835,7 @@ ACCEPTS: %(scale)s """ % {'scale': ' | '.join([repr(x) for x in mscale.get_scale_names()])} self.yaxis.set_scale(value, **kwargs) + self.autoscale_view() self._update_transScale() def get_yticks(self, minor=False): @@ -1992,6 +1995,7 @@ lim = self.viewLim.frozen(), trans = self.transData.frozen(), trans_inverse = self.transData.inverted().frozen(), + bbox = self.bbox.frozen(), x = x, y = y ) @@ -2044,9 +2048,11 @@ p = self._pan_start dx = x - p.x dy = y - p.y + if dx == 0 and dy == 0: + return if button == 1: dx, dy = format_deltas(key, dx, dy) - result = self.bbox.frozen().translated(-dx, -dy) \ + result = p.bbox.translated(-dx, -dy) \ .transformed(p.trans_inverse) elif button == 3: try: Modified: branches/transforms/lib/matplotlib/pyplot.py =================================================================== --- branches/transforms/lib/matplotlib/pyplot.py 2007-12-14 16:28:34 UTC (rev 4732) +++ branches/transforms/lib/matplotlib/pyplot.py 2007-12-14 20:07:59 UTC (rev 4733) @@ -12,6 +12,7 @@ from matplotlib.axes import Axes from matplotlib.projections import PolarAxes from matplotlib import mlab # for csv2rec in plotfile +from matplotlib.scale import get_scale_names from matplotlib import cm from matplotlib.cm import get_cmap @@ -726,8 +727,32 @@ return ret +# MGDTODO: Update docstring +def xscale(*args, **kwargs): + """ + SET_XSCALE(value) + Set the xscaling: %(scale)s + """ % {'scale': ' | '.join([repr(x) for x in get_scale_names()])} + ax = gca() + ret = ax.set_xscale(*args, **kwargs) + draw_if_interactive() + return ret + +# MGDTODO: Update docstring +def yscale(*args, **kwargs): + """ + SET_YSCALE(value) + + Set the yscaling: %(scale)s + """ % {'scale': ' | '.join([repr(x) for x in get_scale_names()])} + ax = gca() + ret = ax.set_yscale(*args, **kwargs) + draw_if_interactive() + return ret + + def xticks(*args, **kwargs): """ Set/Get the xlimits of the current ticklocs, labels Modified: branches/transforms/lib/matplotlib/scale.py =================================================================== --- branches/transforms/lib/matplotlib/scale.py 2007-12-14 16:28:34 UTC (rev 4732) +++ branches/transforms/lib/matplotlib/scale.py 2007-12-14 20:07:59 UTC (rev 4733) @@ -3,7 +3,7 @@ MaskedArray = ma.MaskedArray from ticker import NullFormatter, ScalarFormatter, LogFormatterMathtext -from ticker import NullLocator, LogLocator, AutoLocator +from ticker import NullLocator, LogLocator, AutoLocator, SymmetricalLogLocator from transforms import Transform, IdentityTransform class ScaleBase(object): @@ -12,10 +12,10 @@ def limit_range_for_scale(self, vmin, vmax, minpos): return vmin, vmax - + class LinearScale(ScaleBase): name = 'linear' - + def __init__(self, axis, **kwargs): pass @@ -24,7 +24,7 @@ axis.set_major_formatter(ScalarFormatter()) axis.set_minor_locator(NullLocator()) axis.set_minor_formatter(NullFormatter()) - + def get_transform(self): return IdentityTransform() @@ -33,7 +33,7 @@ if mask.any(): return ma.MaskedArray(a, mask=mask) return a - + class LogScale(ScaleBase): name = 'log' @@ -41,13 +41,14 @@ input_dims = 1 output_dims = 1 is_separable = True - + base = 10.0 + def transform(self, a): a = _mask_non_positives(a * 10.0) if isinstance(a, MaskedArray): return ma.log10(a) return npy.log10(a) - + def inverted(self): return LogScale.InvertedLog10Transform() @@ -55,7 +56,8 @@ input_dims = 1 output_dims = 1 is_separable = True - + base = 10.0 + def transform(self, a): return ma.power(10.0, a) / 10.0 @@ -66,13 +68,14 @@ input_dims = 1 output_dims = 1 is_separable = True - + base = 2.0 + def transform(self, a): a = _mask_non_positives(a * 2.0) if isinstance(a, MaskedArray): return ma.log2(a) return npy.log2(a) - + def inverted(self): return LogScale.InvertedLog2Transform() @@ -80,7 +83,8 @@ input_dims = 1 output_dims = 1 is_separable = True - + base = 2.0 + def transform(self, a): return ma.power(2.0, a) / 2.0 @@ -91,13 +95,14 @@ input_dims = 1 output_dims = 1 is_separable = True - + base = npy.e + def transform(self, a): a = _mask_non_positives(a * npy.e) if isinstance(a, MaskedArray): return ma.log(a) return npy.log(a) - + def inverted(self): return LogScale.InvertedNaturalLogTransform() @@ -105,54 +110,55 @@ input_dims = 1 output_dims = 1 is_separable = True - + base = npy.e + def transform(self, a): return ma.power(npy.e, a) / npy.e def inverted(self): return LogScale.Log2Transform() - + class LogTransform(Transform): input_dims = 1 output_dims = 1 is_separable = True - + def __init__(self, base): Transform.__init__(self) - self._base = base - + self.base = base + def transform(self, a): - a = _mask_non_positives(a * self._base) + a = _mask_non_positives(a * self.base) if isinstance(a, MaskedArray): - return ma.log10(a) / npy.log(self._base) - return npy.log(a) / npy.log(self._base) - + return ma.log(a) / npy.log(self.base) + return npy.log(a) / npy.log(self.base) + def inverted(self): - return LogScale.InvertedLogTransform(self._base) + return LogScale.InvertedLogTransform(self.base) class InvertedLogTransform(Transform): input_dims = 1 output_dims = 1 is_separable = True - + def __init__(self, base): Transform.__init__(self) - self._base = base + self.base = base def transform(self, a): - return ma.power(self._base, a) / self._base + return ma.power(self.base, a) / self.base def inverted(self): - return LogScale.LogTransform(self._base) + return LogScale.LogTransform(self.base) - + def __init__(self, axis, **kwargs): if axis.axis_name == 'x': base = kwargs.pop('basex', 10.0) - subs = kwargs.pop('subsx', []) + subs = kwargs.pop('subsx', None) else: base = kwargs.pop('basey', 10.0) - subs = kwargs.pop('subsy', []) + subs = kwargs.pop('subsy', None) if base == 10.0: self._transform = self.Log10Transform() @@ -163,25 +169,104 @@ else: self._transform = self.LogTransform(base) - self._base = base - self._subs = subs + self.base = base + self.subs = subs def set_default_locators_and_formatters(self, axis): - axis.set_major_locator(LogLocator(self._base)) - axis.set_major_formatter(LogFormatterMathtext(self._base)) - axis.set_minor_locator(LogLocator(self._base, self._subs)) + axis.set_major_locator(LogLocator(self.base)) + axis.set_major_formatter(LogFormatterMathtext(self.base)) + axis.set_minor_locator(LogLocator(self.base, self.subs)) axis.set_minor_formatter(NullFormatter()) - + def get_transform(self): return self._transform def limit_range_for_scale(self, vmin, vmax, minpos): return (vmin <= 0.0 and minpos or vmin, vmax <= 0.0 and minpos or vmax) - + +class SymmetricalLogScale(ScaleBase): + name = 'symlog' + + class SymmetricalLogTransform(Transform): + input_dims = 1 + output_dims = 1 + is_separable = True + + def __init__(self, base, linthresh): + Transform.__init__(self) + self.base = base + self.linthresh = linthresh + self._log_base = npy.log(base) + self._linadjust = (npy.log(linthresh) / self._log_base) / linthresh + + def transform(self, a): + sign = npy.sign(npy.asarray(a)) + masked = ma.masked_inside(a, -self.linthresh, self.linthresh, copy=False) + log = sign * ma.log(npy.abs(masked)) / self._log_base + if masked.mask.any(): + return npy.asarray(ma.where(masked.mask, + a * self._linadjust, + log)) + else: + return npy.asarray(log) + + def inverted(self): + return SymmetricalLogScale.InvertedSymmetricalLogTransform(self.base, self.linthresh) + + class InvertedSymmetricalLogTransform(Transform): + input_dims = 1 + output_dims = 1 + is_separable = True + + def __init__(self, base, linthresh): + Transform.__init__(self) + self.base = base + self.linthresh = linthresh + self._log_base = npy.log(base) + self._log_linthresh = npy.log(linthresh) / self._log_base + self._linadjust = linthresh / (npy.log(linthresh) / self._log_base) + + def transform(self, a): + return npy.where(a <= self._log_linthresh, + npy.where(a >= -self._log_linthresh, + a * self._linadjust, + -(npy.power(self.base, -a))), + npy.power(self.base, a)) + + def inverted(self): + return SymmetricalLogScale.SymmetricalLogTransform(self.base) + + def __init__(self, axis, **kwargs): + if axis.axis_name == 'x': + base = kwargs.pop('basex', 10.0) + linthresh = kwargs.pop('linthreshx', 2.0) + subs = kwargs.pop('subsx', None) + else: + base = kwargs.pop('basey', 10.0) + linthresh = kwargs.pop('linthreshy', 2.0) + subs = kwargs.pop('subsy', None) + + self._transform = self.SymmetricalLogTransform(base, linthresh) + + self.base = base + self.linthresh = linthresh + self.subs = subs + + def set_default_locators_and_formatters(self, axis): + axis.set_major_locator(SymmetricalLogLocator(self.get_transform())) + axis.set_major_formatter(LogFormatterMathtext(self.base)) + axis.set_minor_locator(SymmetricalLogLocator(self.get_transform(), self.subs)) + axis.set_minor_formatter(NullFormatter()) + + def get_transform(self): + return self._transform + + _scale_mapping = { - 'linear' : LinearScale, - 'log' : LogScale + 'linear' : LinearScale, + 'log' : LogScale, + 'symlog' : SymmetricalLogScale } def scale_factory(scale, axis, **kwargs): scale = scale.lower() @@ -190,7 +275,7 @@ if not _scale_mapping.has_key(scale): raise ValueError("Unknown scale type '%s'" % scale) - + return _scale_mapping[scale](axis, **kwargs) def get_scale_names(): Modified: branches/transforms/lib/matplotlib/ticker.py =================================================================== --- branches/transforms/lib/matplotlib/ticker.py 2007-12-14 16:28:34 UTC (rev 4732) +++ branches/transforms/lib/matplotlib/ticker.py 2007-12-14 20:07:59 UTC (rev 4733) @@ -131,14 +131,14 @@ def set_data_interval(self, vmin, vmax): self.dataLim.intervalx = vmin, vmax - + def set_axis(self, axis): self.axis = axis def create_dummy_axis(self): if self.axis is None: self.axis = self.DummyAxis() - + def set_view_interval(self, vmin, vmax): self.axis.set_view_interval(vmin, vmax) @@ -149,7 +149,7 @@ self.set_view_interval(vmin, vmax) self.set_data_interval(vmin, vmax) - + class Formatter(TickHelper): """ Convert the tick location to a string @@ -459,13 +459,18 @@ vmin, vmax = self.axis.get_view_interval() d = abs(vmax - vmin) b=self._base + if x == 0.0: + return '0' + sign = npy.sign(x) # only label the decades - fx = math.log(x)/math.log(b) + fx = math.log(abs(x))/math.log(b) isDecade = self.is_decade(fx) if not isDecade and self.labelOnlyBase: s = '' elif x>10000: s= '%1.0e'%x elif x<1: s = '%1.0e'%x else : s = self.pprint_val(x,d) + if sign == -1: + return '-%s' % s return s def format_data(self,value): @@ -516,8 +521,11 @@ self.verify_intervals() d = abs(self.viewInterval.span()) b=self._base + if x == 0: + return '0' + sign = npy.sign(x) # only label the decades - fx = math.log(x)/math.log(b) + fx = math.log(abs(x))/math.log(b) isDecade = self.is_decade(fx) if not isDecade and self.labelOnlyBase: s = '' #if 0: pass @@ -526,6 +534,8 @@ #elif x<1: s = '10^%d'%fx elif fx<1: s = '%1.0e'%fx else : s = self.pprint_val(fx,d) + if sign == -1: + return '-%s' % s return s @@ -538,22 +548,30 @@ 'Return the format for tick val x at position pos' b = self._base # only label the decades - fx = math.log(x)/math.log(b) + if x == 0: + return '$0$' + sign = npy.sign(x) + fx = math.log(abs(x))/math.log(b) isDecade = self.is_decade(fx) usetex = rcParams['text.usetex'] + if sign == -1: + sign_string = '-' + else: + sign_string = '' + if not isDecade and self.labelOnlyBase: s = '' elif not isDecade: if usetex: - s = r'$%d^{%.2f}$'% (b, fx) + s = r'$%s%d^{%.2f}$'% (sign_string, b, fx) else: - s = '$\mathdefault{%d^{%.2f}}$'% (b, fx) + s = '$\mathdefault{%s%d^{%.2f}}$'% (sign_string, b, fx) else: if usetex: - s = r'$%d^{%d}$'% (b, self.nearest_long(fx)) + s = r'$%s%d^{%d}$'% (sign_string, b, self.nearest_long(fx)) else: - s = r'$\mathdefault{%d^{%d}}$'% (b, self.nearest_long(fx)) + s = r'$\mathdefault{%s%d^{%d}}$'% (sign_string, b, self.nearest_long(fx)) return s @@ -928,13 +946,12 @@ if vmin <= 0.0: raise ValueError( "Data has no positive values, and therefore can not be log-scaled.") - + vmin = math.log(vmin)/math.log(b) vmax = math.log(vmax)/math.log(b) if vmax<vmin: vmin, vmax = vmax, vmin - ticklocs = [] numdec = math.floor(vmax)-math.ceil(vmin) @@ -949,9 +966,14 @@ while numdec/stride+1 > self.numticks: stride += 1 - for decadeStart in b**npy.arange(math.floor(vmin), - math.ceil(vmax)+stride, stride): - ticklocs.extend( subs*decadeStart ) + decades = npy.arange(math.floor(vmin), + math.ceil(vmax)+stride, stride) + if len(subs) > 1 or subs[0] != 1.0: + ticklocs = [] + for decadeStart in b**decades: + ticklocs.extend( subs*decadeStart ) + else: + ticklocs = b**decades return npy.array(ticklocs) @@ -979,6 +1001,80 @@ result = mtransforms.nonsingular(vmin, vmax) return result +class SymmetricalLogLocator(Locator): + """ + Determine the tick locations for log axes + """ + + def __init__(self, transform, subs=[1.0]): + """ + place ticks on the location= base**i*subs[j] + """ + self._transform = transform + self._subs = subs + self.numticks = 15 + + def _set_numticks(self): + self.numticks = 15 # todo; be smart here; this is just for dev + + def __call__(self): + 'Return the locations of the ticks' + b = self._transform.base + + vmin, vmax = self.axis.get_view_interval() + vmin, vmax = self._transform.transform_point((vmin, vmax)) + if vmax<vmin: + vmin, vmax = vmax, vmin + numdec = math.floor(vmax)-math.ceil(vmin) + + if self._subs is None: + if numdec>10: subs = npy.array([1.0]) + elif numdec>6: subs = npy.arange(2.0, b, 2.0) + else: subs = npy.arange(2.0, b) + else: + subs = npy.asarray(self._subs) + + stride = 1 + while numdec/stride+1 > self.numticks: + stride += 1 + + decades = npy.arange(math.floor(vmin), math.ceil(vmax)+stride, stride) + if len(subs) > 1 or subs[0] != 1.0: + ticklocs = [] + for decade in decades: + ticklocs.extend(subs * (npy.sign(decade) * b ** npy.abs(decade))) + else: + ticklocs = npy.sign(decades) * b ** npy.abs(decades) + return npy.array(ticklocs) + + def autoscale(self): + 'Try to choose the view limits intelligently' + b = self._transform.base + vmin, vmax = self.axis.get_data_interval() + if vmax<vmin: + vmin, vmax = vmax, vmin + + if not is_decade(abs(vmin), b): + if vmin < 0: + vmin = -decade_up(-vmin, b) + else: + vmin = decade_down(vmin, b) + if not is_decade(abs(vmax), b): + if vmax < 0: + vmax = -decade_down(-vmax, b) + else: + vmax = decade_up(vmax, b) + + if vmin == vmax: + if vmin < 0: + vmin = -decade_up(-vmin, b) + vmax = -decade_down(-vmax, b) + else: + vmin = decade_down(vmin, b) + vmax = decade_up(vmax, b) + result = mtransforms.nonsingular(vmin, vmax) + return result + class AutoLocator(MaxNLocator): def __init__(self): MaxNLocator.__init__(self, nbins=9, steps=[1, 2, 5, 10]) This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |