Esempio n. 1
0
    def __init__(self, x_type="linear", y_type="linear", range=None, **kwargs):
        # TODO: This is currently an implicit assumption, i.e. that the range
        # will be passed in to the constructor.  It would be impossible to
        # create the xmapper and ymapper otherwise.  However, this should be
        # changed so that the mappers get created or modified in response to
        # the .range attribute changing, instead of requiring the range to
        # be passed in at construction time.
        self.range = range

        if "_xmapper" not in kwargs:
            if x_type == "linear":
                self._xmapper = LinearMapper(range=self.range.x_range)
            elif x_type == "log":
                self._xmapper = LogMapper(range=self.range.x_range)
            else:
                raise ValueError("Invalid x axis type: %s" % x_type)
        else:
            self._xmapper = kwargs.pop("_xmapper")

        if "_ymapper" not in kwargs:
            if y_type == "linear":
                self._ymapper = LinearMapper(range=self.range.y_range)
            elif y_type == "log":
                self._ymapper = LogMapper(range=self.range.y_range)
            else:
                raise ValueError("Invalid y axis type: %s" % y_type)
        else:
            self._ymapper = kwargs.pop("_ymapper")

        # Now that the mappers are created, we can go to the normal HasTraits
        # constructor, which might set values that depend on us having a valid
        # range and mappers.
        super(GridMapper, self).__init__(**kwargs)
Esempio n. 2
0
 def _value_scale_changed(self, old, new):
     if old is None: return
     if new == old: return
     if not self.range2d: return
     if self.value_scale == "linear":
         vmap = LinearMapper(range=self.value_range,
                             screen_bounds=self.value_mapper.screen_bounds,
                             stretch_data=self.value_mapper.stretch_data)
     else:
         vmap = LogMapper(range=self.value_range,
                          screen_bounds=self.value_mapper.screen_bounds,
                             stretch_data=self.value_mapper.stretch_data)
     self.value_mapper = vmap
     for key in self.plots:
         for plot in self.plots[key]:
             if not isinstance(plot, BaseXYPlot):
                 raise ValueError("log scale only supported on XY plots")
             if self.value_scale == "linear":
                 vmap = LinearMapper(range=plot.value_range,
                             screen_bounds=plot.value_mapper.screen_bounds,
                             stretch_data=self.value_mapper.stretch_data)
             else:
                 vmap = LogMapper(range=plot.value_range,
                             screen_bounds=plot.value_mapper.screen_bounds,
                             stretch_data=self.value_mapper.stretch_data)
             plot.value_mapper = vmap
Esempio n. 3
0
    def add_xy_plot(self,
                    index_name,
                    value_name,
                    renderer_factory,
                    name=None,
                    origin=None,
                    **kwds):
        """ Add a BaseXYPlot renderer subclass to this Plot.

        Parameters
        ----------
        index_name : str
            The name of the index datasource.
        value_name : str
            The name of the value datasource.
        renderer_factory : callable
            The callable that creates the renderer.
        name : string (optional)
            The name of the plot.  If None, then a default one is created
            (usually "plotNNN").
        origin : string (optional)
            Which corner the origin of this plot should occupy:
                "bottom left", "top left", "bottom right", "top right"
        **kwds :
            Additional keywords to pass to the factory.
        """
        if name is None:
            name = self._make_new_plot_name()
        if origin is None:
            origin = self.default_origin
        index = self._get_or_create_datasource(index_name)
        self.index_range.add(index)
        value = self._get_or_create_datasource(value_name)
        self.value_range.add(value)

        if self.index_scale == "linear":
            imap = LinearMapper(range=self.index_range)
        else:
            imap = LogMapper(range=self.index_range)
        if self.value_scale == "linear":
            vmap = LinearMapper(range=self.value_range)
        else:
            vmap = LogMapper(range=self.value_range)

        renderer = renderer_factory(index=index,
                                    value=value,
                                    index_mapper=imap,
                                    value_mapper=vmap,
                                    orientation=self.orientation,
                                    origin=origin,
                                    **kwds)
        self.add(renderer)
        self.plots[name] = [renderer]
        self.invalidate_and_redraw()
        return self.plots[name]
