From 0fd204902b0bd3c3f7ade93c6e512f77abf0fec2 Mon Sep 17 00:00:00 2001 From: Damon McDougall Date: Thu, 1 Mar 2012 21:29:08 -0500 Subject: [PATCH 1/2] Added ability to plot callables Plotting functionality for Python callables, ala Matlab's 'fplot' function. A fixed step-size is used, but is recalculated to fit the viewport on zooming and panning. This has the effect of 'discovering' more of the function when the user zooms in on the plot. Only one callable is supported so far, it would be desirable to support an array/list of callables and plot them all simultaneously. --- boilerplate.py | 1 + examples/pylab_examples/fplot_demo.py | 10 ++++ lib/matplotlib/axes/_axes.py | 5 ++ lib/matplotlib/fplot.py | 78 +++++++++++++++++++++++++++ lib/matplotlib/pylab.py | 1 + lib/matplotlib/pyplot.py | 18 +++++++ 6 files changed, 113 insertions(+) create mode 100644 examples/pylab_examples/fplot_demo.py create mode 100644 lib/matplotlib/fplot.py 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/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) From af49455f717d6ba00cc420d4cb157d88dc91f579 Mon Sep 17 00:00:00 2001 From: Damon McDougall Date: Sun, 30 Jun 2013 21:55:16 -0500 Subject: [PATCH 2/2] Add an entry to whats_new and CHANGELOG --- CHANGELOG | 5 +++++ doc/users/whats_new.rst | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) 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/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