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 _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
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]
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)
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]
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]
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
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