Esempio n. 4
0
    def _init_components(self):
        # Since this is called after the HasTraits constructor, we have to make
        # sure that we don't blow away any components that the caller may have
        # already set.

        if not self.range2d:
            self.range2d = DataRange2D()

        if not self.index_mapper:
            if self.index_scale == "linear":
                imap = LinearMapper(range=self.range2d.x_range)
            else:
                imap = LogMapper(range=self.range2d.x_range)
            self.index_mapper = imap

        if not self.value_mapper:
            if self.value_scale == "linear":
                vmap = LinearMapper(range=self.range2d.y_range)
            else:
                vmap = LogMapper(range=self.range2d.y_range)
            self.value_mapper = vmap

        # make sure the grid and bgcolor are not the same color

        grid_color = 'lightgray'
        if color_table[self.bgcolor] == color_table[grid_color]:
            grid_color = 'white'

        if not self.x_grid and self.auto_grid:
            self.x_grid = PlotGrid(mapper=self.x_mapper,
                                   orientation="vertical",
                                   line_color=grid_color,
                                   line_style="dot",
                                   component=self)
        if not self.y_grid and self.auto_grid:
            self.y_grid = PlotGrid(mapper=self.y_mapper,
                                   orientation="horizontal",
                                   line_color=grid_color,
                                   line_style="dot",
                                   component=self)

        if not self.x_axis and self.auto_axis:
            self.x_axis = PlotAxis(mapper=self.x_mapper,
                                   orientation="bottom",
                                   component=self)

        if not self.y_axis and self.auto_axis:
            self.y_axis = PlotAxis(mapper=self.y_mapper,
                                   orientation="left",
                                   component=self)
Esempio n. 5
0
    def __init__(self, x_type="linear", y_type="linear", range=None, **kwargs):
        # TODO: This is currently an implicit assumption, i.e. that the range
        # will be passed in to the constructor.  It would be impossible to
        # create the xmapper and ymapper otherwise.  However, this should be
        # changed so that the mappers get created or modified in response to
        # the .range attribute changing, instead of requiring the range to
        # be passed in at construction time.
        self.range = range

        if "_xmapper" not in kwargs:
            if x_type == "linear":
                self._xmapper = LinearMapper(range=self.range.x_range)
            elif x_type == "log":
                self._xmapper = LogMapper(range=self.range.x_range)
            else:
                raise ValueError("Invalid x axis type: %s" % x_type)
        else:
            self._xmapper = kwargs.pop("_xmapper")

        if "_ymapper" not in kwargs:
            if y_type == "linear":
                self._ymapper = LinearMapper(range=self.range.y_range)
            elif y_type == "log":
                self._ymapper = LogMapper(range=self.range.y_range)
            else:
                raise ValueError("Invalid y axis type: %s" % y_type)
        else:
            self._ymapper = kwargs.pop("_ymapper")

        # Now that the mappers are created, we can go to the normal HasTraits
        # constructor, which might set values that depend on us having a valid
        # range and mappers.
        super(GridMapper, self).__init__(**kwargs)
