diff --git a/CHANGELOG b/CHANGELOG index 345d5f8f295f..46eba918ffc2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +2013-06-30 Add support for plotting callables via fplot + Now one can use fplot and pass a callable to plot. Plotted + coordinates are dynamically managed based on the bounds of the + current viewport - Damon McDougall + 2013-06-26 Refactored the axes module: the axes module is now a folder, containing the following submodule: - _subplots.py, containing all the subplots helper methods diff --git a/boilerplate.py b/boilerplate.py index b7839c72beeb..6d6c3c1cab35 100644 --- a/boilerplate.py +++ b/boilerplate.py @@ -112,6 +112,7 @@ def boilerplate_gen(): 'fill', 'fill_between', 'fill_betweenx', + 'fplot', 'hexbin', 'hist', 'hist2d', diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst index 2017452f6cbf..73e38662aa4b 100644 --- a/doc/users/whats_new.rst +++ b/doc/users/whats_new.rst @@ -18,6 +18,26 @@ revision, see the :ref:`github-stats`. .. contents:: Table of Contents :depth: 3 +.. _whats-new-1-4: + +new in matplotlib-1.4 +===================== + +New plotting features +--------------------- + +Callable plotting via fplot +``````````````````````````` + +Damon McDougall added :func:`matplotlib.pyplot.fplot` which offers the +flexibilty of plotting callables. This is similar to Matlab's fplot. Zooming +and panning dynamically adjusts the plotted coordinates, allowing the +possibilty to explore parts of the function that are less well behaved. An +attempt has been made to detect singularities in functions as well. This +feature is experimental and is likely to change in the future. + +.. plot:: mpl_examples/pylab_examples/fplot_demo.py + .. _whats-new-1-3: new in matplotlib-1.3 diff --git a/examples/pylab_examples/fplot_demo.py b/examples/pylab_examples/fplot_demo.py new file mode 100644 index 000000000000..0368a73009ed --- /dev/null +++ b/examples/pylab_examples/fplot_demo.py @@ -0,0 +1,10 @@ +import numpy as np +import matplotlib.pyplot as plt + +# Set up figure +fig, ax = plt.subplots() + +# Plot function +fp = ax.fplot(np.tan, [0, 2]) +ax.set_xlim([1, 2]) +plt.show() diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 96c885b63362..f96e4d8e33e5 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -15,6 +15,7 @@ import matplotlib.contour as mcontour import matplotlib.dates as _ # <-registers a date unit converter from matplotlib import docstring +import matplotlib.fplot as mfplot import matplotlib.image as mimage import matplotlib.legend as mlegend import matplotlib.lines as mlines @@ -3762,6 +3763,10 @@ def streamplot(self, x, y, u, v, density=1, linewidth=None, color=None, return stream_container streamplot.__doc__ = mstream.streamplot.__doc__ + def fplot(self, *args, **kwargs): + return mfplot.fplot(self, *args, **kwargs) + fplot.__doc__ = mfplot.fplot.__doc__ + @docstring.dedent_interpd def barbs(self, *args, **kw): """ diff --git a/lib/matplotlib/fplot.py b/lib/matplotlib/fplot.py new file mode 100644 index 000000000000..0277bb333069 --- /dev/null +++ b/lib/matplotlib/fplot.py @@ -0,0 +1,78 @@ +""" +1D Callable function plotting. +""" + +import numpy as np + + +__all__ = ['fplot'] + + +class FPlot(object): + def __init__(self, axes, *args, **kwargs): + self._process_args(*args, **kwargs) + + self.axes = axes + self.axes.set_autoscale_on(False) + + self.n = kwargs.pop('res', 1000) + + self.x = np.linspace(self.limits[0], self.limits[1], self.n) + self.f_vals = np.asarray([self.f(xi) for xi in self.x]) + + self.fline, = self.axes.plot(self.x, self.f_vals) + self._process_singularities() + self.axes.set_xlim([self.x[0], self.x[-1]]) + mn, mx = np.nanmin(self.f_vals), np.nanmax(self.f_vals) + self.axes.set_ylim([mn, mx]) + + axes.callbacks.connect('xlim_changed', self._update) + axes.callbacks.connect('ylim_changed', self._update) + + def _process_args(self, *args, **kwargs): + # TODO: Check f is callable. If not callable, support array of callables. + # TODO: Support y limits? + self.f = args[0] + self.limits = args[1] + + def _update(self, axes): + # bounds is (l, b, w, h) + bounds = axes.viewLim.bounds + self.x = np.linspace(bounds[0], bounds[0] + bounds[2], self.n) + self.f_vals = [self.f(xi) for xi in self.x] + self._process_singularities() + self.fline.set_data(self.x, self.f_vals) + self.axes.figure.canvas.draw_idle() + + def _process_singularities(self): + # Note: d[i] == f_vals[i+1] - f_vals[i] + d = np.diff(self.f_vals) + + # 80% is arbitrary. Perhaps more control could be offered here? + badness = np.where(d > 0.80 * self.axes.viewLim.bounds[3])[0] + + # We don't draw the signularities + for b in badness: + self.f_vals[b] = np.nan + self.f_vals[b + 1] = np.nan + + +def fplot(ax, *args, **kwargs): + """ + Plots a callable function f. + + Parameters + ---------- + f : Python callable, the function that is to be plotted. + limits : 2-element array or list of limits: [xmin, xmax]. The function f + is to to be plotted between xmin and xmax. + + Returns + ------- + lines : `matplotlib.collections.LineCollection` + Line collection with that describes the function *f* between xmin + and xmax. all streamlines as a series of line segments. + """ + if not ax._hold: + ax.cla() + return FPlot(ax, *args, **kwargs) diff --git a/lib/matplotlib/pylab.py b/lib/matplotlib/pylab.py index bf8b97ba5eb7..f0ac9e7e1af8 100644 --- a/lib/matplotlib/pylab.py +++ b/lib/matplotlib/pylab.py @@ -42,6 +42,7 @@ figure - create or change active figure fill - make filled polygons findobj - recursively find all objects matching some criteria + fplot - a method to plot callables gca - return the current axes gcf - return the current figure gci - get the current image, or None diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index e650d13b0573..0b31b18e1cc4 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2779,6 +2779,24 @@ def fill_betweenx(y, x1, x2=0, where=None, hold=None, **kwargs): return ret +# This function was autogenerated by boilerplate.py. Do not edit as +# changes will be lost +@_autogen_docstring(Axes.fplot) +def fplot(*args, **kwargs): + ax = gca() + # allow callers to override the hold state by passing hold=True|False + washold = ax.ishold() + hold = kwargs.pop('hold', None) + if hold is not None: + ax.hold(hold) + try: + ret = ax.fplot(*args, **kwargs) + draw_if_interactive() + finally: + ax.hold(washold) + + return ret + # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.hexbin)