Hi. A couple of questions about `scatter`:

Q1
====

The bounding box `axes.dataLim` increases in size when calling scatter(x, y) compared to plot(x, y) (for the same x, y data, of course). I think this change is due to the size of the markers being added to the data limits (not sure if this is true). Is there an easy way to get the same data limits I would have for a call to `plot`?

Q2 
==== 

Is there a way to get the data from the axes of a scatter plot?

Initially I thought I could get it with:

>>> for collection in ax.collections:
>>>     for path in collection._paths:
>>>         print path.vertices

But this seems to be the path that draws the scatter markers. Any ideas?

Frame Class 
==========

Finally, if anyone is interested, I'm playing around with a Frame class for `axes.frame`. This class adds customizable axes frames similar to the topic of this thread:
In this older thread, the SAGE axes frames were criticized for not being flexible enough. I've tried to make this class as general as possible (within my ability:).  As an example of the flexibility of this Frame class, I've added some Tufte-style frames similar to:

To the developers on this thread: If there's anything I could do to make the attached Frame class more flexible (and more suitable for possible inclusion into MPL), I'd be happy to get some feedback.

Current Limitations:
================
* the frame can only be placed on the borders of the axes (mainly because I don't know how to move the tickers anywhere else).
* RangeFrame only works with linear data (I'm not sure how to use the `axes.transScale` to properly transform the data)
* RangeFrame and DashDotFrame don't work properly with `scatter` (because of the questions in this post).

The frame class itself isn't too long, but I got a little carried away adding in extra crap.

Sorry for the long, rambling email.;)
-Tony

#!/usr/bin/env python
"""
Frame classes for customizing frame borders that surround the plot axes.
"""
import numpy as np
from numpy.random import rand

import matplotlib.axes as maxes
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import matplotlib.artist as martist
import matplotlib.collections as col
import matplotlib.projections as projections


class Frame(martist.Artist):
    """Draw frame along the edges of the axes patch.
    
    Frame position can be controlled upon initialization or by setting
    `frame_position` property with a list of positions
        ['left', 'right', 'top', 'bottom' | 'all']
    """
    _position_list = ('left', 'right', 'top', 'bottom')
    def __init__(self, axes, positions=('left', 'bottom'), **kwargs):
        """
        `positions` is a list of strings of frame positions to plot.
            ['left', 'right', 'top', 'bottom' | 'all']
        """
        super(Frame, self).__init__()
        # TODO: allow more keyword configuration
        self.axes = axes
        
        rc = plt.rcParams
        self.color = kwargs.pop('color', rc['axes.edgecolor'])
        self.linewidth = kwargs.pop('linewidth', rc['axes.linewidth'])
        self.linestyle = kwargs.pop('linestyle', 'solid')
        self.frame_position = positions

    def get_data(self):
        """Convenience method returns tuple of (x, y) data in `self.axes`"""
        x, y = [], []
        ax = self.axes
        for artist in (ax.lines, ax.patches):
            if not artist == []:
                x.append(np.concatenate([a.get_xdata() for a in artist]))
                y.append(np.concatenate([a.get_ydata() for a in artist]))
        # TODO: get scatter data from ax.collections
        return (np.concatenate(x), np.concatenate(y))

    def _set_frame_position(self, positions):
        """Set positions where frame will be drawn.

        `positions` is a list of strings of frame positions to plot.
            ['left', 'right', 'top', 'bottom' | 'all']
        """
        self._frame_on = self._frame_dict_from(positions)

    def _get_frame_position(self):
        return [p for p in self._position_list if self._frame_on[p]]

    # xposition tuples turn on frame for (bottom, top)
    _xposition_pairs = {(True, False): 'bottom', (False, True): 'top',
                        (True, True): 'both', (False, False): 'none'}
    def _get_xposition(self, frame_on=None):
        """Returns position that matches `XAxis.set_ticks_position` inputs.

        `frame_on` is a dict that matches frame positions with bools.
        """
        if frame_on is None:
            frame_on = self._frame_on
        return self._xposition_pairs[(frame_on['bottom'], frame_on['top'])]

    # yposition tuples turn on frame for (left, right)
    _yposition_pairs = {(True, False): 'left', (False, True): 'right',
                        (True, True): 'both', (False, False): 'none'}
    def _get_yposition(self, frame_on=None):
        """Returns position that matches `YAxis.set_ticks_position` inputs.

        `frame_on` is a dict that matches frame positions with bools.
        """
        if frame_on is None:
            frame_on = self._frame_on
        return self._yposition_pairs[(frame_on['left'], frame_on['right'])]

    def _frame_dict_from(self, positions):
        """Parse `positions` and return xposition, yposition tuple

        `positions` is a list of strings of frame positions to plot.
            ['left', 'right', 'top', 'bottom' | 'all']
        """
        frame_dict = dict.fromkeys(self._position_list, False)
        
        if 'all' in positions:
            frame_dict = dict.fromkeys(self._position_list, True)
        else:
            for position in positions:
                frame_dict[position] = True
        return frame_dict

    def _set_ticks(self):
        """Overide this method to customize tick positioning."""
        # Draw ticks on axes only where a frame is drawn
        self.axes.xaxis.set_ticks_position(self._get_xposition())
        self.axes.yaxis.set_ticks_position(self._get_yposition())

    _frame_lines = dict(bottom=[(0., 0.), (1., 0.)], top=[(0., 1.), (1., 1.)],
                        left=[(0., 0.), (0., 1.)], right=[(1., 0.), (1., 1.)])
    def _make_frame(self):
        """Get axis frame specified by `self._frame_on`."""
        lines = [self._frame_lines[p] for p in self._position_list
                                      if self._frame_on[p]]
        frame_lines = col.LineCollection(segments=lines,
                                         linewidths=[self.linewidth],
                                         colors=[self.color])
        frame_lines.set_transform(self.axes.transAxes)
        return frame_lines

    def draw(self, renderer):
        if not self.get_visible():
            return
        self._set_ticks()
        frame = self._make_frame()
        frame.draw(renderer)

    frame_position = property(_get_frame_position, _set_frame_position)


class RangeFrame(Frame):
    """Range frame from Edward Tufte's book VDQI p.131.

    The x-axis frame begins and ends at the minimum and maximum x-values of
    the data, resprectively. Similarly, for the y-axis.
    """
    def __init__(self, *args, **kwargs):
        super(RangeFrame, self).__init__(*args, **kwargs)
        self.show_extrema_lines = kwargs.pop('extrema', False)

    def _make_frame(self):
        """Get frame that extends to the limits of the data"""
        t = self.axes.transLimits
        (xminf, yminf), (xmaxf, ymaxf) = t.transform(self.axes.dataLim)
        frame_lines = dict(bottom=[(xminf, 0.), (xmaxf, 0.)],
                           top=[(xminf, 1.), (xmaxf, 1.)],
                           left=[(0., yminf), (0., ymaxf)],
                           right=[(1., yminf), (1., ymaxf)])
        lines = [frame_lines[p] for p in self._position_list
                                if self._frame_on[p]]
        linestyles = [self.linestyle] * len(lines)
        colors = [self.color] * len(lines)
        if self.show_extrema_lines:
            extrema_lines = self._get_extrema_lines()
            linestyles += ['dotted'] * len(extrema_lines)
            colors += ['gray'] * len(extrema_lines)
            lines += extrema_lines

        range_lines = col.LineCollection(segments=lines,
                                         linestyles=linestyles,
                                         linewidths=[self.linewidth],
                                         colors=colors)
        range_lines.set_transform(self.axes.transAxes)
        return range_lines

    def _get_extrema_lines(self):
        """Get lines that go vertically from x-axis to data with min and max
        x values, and horizontally from y_axis to data with min and max y
        values.

        This method is mainly intended for debuging.
        """
        x, y = self.get_data()
        xy = np.array([x, y]).transpose()
        xy = self.axes.transLimits.transform(xy)
        x = xy[:, 0]
        y = xy[:, 1]

        lines = []
        for extremum in (x.min(), x.max()):
            x0 = float(x[x == extremum])
            y0 = float(y[x == extremum])
            lines.append([(x0, 0), (x0, y0)])

        for extremum in (y.min(), y.max()):
            x0 = float(x[y == extremum])
            y0 = float(y[y == extremum])
            lines.append([(0, y0), (x0, y0)])
        return lines


class DashDotFrame(Frame):
    """Dash dot plot frame Edward Tufte's book VDQI p.133.

    x and y coordinates of data are projected on the x- and y-axis as minor
    ticks. By default, no frame lines are plotted, only ticks. However, you
    can override this default, by specifying the `positions` keyword on init.
    """
    def __init__(self, axes, tick_positions=('left', 'bottom'), positions=(),
                 majorsize=5, minorsize=3, *args, **kwargs):
        """
        `tick positions` is a list of strings of tick frame positions to plot.
            ['left', 'right', 'top', 'bottom' | 'all']
        """
        super(DashDotFrame, self).__init__(axes, positions=positions, 
                                           *args, **kwargs)
        self.tframe_position = tick_positions
        self.majorsize = majorsize
        self.minorsize = minorsize
    
    def _set_tframe_position(self, positions):
        """Set positions where tick frame will be drawn.

        `positions` is a list of strings of frame positions to plot.
            ['left', 'right', 'top', 'bottom' | 'all']
        """
        self._tick_frame_on = self._frame_dict_from(positions)
        
    def _get_tframe_position(self):
        return [p for p in self._position_list if self._tick_frame_on[p]]
        
    def _set_ticks(self):
        """Project x, y data as minor ticks on the x- and y-axes."""
        x, y = self.get_data()
        xaxis = self.axes.xaxis
        yaxis = self.axes.yaxis
        # add minor ticks for every data point
        xaxis.set_minor_locator(ticker.FixedLocator(x))
        yaxis.set_minor_locator(ticker.FixedLocator(y))

        xticks_position = self._get_xposition(self._tick_frame_on)
        yticks_position = self._get_yposition(self._tick_frame_on)
        xaxis.set_ticks_position(xticks_position)
        yaxis.set_ticks_position(yticks_position)

        # TODO: major and minor ticks on opposite sides of the axis
        majorlines = (xaxis.get_majorticklines() + yaxis.get_majorticklines())
        minorlines = (xaxis.get_minorticklines() + yaxis.get_minorticklines())
        for line in majorlines:
            line.set_markersize(self.majorsize)
        for line in minorlines:
            line.set_markersize(self.minorsize)
    tframe_position = property(_get_tframe_position, _set_tframe_position)


class FrameAxes(maxes.Axes):
    """Override Axes so that `frame` attribute is a custom FrameArtist
    """
    name = 'frameaxes'

    def cla(self):
        """Override default Axes.frame and clear patch edgecolor"""
        if not hasattr(self, '_frame'):
            self._frame = Frame(self)
        super(FrameAxes, self).cla()
        self.patch.set_edgecolor('none')
        # retrieve `_frame` because Axes.cla() overwrites `frame`
        self.frame = self._frame

    def set_frame(self, frame_artist, **kwargs):
        """Set artist to draw axes frame.

        `frame_artist` should be subclass of `FrameArtist`.
        """
        # save frame artist as `_frame` because Axes.cla() overwrites `frame`
        self.frame = self._frame = frame_artist(self, **kwargs)

    def get_frame(self):
        return self.frame
projections.register_projection(FrameAxes)


def plot_test():
    """Plot comparison of some custom frames"""
    plt.rcParams['xtick.direction'] = 'out'
    plt.rcParams['ytick.direction'] = 'out'

    frames_test = (Frame, Frame, RangeFrame, DashDotFrame)
    positions_test = (['all'], None, None, None)
    names = ('normal frame', 'L frame', 'range frame',  'dash-dot frame')
    sub_pos = (221, 222, 223, 224)

    x = rand(10)
    y = rand(10)
    test = zip(frames_test, positions_test, names, sub_pos)
    for frametype, positions, name, sub in test:
        ax = plt.subplot(sub, projection='frameaxes')
        ax.set_frame(frametype)
        if frametype is RangeFrame:
            ax.frame.show_extrema_lines = True
        if frametype is DashDotFrame:
            ax.frame.tframe_position = ['top', 'right']
        if positions is not None:
            ax.frame.frame_position = positions
        ax.plot(x, y, 'o')
        if False: # debug scatter
            print 'Before Scatter:\n', ax.dataLim
            ax.scatter(x, y)
            print 'After Scatter:\n', ax.dataLim
        ax.set_ylabel(name)
        ax.axis([0, 1, 0, 1])
    fig = plt.gcf()
    # remove background color to make the frames stand out
    fig.set_facecolor('w')
    fig.subplots_adjust(hspace=0.5, wspace=0.5)
    plt.show()

if __name__ == '__main__':
    plot_test()

-------------------------------------------------------------------------
Check out the new SourceForge.net Marketplace.
It's the best place to buy or sell services for
just about anything Open Source.
http://sourceforge.net/services/buy/index.php
_______________________________________________
Matplotlib-users mailing list
Matplotlib-users@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-users

Reply via email to