Esempio n. 6
0
    def candle_plot(self, data, name=None, value_scale="linear", origin=None,
                    **styles):
        """ Adds a new sub-plot using the given data and plot style.

        Parameters
        ----------
        data : list(string), tuple(string)
            The names of the data to be plotted in the ArrayDataSource.  The
            number of arguments determines how they are interpreted:

            (index, bar_min, bar_max)
                filled or outline-only bar extending from **bar_min** to
                **bar_max**

            (index, bar_min, center, bar_max)
                above, plus a center line of a different color at **center**

            (index, min, bar_min, bar_max, max)
                bar extending from **bar_min** to **bar_max**, with thin
                bars at **min** and **max** connected to the bar by a long
                stem

            (index, min, bar_min, center, bar_max, max)
                like above, plus a center line of a different color and
                configurable thickness at **center**

        name : string
            The name of the plot.  If None, then a default one is created.

        value_scale : string
            The type of scale to use for the value axis.  If not "linear",
            then a log scale is used.

        Styles
        ------
        These are all optional keyword arguments.

        bar_color : string, 3- or 4-tuple
            The fill color of the bar; defaults to "auto".
        bar_line_color : string, 3- or 4-tuple
            The color of the rectangular box forming the bar.
        stem_color : string, 3- or 4-tuple (default = bar_line_color)
            The color of the stems reaching from the bar to the min and
            max values.
        center_color : string, 3- or 4-tuple (default = bar_line_color)
            The color of the line drawn across the bar at the center values.
        line_width : int (default = 1)
            The thickness, in pixels, of the outline around the bar.
        stem_width : int (default = line_width)
            The thickness, in pixels, of the stem lines
        center_width : int (default = line_width)
            The width, in pixels, of the line drawn across the bar at the
            center values.
        end_cap : bool (default = True)
            Whether or not to draw bars at the min and max extents of the
            error bar.

        Returns
        -------
        [renderers] -> list of renderers created in response to this call.
        """
        if len(data) == 0:
            return
        self.value_scale = value_scale

        if name is None:
            name = self._make_new_plot_name()
        if origin is None:
            origin = self.default_origin

        # Create the datasources
        if len(data) == 3:
            index, bar_min, bar_max = map(self._get_or_create_datasource, data)
            self.value_range.add(bar_min, bar_max)
            center = None
            min = None
            max = None
        elif len(data) == 4:
            index, bar_min, center, bar_max = map(self._get_or_create_datasource, data)
            self.value_range.add(bar_min, center, bar_max)
            min = None
            max = None
        elif len(data) == 5:
            index, min, bar_min, bar_max, max = \
                            map(self._get_or_create_datasource, data)
            self.value_range.add(min, bar_min, bar_max, max)
            center = None
        elif len(data) == 6:
            index, min, bar_min, center, bar_max, max = \
                            map(self._get_or_create_datasource, data)
            self.value_range.add(min, bar_min, center, bar_max, max)
        self.index_range.add(index)

        if styles.get("bar_color") == "auto" or styles.get("color") == "auto":
            self._auto_color_idx = \
                (self._auto_color_idx + 1) % len(self.auto_colors)
            styles["color"] = self.auto_colors[self._auto_color_idx]

        if self.index_scale == "linear":
            imap = LinearMapper(range=self.index_range,
                        stretch_data=self.index_mapper.stretch_data)
        else:
            imap = LogMapper(range=self.index_range,
                        stretch_data=self.index_mapper.stretch_data)
        if self.value_scale == "linear":
            vmap = LinearMapper(range=self.value_range,
                        stretch_data=self.value_mapper.stretch_data)
        else:
            vmap = LogMapper(range=self.value_range,
                        stretch_data=self.value_mapper.stretch_data)

        cls = self.renderer_map["candle"]
        plot = cls(index = index,
                          min_values = min,
                          bar_min = bar_min,
                          center_values = center,
                          bar_max = bar_max,
                          max_values = max,
                          index_mapper = imap,
                          value_mapper = vmap,
                          orientation = self.orientation,
                          origin = self.origin,
                          **styles)
        self.add(plot)
        self.plots[name] = [plot]
        return [plot]
Esempio n. 7
0
    def plot(self, data, type="line", name=None, index_scale="linear",
             value_scale="linear", origin=None, **styles):
        """ Adds a new sub-plot using the given data and plot style.

        Parameters
        ----------
        data : string, tuple(string), list(string)
            The data to be plotted. The type of plot and the number of
            arguments determines how the arguments are interpreted:

            one item: (line/scatter)
                The data is treated as the value and self.default_index is
                used as the index.  If **default_index** does not exist, one is
                created from arange(len(*data*))
            two or more items: (line/scatter)
                Interpreted as (index, value1, value2, ...).  Each index,value
                pair forms a new plot of the type specified.
            two items: (cmap_scatter)
                Interpreted as (value, color_values).  Uses **default_index**.
            three or more items: (cmap_scatter)
                Interpreted as (index, val1, color_val1, val2, color_val2, ...)

        type : comma-delimited string of "line", "scatter", "cmap_scatter"
            The types of plots to add.
        name : string
            The name of the plot.  If None, then a default one is created
            (usually "plotNNN").
        index_scale : string
            The type of scale to use for the index axis. If not "linear", then
            a log scale is used.
        value_scale : string
            The type of scale to use for the value axis. If not "linear", then
            a log scale is used.
        origin : string
            Which corner the origin of this plot should occupy:
                "bottom left", "top left", "bottom right", "top right"
        styles : series of keyword arguments
            attributes and values that apply to one or more of the
            plot types requested, e.g.,'line_color' or 'line_width'.

        Examples
        --------
        ::

            plot("my_data", type="line", name="myplot", color=lightblue)

            plot(("x-data", "y-data"), type="scatter")

            plot(("x", "y1", "y2", "y3"))

        Returns
        -------
        [renderers] -> list of renderers created in response to this call to plot()
        """
        if len(data) == 0:
            return

        if isinstance(data, basestring):
            data = (data,)

        self.index_scale = index_scale
        self.value_scale = value_scale

        # TODO: support lists of plot types
        plot_type = type
        if name is None:
            name = self._make_new_plot_name()
        if origin is None:
            origin = self.default_origin

        if plot_type in ("line", "scatter", "polygon", "bar", "filled_line"):
            # Tie data to the index range
            if len(data) == 1:
                if self.default_index is None:
                    # Create the default index based on the length of the first
                    # data series
                    value = self._get_or_create_datasource(data[0])
                    self.default_index = ArrayDataSource(arange(len(value.get_data())),
                                                         sort_order="none")
                    self.index_range.add(self.default_index)
                index = self.default_index
            else:
                index = self._get_or_create_datasource(data[0])
                if self.default_index is None:
                    self.default_index = index
                self.index_range.add(index)
                data = data[1:]

            # Tie data to the value_range and create the renderer for each data
            new_plots = []
            simple_plot_types = ("line", "scatter")
            for value_name in data:
                value = self._get_or_create_datasource(value_name)
                self.value_range.add(value)
                if plot_type in simple_plot_types:
                    cls = self.renderer_map[plot_type]
                    # handle auto-coloring request
                    if styles.get("color") == "auto":
                        self._auto_color_idx = \
                            (self._auto_color_idx + 1) % len(self.auto_colors)
                        styles["color"] = self.auto_colors[self._auto_color_idx]
                elif plot_type in ("polygon", "filled_line"):
                    cls = self.renderer_map[plot_type]
                    # handle auto-coloring request
                    if styles.get("edge_color") == "auto":
                        self._auto_edge_color_idx = \
                            (self._auto_edge_color_idx + 1) % len(self.auto_colors)
                        styles["edge_color"] = self.auto_colors[self._auto_edge_color_idx]
                    if styles.get("face_color") == "auto":
                        self._auto_face_color_idx = \
                            (self._auto_face_color_idx + 1) % len(self.auto_colors)
                        styles["face_color"] = self.auto_colors[self._auto_face_color_idx]
                elif plot_type == 'bar':
                    cls = self.renderer_map[plot_type]
                    # handle auto-coloring request
                    if styles.get("color") == "auto":
                        self._auto_color_idx = \
                            (self._auto_color_idx + 1) % len(self.auto_colors)
                        styles["fill_color"] = self.auto_colors[self._auto_color_idx]
                else:
                    raise ValueError("Unhandled plot type: " + plot_type)

                if self.index_scale == "linear":
                    imap = LinearMapper(range=self.index_range,
                                stretch_data=self.index_mapper.stretch_data)
                else:
                    imap = LogMapper(range=self.index_range,
                                stretch_data=self.index_mapper.stretch_data)
                if self.value_scale == "linear":
                    vmap = LinearMapper(range=self.value_range,
                                stretch_data=self.value_mapper.stretch_data)
                else:
                    vmap = LogMapper(range=self.value_range,
                                stretch_data=self.value_mapper.stretch_data)

                plot = cls(index=index,
                           value=value,
                           index_mapper=imap,
                           value_mapper=vmap,
                           orientation=self.orientation,
                           origin = origin,
                           **styles)

                self.add(plot)
                new_plots.append(plot)

            if plot_type == 'bar':
                # For bar plots, compute the ranges from the data to make the
                # plot look clean.

                def custom_index_func(data_low, data_high, margin, tight_bounds):
                    """ Compute custom bounds of the plot along index (in
                    data space).
                    """
                    bar_width = styles.get('bar_width', cls().bar_width)
                    plot_low = data_low - bar_width
                    plot_high = data_high + bar_width
                    return plot_low, plot_high

                if self.index_range.bounds_func is None:
                    self.index_range.bounds_func = custom_index_func

                def custom_value_func(data_low, data_high, margin, tight_bounds):
                    """ Compute custom bounds of the plot along value (in
                    data space).
                    """
                    plot_low = data_low - (data_high-data_low)*0.1
                    plot_high = data_high + (data_high-data_low)*0.1
                    return plot_low, plot_high

                if self.value_range.bounds_func is None:
                    self.value_range.bounds_func = custom_value_func

                self.index_range.tight_bounds = False
                self.value_range.tight_bounds = False
                self.index_range.refresh()
                self.value_range.refresh()

            self.plots[name] = new_plots

        elif plot_type == "cmap_scatter":
            if len(data) != 3:
                raise ValueError("Colormapped scatter plots require (index, value, color) data")
            else:
                index = self._get_or_create_datasource(data[0])
                if self.default_index is None:
                    self.default_index = index
                self.index_range.add(index)
                value = self._get_or_create_datasource(data[1])
                self.value_range.add(value)
                color = self._get_or_create_datasource(data[2])
                if not styles.has_key("color_mapper"):
                    raise ValueError("Scalar 2D data requires a color_mapper.")

                colormap = styles.pop("color_mapper", None)

                if self.color_mapper is not None and self.color_mapper.range is not None:
                    color_range = self.color_mapper.range
                else:
                    color_range = DataRange1D()

                if isinstance(colormap, AbstractColormap):
                    self.color_mapper = colormap
                    if colormap.range is None:
                        color_range.add(color)
                        colormap.range = color_range

                elif callable(colormap):
                    color_range.add(color)
                    self.color_mapper = colormap(color_range)
                else:
                    raise ValueError("Unexpected colormap %r in plot()." % colormap)

                if self.index_scale == "linear":
                    imap = LinearMapper(range=self.index_range,
                                stretch_data=self.index_mapper.stretch_data)
                else:
                    imap = LogMapper(range=self.index_range,
                                stretch_data=self.index_mapper.stretch_data)
                if self.value_scale == "linear":
                    vmap = LinearMapper(range=self.value_range,
                                stretch_data=self.value_mapper.stretch_data)
                else:
                    vmap = LogMapper(range=self.value_range,
                                stretch_data=self.value_mapper.stretch_data)

                cls = self.renderer_map["cmap_scatter"]
                plot = cls(index=index,
                           index_mapper=imap,
                           value=value,
                           value_mapper=vmap,
                           color_data=color,
                           color_mapper=self.color_mapper,
                           orientation=self.orientation,
                           origin=origin,
                           **styles)
                self.add(plot)

            self.plots[name] = [plot]
        else:
            raise ValueError("Unknown plot type: " + plot_type)

        return self.plots[name]
Esempio n. 8
0
class GridMapper(AbstractMapper):
    """
    Maps a 2-D data space to and from screen space by specifying a 2-tuple in
    data space or by specifying a pair of screen coordinates.

    The mapper concerns itself only with metric and not with orientation. So,
    to "flip" a screen space orientation, swap the appropriate screen space
    values for **x_low_pos**, **x_high_pos**, **y_low_pos**, and
    **y_high_pos**.
    """

    # The data-space bounds of the mapper.
    range = Instance(DataRange2D)

    # The screen space position of the lower bound of the horizontal axis.
    x_low_pos = Float(0.0)

    # The screen space position of the upper bound of the horizontal axis.
    x_high_pos = Float(1.0)

    # The screen space position of the lower bound of the vertical axis.
    y_low_pos = Float(0.0)

    # The screen space position of the upper bound of the vertical axis.
    y_high_pos = Float(1.0)

    # Convenience property for low and high positions in one structure.
    # Must be a tuple (x_low_pos, x_high_pos, y_low_pos, y_high_pos).
    screen_bounds = Property

    # Should the mapper stretch the dataspace when its screen space bounds are
    # modified (default), or should it preserve the screen-to-data ratio and
    # resize the data bounds?  If the latter, it will only try to preserve
    # the ratio if both screen and data space extents are non-zero.
    stretch_data_x = DelegatesTo("_xmapper", prefix="stretch_data")
    stretch_data_y = DelegatesTo("_ymapper", prefix="stretch_data")

    # Should the mapper try to maintain a fixed aspect ratio between x and y
    maintain_aspect_ratio = Bool

    # The aspect ratio that we wish to maintain
    aspect_ratio = Float(1.0)

    #------------------------------------------------------------------------
    # Private Traits
    #------------------------------------------------------------------------

    _updating_submappers = Bool(False)
    _updating_aspect = Bool(False)

    _xmapper = Instance(Base1DMapper)
    _ymapper = Instance(Base1DMapper)

    #------------------------------------------------------------------------
    # Public methods
    #------------------------------------------------------------------------

    def __init__(self, x_type="linear", y_type="linear", range=None, **kwargs):
        # TODO: This is currently an implicit assumption, i.e. that the range
        # will be passed in to the constructor.  It would be impossible to
        # create the xmapper and ymapper otherwise.  However, this should be
        # changed so that the mappers get created or modified in response to
        # the .range attribute changing, instead of requiring the range to
        # be passed in at construction time.
        self.range = range

        if "_xmapper" not in kwargs:
            if x_type == "linear":
                self._xmapper = LinearMapper(range=self.range.x_range)
            elif x_type == "log":
                self._xmapper = LogMapper(range=self.range.x_range)
            else:
                raise ValueError("Invalid x axis type: %s" % x_type)
        else:
            self._xmapper = kwargs.pop("_xmapper")

        if "_ymapper" not in kwargs:
            if y_type == "linear":
                self._ymapper = LinearMapper(range=self.range.y_range)
            elif y_type == "log":
                self._ymapper = LogMapper(range=self.range.y_range)
            else:
                raise ValueError("Invalid y axis type: %s" % y_type)
        else:
            self._ymapper = kwargs.pop("_ymapper")

        # Now that the mappers are created, we can go to the normal HasTraits
        # constructor, which might set values that depend on us having a valid
        # range and mappers.
        super(GridMapper, self).__init__(**kwargs)

    def map_screen(self, data_pts):
        """ map_screen(data_pts) -> screen_array

        Maps values from data space into screen space.
        """
        xs, ys = transpose(data_pts)
        screen_xs = self._xmapper.map_screen(xs)
        screen_ys = self._ymapper.map_screen(ys)
        screen_pts = column_stack([screen_xs, screen_ys])
        return screen_pts

    def map_data(self, screen_pts):
        """ map_data(screen_pts) -> data_vals

        Maps values from screen space into data space.
        """
        screen_xs, screen_ys = transpose(screen_pts)
        xs = self._xmapper.map_data(screen_xs)
        ys = self._ymapper.map_data(screen_ys)
        data_pts = column_stack([xs, ys])
        return data_pts

    def map_data_array(self, screen_pts):
        return self.map_data(screen_pts)

    #------------------------------------------------------------------------
    # Private Methods
    #------------------------------------------------------------------------

    def _update_bounds(self):
        with self._update_submappers():
            self._xmapper.screen_bounds = (self.x_low_pos, self.x_high_pos)
            self._ymapper.screen_bounds = (self.y_low_pos, self.y_high_pos)
        self.updated = True

    def _update_range(self):
        self.updated = True

    def _update_aspect_x(self):
        y_width = self._ymapper.high_pos - self._ymapper.low_pos
        if y_width == 0:
            return
        y_scale = (self._ymapper.range.high -
                   self._ymapper.range.low) / y_width
        x_range_low = self._xmapper.range.low
        x_width = self._xmapper.high_pos - self._xmapper.low_pos
        sign = self._xmapper.sign * self._ymapper.sign
        if x_width == 0 or sign == 0:
            return
        x_scale = sign * y_scale / self.aspect_ratio
        with self._update_aspect():
            self._xmapper.range.set_bounds(x_range_low,
                                           x_range_low + x_scale * x_width)

    def _update_aspect_y(self):
        x_width = self._xmapper.high_pos - self._xmapper.low_pos
        if x_width == 0:
            return
        x_scale = (self._xmapper.range.high -
                   self._xmapper.range.low) / x_width
        y_range_low = self._ymapper.range.low
        y_width = self._ymapper.high_pos - self._ymapper.low_pos
        sign = self._xmapper.sign * self._ymapper.sign
        if y_width == 0 or sign == 0:
            return
        y_scale = sign * x_scale * self.aspect_ratio
        with self._update_aspect():
            self._ymapper.range.set_bounds(y_range_low,
                                           y_range_low + y_scale * y_width)

    #------------------------------------------------------------------------
    # Property handlers
    #------------------------------------------------------------------------

    def _range_changed(self, old, new):
        if old is not None:
            old.on_trait_change(self._update_range, "updated", remove=True)
        if new is not None:
            new.on_trait_change(self._update_range, "updated")
            if self._xmapper is not None:
                self._xmapper.range = new.x_range
            if self._ymapper is not None:
                self._ymapper.range = new.y_range
            self._update_range()

    def _x_low_pos_changed(self):
        self._xmapper.low_pos = self.x_low_pos

    def _x_high_pos_changed(self):
        self._xmapper.high_pos = self.x_high_pos

    def _y_low_pos_changed(self):
        self._ymapper.low_pos = self.y_low_pos

    def _y_high_pos_changed(self):
        self._ymapper.high_pos = self.y_high_pos

    def _set_screen_bounds(self, new_bounds):
        # TODO: figure out a way to not need to do this check:
        if self.screen_bounds == new_bounds:
            return
        self.set(x_low_pos=new_bounds[0], trait_change_notify=False)
        self.set(x_high_pos=new_bounds[1], trait_change_notify=False)
        self.set(y_low_pos=new_bounds[2], trait_change_notify=False)
        self.set(y_high_pos=new_bounds[3], trait_change_notify=False)
        self._update_bounds()

    def _get_screen_bounds(self):
        return (self.x_low_pos, self.x_high_pos, self.y_low_pos,
                self.y_high_pos)

    def _updated_fired_for__xmapper(self):
        if not self._updating_aspect:
            if self.maintain_aspect_ratio and self.stretch_data_x:
                self._update_aspect_y()
        if not self._updating_submappers:
            self.updated = True

    def _updated_fired_for__ymapper(self):
        if not self._updating_aspect:
            if self.maintain_aspect_ratio and self.stretch_data_y:
                self._update_aspect_x()
        if not self._updating_submappers:
            self.updated = True

    @contextmanager
    def _update_submappers(self):
        self._updating_submappers = True
        try:
            yield
        finally:
            self._updating_submappers = False

    @contextmanager
    def _update_aspect(self):
        self._updating_aspect = True
        try:
            yield
        finally:
            self._updating_aspect = False
Esempio n. 9
0
class GridMapper(AbstractMapper):
    """
    Maps a 2-D data space to and from screen space by specifying a 2-tuple in
    data space or by specifying a pair of screen coordinates.

    The mapper concerns itself only with metric and not with orientation. So, to
    "flip" a screen space orientation, swap the appropriate screen space
    values for **x_low_pos**, **x_high_pos**, **y_low_pos**, and **y_high_pos**.
    """

    # The data-space bounds of the mapper.
    range = Instance(DataRange2D)

    # The screen space position of the lower bound of the horizontal axis.
    x_low_pos = Float(0.0)

    # The screen space position of the upper bound of the horizontal axis.
    x_high_pos  = Float(1.0)

    # The screen space position of the lower bound of the vertical axis.
    y_low_pos = Float(0.0)

    # The screen space position of the upper bound of the vertical axis.
    y_high_pos  = Float(1.0)

    # Convenience property for low and high positions in one structure.
    # Must be a tuple (x_low_pos, x_high_pos, y_low_pos, y_high_pos).
    screen_bounds = Property

    # Should the mapper stretch the dataspace when its screen space bounds are
    # modified (default), or should it preserve the screen-to-data ratio and
    # resize the data bounds?  If the latter, it will only try to preserve
    # the ratio if both screen and data space extents are non-zero.
    stretch_data_x = DelegatesTo("_xmapper", prefix="stretch_data")
    stretch_data_y = DelegatesTo("_ymapper", prefix="stretch_data")

    #------------------------------------------------------------------------
    # Private Traits
    #------------------------------------------------------------------------

    _updating_submappers = Bool(False)

    _xmapper = Instance(Base1DMapper)
    _ymapper = Instance(Base1DMapper)


    #------------------------------------------------------------------------
    # Public methods
    #------------------------------------------------------------------------

    def __init__(self, x_type="linear", y_type="linear", range=None, **kwargs):
        # TODO: This is currently an implicit assumption, i.e. that the range
        # will be passed in to the constructor.  It would be impossible to
        # create the xmapper and ymapper otherwise.  However, this should be
        # changed so that the mappers get created or modified in response to
        # the .range attribute changing, instead of requiring the range to
        # be passed in at construction time.
        self.range = range

        if "_xmapper" not in kwargs:
            if x_type == "linear":
                self._xmapper = LinearMapper(range=self.range.x_range)
            elif x_type == "log":
                self._xmapper = LogMapper(range=self.range.x_range)
            else:
                raise ValueError("Invalid x axis type: %s" % x_type)
        else:
            self._xmapper = kwargs.pop("_xmapper")

        if "_ymapper" not in kwargs:
            if y_type == "linear":
                self._ymapper = LinearMapper(range=self.range.y_range)
            elif y_type == "log":
                self._ymapper = LogMapper(range=self.range.y_range)
            else:
                raise ValueError("Invalid y axis type: %s" % y_type)
        else:
            self._ymapper = kwargs.pop("_ymapper")

        # Now that the mappers are created, we can go to the normal HasTraits
        # constructor, which might set values that depend on us having a valid
        # range and mappers.
        super(GridMapper, self).__init__(**kwargs)


    def map_screen(self, data_pts):
        """ map_screen(data_pts) -> screen_array

        Maps values from data space into screen space.
        """
        xs, ys = transpose(data_pts)
        screen_xs = self._xmapper.map_screen(xs)
        screen_ys = self._ymapper.map_screen(ys)
        return zip(screen_xs,screen_ys)

    def map_data(self, screen_pts):
        """ map_data(screen_pts) -> data_vals

        Maps values from screen space into data space.
        """
        screen_xs, screen_ys = transpose(screen_pts)
        xs = self._xmapper.map_data(screen_xs)
        ys = self._ymapper.map_data(screen_ys)
        return zip(xs,ys)

    def map_data_array(self, screen_pts):
        return self.map_data(screen_pts)


    #------------------------------------------------------------------------
    # Private Methods
    #------------------------------------------------------------------------

    def _update_bounds(self):
        self._updating_submappers = True
        self._xmapper.screen_bounds = (self.x_low_pos, self.x_high_pos)
        self._ymapper.screen_bounds = (self.y_low_pos, self.y_high_pos)
        self._updating_submappers = False
        self.updated = True

    def _update_range(self):
        self.updated = True


    #------------------------------------------------------------------------
    # Property handlers
    #------------------------------------------------------------------------

    def _range_changed(self, old, new):
        if old is not None:
            old.on_trait_change(self._update_range, "updated", remove=True)
        if new is not None:
            new.on_trait_change(self._update_range, "updated")
            if self._xmapper is not None:
                self._xmapper.range = new.x_range
            if self._ymapper is not None:
                self._ymapper.range = new.y_range
            self._update_range()

    def _x_low_pos_changed(self):
        self._xmapper.low_pos = self.x_low_pos

    def _x_high_pos_changed(self):
        self._xmapper.high_pos = self.x_high_pos

    def _y_low_pos_changed(self):
        self._ymapper.low_pos = self.y_low_pos

    def _y_high_pos_changed(self):
        self._ymapper.high_pos = self.y_high_pos

    def _set_screen_bounds(self, new_bounds):
        # TODO: figure out a way to not need to do this check:
        if self.screen_bounds == new_bounds:
            return
        self.set(x_low_pos = new_bounds[0], trait_change_notify=False)
        self.set(x_high_pos = new_bounds[1], trait_change_notify=False)
        self.set(y_low_pos = new_bounds[2], trait_change_notify=False)
        self.set(y_high_pos = new_bounds[3], trait_change_notify=False)
        self._update_bounds( )

    def _get_screen_bounds(self):
        return (self.x_low_pos, self.x_high_pos,
                self.y_low_pos, self.y_high_pos)

    def _updated_fired_for__xmapper(self):
        if not self._updating_submappers:
            self.updated = True

    def _updated_fired_for__ymapper(self):
        if not self._updating_submappers:
            self.updated = True