class RegressionOverlay(LassoOverlay): line_color = ColorTrait("black") line_style = LineStyle("dash") line_width = Float(2.0) _label = Instance(Label, kw=dict(bgcolor="white", border_color="black", font="modern 14", border_width=1)) def _draw_component(self, gc, view_bounds=None, mode="normal"): LassoOverlay._draw_component(self, gc, view_bounds, mode) selection = self.lasso_selection if selection.fit_params is not None: # draw the label overlay self._label.component = self.component c = self.component if selection.fit_params[1] < 0: operator = "-" else: operator = "+" self._label.text = "%.2fx "%selection.fit_params[0] + operator + \ " %.2f" % fabs(selection.fit_params[1]) w, h = self._label.get_width_height(gc) x = (c.x + c.x2) / 2 - w / 2 y = c.y + 5 # add some padding on the bottom with gc: gc.translate_ctm(x, y) self._label.draw(gc) # draw the line slope, y0 = selection.fit_params f = lambda x: slope * x + y0 cx, cy = c.map_screen([selection.centroid])[0] left = c.x right = c.x2 left_x = c.map_data([left, c.y])[0] right_x = c.map_data([right, c.y])[0] left_y = f(left_x) right_y = f(right_x) left_pt, right_pt = c.map_screen([[left_x, left_y], [right_x, right_y]]) with gc: gc.set_line_dash(self.line_style_) gc.set_stroke_color(self.line_color_) gc.set_line_width(self.line_width) gc.move_to(*left_pt) gc.line_to(*right_pt) gc.stroke_path() return
class LineRendererStyle(BaseXYRendererStyle): """ Styling object for customizing line renderers. """ renderer_type = REND_TYPE_LINE line_width = Float(DEFAULT_LINE_WIDTH) line_style = LineStyle("solid") def traits_view(self): view = self.view_klass( VGroup( HGroup( Item('line_width', label="Line width"), Item('line_style', label="Line style", style="custom"), ), *self.general_view_elements), ) return view def _dict_keys_default(self): general_items = super(LineRendererStyle, self)._dict_keys_default() new = [("line_width", "width"), ("line_style", "dash")] return general_items + new
class ContourLinePlot(BaseContourPlot): """ Takes a value data object whose elements are scalars, and renders them as a contour plot. """ # TODO: Modify ImageData to explicitly support scalar value arrays #------------------------------------------------------------------------ # Data-related traits #------------------------------------------------------------------------ # The thickness(es) of the contour lines. # It can be either a scalar value, valid for all contour lines, or a list # of widths. If the list is too short with respect to then number of # contour lines, the values are repeated from the beginning of the list. # Widths are associated with levels of increasing value. widths = Trait(1.0, Float, List) # The line dash style(s). styles = Trait("signed", Str, List) # Line style for positive levels. positive_style = LineStyle("solid") # Line style for negative levels. negative_style = LineStyle("dash") #------------------------------------------------------------------------ # Private traits #------------------------------------------------------------------------ # Are the cached contours valid? If False, new ones need to be computed. _contour_cache_valid = Bool(False) # Cached collection of traces. _cached_contours = Dict # Is the cached width data valid? _widths_cache_valid = Bool(False) # Is the cached style data valid? _styles_cache_valid = Bool(False) # Cached list of line widths _widths = List # Cached list of line styles _styles = List # Mapped trait used to convert user-supplied line style values to # AGG-acceptable ones. (Mapped traits in lists are not supported, must be # converted one at a time.) _style_map_trait = LineStyle #------------------------------------------------------------------------ # Private methods #------------------------------------------------------------------------ def _render(self, gc): """ Actually draws the plot. Implements the Base2DPlot interface. """ if not self._level_cache_valid: self._update_levels() if not self._contour_cache_valid: self._update_contours() if not self._widths_cache_valid: self._update_widths() if not self._styles_cache_valid: self._update_styles() if not self._colors_cache_valid: self._update_colors() with gc: gc.set_antialias(True) gc.clip_to_rect(self.x, self.y, self.width, self.height) gc.set_alpha(self.alpha) gc.set_line_join(constants.JOIN_BEVEL) gc.set_line_cap(constants.CAP_ROUND) for i in range(len(self._levels)): gc.set_stroke_color(self._colors[i]) gc.set_line_width(self._widths[i]) gc.set_line_dash(self._styles[i]) for trace in self._cached_contours[self._levels[i]]: if self.orientation == "h": strace = self.index_mapper.map_screen(trace) else: strace = array( self.index_mapper.map_screen(trace))[:, ::-1] gc.begin_path() gc.lines(strace) gc.stroke_path() def _update_contours(self): """ Updates the cache of contour lines """ if self.value.is_masked(): # XXX masked data and get_data_mask not currently implemented data, mask = self.value.get_data_mask() mask &= isfinite(data) else: data = self.value.get_data() mask = isfinite(data) x_data, y_data = self.index.get_data() xs = x_data.get_data() ys = y_data.get_data() xg, yg = meshgrid(xs, ys) # note: contour wants mask True in invalid locations c = Cntr(xg, yg, data, ~mask) self._cached_contours = {} for level in self._levels: self._cached_contours[level] = [] traces = c.trace(level) for trace in traces: self._cached_contours[level].append(transpose(trace)) self._contour_cache_valid = True def _update_levels(self): """ Extends the parent method to also invalidate some other things """ super(ContourLinePlot, self)._update_levels() self._contour_cache_valid = False self._widths_cache_valid = False self._styles_cache_valid = False def _update_widths(self): """ Updates the widths cache. """ # If we are given a single width, apply it to all levels if isinstance(self.widths, float): self._widths = [self.widths] * len(self._levels) # If the list of widths is shorter than the list of levels, # simply repeat widths from the beginning of the list as needed else: self._widths = [] for i in range(len(self._levels)): self._widths.append(self.widths[i % len(self.widths)]) self._widths_cache_valid = True def _update_styles(self): """ Updates the styles cache. """ # If the style type is "signed" then assign styles to levels based # on their sign if self.styles == "signed": self._styles = [] for level in self._levels: if level < 0: self._styles.append(self.negative_style_) else: self._styles.append(self.positive_style_) # If we not given a list, apply the one style to all levels elif not isinstance(self.styles, list): self._style_map_trait = self.styles self._styles = [self._style_map_trait_] * len(self._levels) # If the list of styles is shorter than the list of levels, # simply repeat styles from the beginning of the list as needed else: self._styles = [] for i in range(len(self._levels)): self._style_map_trait = self.styles[i % len(self.styles)] self._styles.append(self._style_map_trait_) self._styles_cache_valid = True #------------------------------------------------------------------------ # Event handlers #------------------------------------------------------------------------ def _widths_changed(self): if self._level_cache_valid: self._update_widths() self.invalidate_draw() def _styles_changed(self): if self._level_cache_valid: self._update_styles() self.invalidate_draw() def _negative_style_changed(self): if self._level_cache_valid: self._update_styles() self.invalidate_draw() def _positive_style_changed(self): if self._level_cache_valid: self._update_styles() self.invalidate_draw()
class RangeSelectionOverlay(AbstractOverlay): """ Highlights the selection region on a component. Looks at a given metadata field of self.component for regions to draw as selected. """ # The axis to which this tool is perpendicular. axis = Enum("index", "value") # Mapping from screen space to data space. By default, it is just # self.component. plot = Property(depends_on='component') # The mapper (and associated range) that drive this RangeSelectionOverlay. # By default, this is the mapper on self.plot that corresponds to self.axis. mapper = Instance(AbstractMapper) # The element of an (x,y) tuple that corresponds to the axis index. # By default, this is set based on self.asix and self.plot.orientation, # but it can be overriden and set to 0 or 1. axis_index = Property # The name of the metadata to look at for dataspace bounds. The metadata # can be either a tuple (dataspace_start, dataspace_end) in "selections" or # a boolean array mask of seleted dataspace points with any other name metadata_name = Str("selections") #------------------------------------------------------------------------ # Appearance traits #------------------------------------------------------------------------ # The color of the selection border line. border_color = ColorTrait("dodgerblue") # The width, in pixels, of the selection border line. border_width = Float(1.0) # The line style of the selection border line. border_style = LineStyle("solid") # The color to fill the selection region. fill_color = ColorTrait("lightskyblue") # The transparency of the fill color. alpha = Float(0.3) #------------------------------------------------------------------------ # AbstractOverlay interface #------------------------------------------------------------------------ def overlay(self, component, gc, view_bounds=None, mode="normal"): """ Draws this component overlaid on another component. Overrides AbstractOverlay. """ axis_ndx = self.axis_index lower_left = [0, 0] upper_right = [0, 0] # Draw the selection coords = self._get_selection_screencoords() for coord in coords: start, end = coord lower_left[axis_ndx] = start lower_left[1 - axis_ndx] = component.position[1 - axis_ndx] upper_right[axis_ndx] = end - start upper_right[1 - axis_ndx] = component.bounds[1 - axis_ndx] with gc: gc.clip_to_rect(component.x, component.y, component.width, component.height) gc.set_alpha(self.alpha) gc.set_fill_color(self.fill_color_) gc.set_stroke_color(self.border_color_) gc.set_line_width(self.border_width) gc.set_line_dash(self.border_style_) gc.draw_rect((lower_left[0], lower_left[1], upper_right[0], upper_right[1])) #------------------------------------------------------------------------ # Private methods #------------------------------------------------------------------------ def _get_selection_screencoords(self): """ Returns a tuple of (x1, x2) screen space coordinates of the start and end selection points. If there is no current selection, then returns an empty list. """ ds = getattr(self.plot, self.axis) selection = ds.metadata.get(self.metadata_name, None) if selection is None: return [] # "selections" metadata must be a tuple if self.metadata_name == "selections" or \ (selection is not None and isinstance(selection, tuple)): if selection is not None and len(selection) == 2: return [self.mapper.map_screen(array(selection))] else: return [] # All other metadata is interpreted as a mask on dataspace else: ar = arange(0, len(selection), 1) runs = arg_find_runs(ar[selection]) coords = [] for inds in runs: start = ds._data[ar[selection][inds[0]]] end = ds._data[ar[selection][inds[1] - 1]] coords.append(self.mapper.map_screen(array((start, end)))) return coords def _determine_axis(self): """ Determines which element of an (x,y) coordinate tuple corresponds to the tool's axis of interest. This method is only called if self._axis_index hasn't been set (or is None). """ if self.axis == "index": if self.plot.orientation == "h": return 0 else: return 1 else: # self.axis == "value" if self.plot.orientation == "h": return 1 else: return 0 #------------------------------------------------------------------------ # Trait event handlers #------------------------------------------------------------------------ def _component_changed(self, old, new): self._attach_metadata_handler(old, new) return def _axis_changed(self, old, new): self._attach_metadata_handler(old, new) return def _attach_metadata_handler(self, old, new): # This is used to attach a listener to the datasource so that when # its metadata has been updated, we catch the event and update properly if not self.plot: return datasource = getattr(self.plot, self.axis) if old: datasource.on_trait_change(self._metadata_change_handler, "metadata_changed", remove=True) if new: datasource.on_trait_change(self._metadata_change_handler, "metadata_changed") return def _metadata_change_handler(self, event): self.component.request_redraw() return #------------------------------------------------------------------------ # Default initializers #------------------------------------------------------------------------ def _mapper_default(self): # If the plot's mapper is a GridMapper, return either its # x mapper or y mapper mapper = getattr(self.plot, self.axis + "_mapper") if isinstance(mapper, GridMapper): if self.axis == 'index': return mapper._xmapper else: return mapper._ymapper else: return mapper #------------------------------------------------------------------------ # Property getter/setters #------------------------------------------------------------------------ @cached_property def _get_plot(self): return self.component @cached_property def _get_axis_index(self): return self._determine_axis()
class PlotAxis(AbstractOverlay): """ The PlotAxis is a visual component that can be rendered on its own as a standalone component or attached as an overlay to another component. (To attach it as an overlay, set its **component** attribute.) When it is attached as an overlay, it draws into the padding around the component. """ #: The mapper that drives this axis. mapper = Instance(AbstractMapper) #: Keep an origin for plots that aren't attached to a component origin = Enum("bottom left", "top left", "bottom right", "top right") #: The text of the axis title. title = Trait('', Str, Unicode) #May want to add PlotLabel option #: The font of the title. title_font = KivaFont('modern 12') #: The spacing between the axis line and the title title_spacing = Trait('auto', 'auto', Float) #: The color of the title. title_color = ColorTrait("black") #: The angle of the title, in degrees, from horizontal line title_angle = Float(0.) #: The thickness (in pixels) of each tick. tick_weight = Float(1.0) #: The color of the ticks. tick_color = ColorTrait("black") #: The font of the tick labels. tick_label_font = KivaFont('modern 10') #: The color of the tick labels. tick_label_color = ColorTrait("black") #: The rotation of the tick labels. tick_label_rotate_angle = Float(0) #: Whether to align to corners or edges (corner is better for 45 degree rotation) tick_label_alignment = Enum('edge', 'corner') #: The margin around the tick labels. tick_label_margin = Int(2) #: The distance of the tick label from the axis. tick_label_offset = Float(8.) #: Whether the tick labels appear to the inside or the outside of the plot area tick_label_position = Enum("outside", "inside") #: A callable that is passed the numerical value of each tick label and #: that returns a string. tick_label_formatter = Callable(DEFAULT_TICK_FORMATTER) #: The number of pixels by which the ticks extend into the plot area. tick_in = Int(5) #: The number of pixels by which the ticks extend into the label area. tick_out = Int(5) #: Are ticks visible at all? tick_visible = Bool(True) #: The dataspace interval between ticks. tick_interval = Trait('auto', 'auto', Float) #: A callable that implements the AbstractTickGenerator interface. tick_generator = Instance(AbstractTickGenerator) #: The location of the axis relative to the plot. This determines where #: the axis title is located relative to the axis line. orientation = Enum("top", "bottom", "left", "right") #: Is the axis line visible? axis_line_visible = Bool(True) #: The color of the axis line. axis_line_color = ColorTrait("black") #: The line thickness (in pixels) of the axis line. axis_line_weight = Float(1.0) #: The dash style of the axis line. axis_line_style = LineStyle('solid') #: A special version of the axis line that is more useful for geophysical #: plots. small_haxis_style = Bool(False) #: Does the axis ensure that its end labels fall within its bounding area? ensure_labels_bounded = Bool(False) #: Does the axis prevent the ticks from being rendered outside its bounds? #: This flag is off by default because the standard axis *does* render ticks #: that encroach on the plot area. ensure_ticks_bounded = Bool(False) #: Fired when the axis's range bounds change. updated = Event #------------------------------------------------------------------------ # Override default values of inherited traits #------------------------------------------------------------------------ #: Background color (overrides AbstractOverlay). Axes usually let the color of #: the container show through. bgcolor = ColorTrait("transparent") #: Dimensions that the axis is resizable in (overrides PlotComponent). #: Typically, axes are resizable in both dimensions. resizable = "hv" #------------------------------------------------------------------------ # Private Traits #------------------------------------------------------------------------ # Cached position calculations _tick_list = List # These are caches of their respective positions _tick_positions = ArrayOrNone() _tick_label_list = ArrayOrNone() _tick_label_positions = ArrayOrNone() _tick_label_bounding_boxes = List _major_axis_size = Float _minor_axis_size = Float _major_axis = Array _title_orientation = Array _title_angle = Float _origin_point = Array _inside_vector = Array _axis_vector = Array _axis_pixel_vector = Array _end_axis_point = Array ticklabel_cache = List _cache_valid = Bool(False) #------------------------------------------------------------------------ # Public methods #------------------------------------------------------------------------ def __init__(self, component=None, **kwargs): # TODO: change this back to a factory in the instance trait some day self.tick_generator = DefaultTickGenerator() # Override init so that our component gets set last. We want the # _component_changed() event handler to get run last. super(PlotAxis, self).__init__(**kwargs) if component is not None: self.component = component def invalidate(self): """ Invalidates the pre-computed layout and scaling data. """ self._reset_cache() self.invalidate_draw() return def traits_view(self): """ Returns a View instance for use with Traits UI. This method is called automatically be the Traits framework when .edit_traits() is invoked. """ from .axis_view import AxisView return AxisView #------------------------------------------------------------------------ # PlotComponent and AbstractOverlay interface #------------------------------------------------------------------------ def _do_layout(self, *args, **kw): """ Tells this component to do layout at a given size. Overrides Component. """ if self.use_draw_order and self.component is not None: self._layout_as_overlay(*args, **kw) else: super(PlotAxis, self)._do_layout(*args, **kw) return def overlay(self, component, gc, view_bounds=None, mode='normal'): """ Draws this component overlaid on another component. Overrides AbstractOverlay. """ if not self.visible: return self._draw_component(gc, view_bounds, mode, component) return def _draw_overlay(self, gc, view_bounds=None, mode='normal'): """ Draws the overlay layer of a component. Overrides PlotComponent. """ self._draw_component(gc, view_bounds, mode) return def _draw_component(self, gc, view_bounds=None, mode='normal', component=None): """ Draws the component. This method is preserved for backwards compatibility. Overrides PlotComponent. """ if not self.visible: return if not self._cache_valid: if component is not None: self._calculate_geometry_overlay(component) else: self._calculate_geometry() self._compute_tick_positions(gc, component) self._compute_labels(gc) with gc: # slight optimization: if we set the font correctly on the # base gc before handing it in to our title and tick labels, # their set_font() won't have to do any work. gc.set_font(self.tick_label_font) if self.axis_line_visible: self._draw_axis_line(gc, self._origin_point, self._end_axis_point) if self.title: self._draw_title(gc) self._draw_ticks(gc) self._draw_labels(gc) self._cache_valid = True return #------------------------------------------------------------------------ # Private draw routines #------------------------------------------------------------------------ def _layout_as_overlay(self, size=None, force=False): """ Lays out the axis as an overlay on another component. """ if self.component is not None: if self.orientation in ("left", "right"): self.y = self.component.y self.height = self.component.height if self.orientation == "left": self.width = self.component.padding_left self.x = self.component.outer_x elif self.orientation == "right": self.width = self.component.padding_right self.x = self.component.x2 + 1 else: self.x = self.component.x self.width = self.component.width if self.orientation == "bottom": self.height = self.component.padding_bottom self.y = self.component.outer_y elif self.orientation == "top": self.height = self.component.padding_top self.y = self.component.y2 + 1 return def _draw_axis_line(self, gc, startpoint, endpoint): """ Draws the line for the axis. """ with gc: gc.set_antialias(0) gc.set_line_width(self.axis_line_weight) gc.set_stroke_color(self.axis_line_color_) gc.set_line_dash(self.axis_line_style_) gc.move_to(*around(startpoint)) gc.line_to(*around(endpoint)) gc.stroke_path() return def _draw_title(self, gc, label=None, axis_offset=None): """ Draws the title for the axis. """ if label is None: title_label = Label(text=self.title, font=self.title_font, color=self.title_color, rotate_angle=self.title_angle) else: title_label = label # get the _rotated_ bounding box of the label tl_bounds = array(title_label.get_bounding_box(gc), float64) text_center_to_corner = -tl_bounds/2.0 # which axis are we moving away from the axis line along? axis_index = self._major_axis.argmin() if self.title_spacing != 'auto': axis_offset = self.title_spacing if (self.title_spacing) and (axis_offset is None ): if not self.ticklabel_cache: axis_offset = 25 else: axis_offset = max([l._bounding_box[axis_index] for l in self.ticklabel_cache]) * 1.3 offset = (self._origin_point+self._end_axis_point)/2 axis_dist = self.tick_out + tl_bounds[axis_index]/2.0 + axis_offset offset -= self._inside_vector * axis_dist offset += text_center_to_corner gc.translate_ctm(*offset) title_label.draw(gc) gc.translate_ctm(*(-offset)) return def _draw_ticks(self, gc): """ Draws the tick marks for the axis. """ if not self.tick_visible: return gc.set_stroke_color(self.tick_color_) gc.set_line_width(self.tick_weight) gc.set_antialias(False) gc.begin_path() tick_in_vector = self._inside_vector*self.tick_in tick_out_vector = self._inside_vector*self.tick_out for tick_pos in self._tick_positions: gc.move_to(*(tick_pos + tick_in_vector)) gc.line_to(*(tick_pos - tick_out_vector)) gc.stroke_path() return def _draw_labels(self, gc): """ Draws the tick labels for the axis. """ # which axis are we moving away from the axis line along? axis_index = self._major_axis.argmin() inside_vector = self._inside_vector if self.tick_label_position == "inside": inside_vector = -inside_vector for i in range(len(self._tick_label_positions)): #We want a more sophisticated scheme than just 2 decimals all the time ticklabel = self.ticklabel_cache[i] tl_bounds = self._tick_label_bounding_boxes[i] #base_position puts the tick label at a point where the vector #extending from the tick mark inside 8 units #just touches the rectangular bounding box of the tick label. #Note: This is not necessarily optimal for non #horizontal/vertical axes. More work could be done on this. base_position = self._tick_label_positions[i].copy() axis_dist = self.tick_label_offset + tl_bounds[axis_index]/2.0 base_position -= inside_vector * axis_dist base_position -= tl_bounds/2.0 if self.tick_label_alignment == 'corner': if self.orientation in ("top", "bottom"): base_position[0] += tl_bounds[0]/2.0 elif self.orientation == "left": base_position[1] -= tl_bounds[1]/2.0 elif self.orientation == "right": base_position[1] += tl_bounds[1]/2.0 if self.ensure_labels_bounded: bound_idx = self._major_axis.argmax() if i == 0: base_position[bound_idx] = max(base_position[bound_idx], self._origin_point[bound_idx]) elif i == len(self._tick_label_positions)-1: base_position[bound_idx] = min(base_position[bound_idx], self._end_axis_point[bound_idx] - \ tl_bounds[bound_idx]) tlpos = around(base_position) gc.translate_ctm(*tlpos) ticklabel.draw(gc) gc.translate_ctm(*(-tlpos)) return #------------------------------------------------------------------------ # Private methods for computing positions and layout #------------------------------------------------------------------------ def _reset_cache(self): """ Clears the cached tick positions, labels, and label positions. """ self._tick_positions = [] self._tick_label_list = [] self._tick_label_positions = [] return def _compute_tick_positions(self, gc, overlay_component=None): """ Calculates the positions for the tick marks. """ if (self.mapper is None): self._reset_cache() self._cache_valid = True return datalow = self.mapper.range.low datahigh = self.mapper.range.high screenhigh = self.mapper.high_pos screenlow = self.mapper.low_pos if overlay_component is not None: origin = getattr(overlay_component, 'origin', 'bottom left') else: origin = self.origin if self.orientation in ("top", "bottom"): if "right" in origin: flip_from_gc = True else: flip_from_gc = False elif self.orientation in ("left", "right"): if "top" in origin: flip_from_gc = True else: flip_from_gc = False if flip_from_gc: screenlow, screenhigh = screenhigh, screenlow if (datalow == datahigh) or (screenlow == screenhigh) or \ (datalow in [inf, -inf]) or (datahigh in [inf, -inf]): self._reset_cache() self._cache_valid = True return if datalow > datahigh: raise RuntimeError("DataRange low is greater than high; unable to compute axis ticks.") if not self.tick_generator: return if hasattr(self.tick_generator, "get_ticks_and_labels"): # generate ticks and labels simultaneously tmp = self.tick_generator.get_ticks_and_labels(datalow, datahigh, screenlow, screenhigh) if len(tmp) == 0: tick_list = [] labels = [] else: tick_list, labels = tmp # compute the labels here self.ticklabel_cache = [Label(text=lab, font=self.tick_label_font, color=self.tick_label_color) \ for lab in labels] self._tick_label_bounding_boxes = [array(ticklabel.get_bounding_box(gc), float64) \ for ticklabel in self.ticklabel_cache] else: scale = 'log' if isinstance(self.mapper, LogMapper) else 'linear' if self.small_haxis_style: tick_list = array([datalow, datahigh]) else: tick_list = array(self.tick_generator.get_ticks(datalow, datahigh, datalow, datahigh, self.tick_interval, use_endpoints=False, scale=scale), float64) mapped_tick_positions = (array(self.mapper.map_screen(tick_list))-screenlow) / \ (screenhigh-screenlow) self._tick_positions = around(array([self._axis_vector*tickpos + self._origin_point \ for tickpos in mapped_tick_positions])) self._tick_label_list = tick_list self._tick_label_positions = self._tick_positions return def _compute_labels(self, gc): """Generates the labels for tick marks. Waits for the cache to become invalid. """ # tick labels are already computed if hasattr(self.tick_generator, "get_ticks_and_labels"): return formatter = self.tick_label_formatter def build_label(val): tickstring = formatter(val) if formatter is not None else str(val) return Label(text=tickstring, font=self.tick_label_font, color=self.tick_label_color, rotate_angle=self.tick_label_rotate_angle, margin=self.tick_label_margin) self.ticklabel_cache = [build_label(val) for val in self._tick_label_list] self._tick_label_bounding_boxes = [array(ticklabel.get_bounding_box(gc), float) for ticklabel in self.ticklabel_cache] return def _calculate_geometry(self): origin = self.origin screenhigh = self.mapper.high_pos screenlow = self.mapper.low_pos if self.orientation in ('top', 'bottom'): self._major_axis_size = self.bounds[0] self._minor_axis_size = self.bounds[1] self._major_axis = array([1., 0.]) self._title_orientation = array([0.,1.]) if self.orientation == 'top': self._origin_point = array(self.position) self._inside_vector = array([0.,-1.]) else: #self.oriention == 'bottom' self._origin_point = array(self.position) + array([0., self.bounds[1]]) self._inside_vector = array([0., 1.]) if "right" in origin: screenlow, screenhigh = screenhigh, screenlow elif self.orientation in ('left', 'right'): self._major_axis_size = self.bounds[1] self._minor_axis_size = self.bounds[0] self._major_axis = array([0., 1.]) self._title_orientation = array([-1., 0]) if self.orientation == 'left': self._origin_point = array(self.position) + array([self.bounds[0], 0.]) self._inside_vector = array([1., 0.]) else: #self.orientation == 'right' self._origin_point = array(self.position) self._inside_vector = array([-1., 0.]) if "top" in origin: screenlow, screenhigh = screenhigh, screenlow if self.ensure_ticks_bounded: self._origin_point -= self._inside_vector*self.tick_in self._end_axis_point = abs(screenhigh-screenlow)*self._major_axis + self._origin_point self._axis_vector = self._end_axis_point - self._origin_point # This is the vector that represents one unit of data space in terms of screen space. self._axis_pixel_vector = self._axis_vector/sqrt(dot(self._axis_vector,self._axis_vector)) return def _calculate_geometry_overlay(self, overlay_component=None): if overlay_component is None: overlay_component = self component_origin = getattr(overlay_component, "origin", 'bottom left') screenhigh = self.mapper.high_pos screenlow = self.mapper.low_pos if self.orientation in ('top', 'bottom'): self._major_axis_size = overlay_component.bounds[0] self._minor_axis_size = overlay_component.bounds[1] self._major_axis = array([1., 0.]) self._title_orientation = array([0.,1.]) if self.orientation == 'top': self._origin_point = array([overlay_component.x, overlay_component.y2]) self._inside_vector = array([0.0, -1.0]) else: self._origin_point = array([overlay_component.x, overlay_component.y]) self._inside_vector = array([0.0, 1.0]) if "right" in component_origin: screenlow, screenhigh = screenhigh, screenlow elif self.orientation in ('left', 'right'): self._major_axis_size = overlay_component.bounds[1] self._minor_axis_size = overlay_component.bounds[0] self._major_axis = array([0., 1.]) self._title_orientation = array([-1., 0]) if self.orientation == 'left': self._origin_point = array([overlay_component.x, overlay_component.y]) self._inside_vector = array([1.0, 0.0]) else: self._origin_point = array([overlay_component.x2, overlay_component.y]) self._inside_vector = array([-1.0, 0.0]) if "top" in component_origin: screenlow, screenhigh = screenhigh, screenlow if self.ensure_ticks_bounded: self._origin_point -= self._inside_vector*self.tick_in self._end_axis_point = abs(screenhigh-screenlow)*self._major_axis + self._origin_point self._axis_vector = self._end_axis_point - self._origin_point # This is the vector that represents one unit of data space in terms of screen space. self._axis_pixel_vector = self._axis_vector/sqrt(dot(self._axis_vector,self._axis_vector)) return #------------------------------------------------------------------------ # Event handlers #------------------------------------------------------------------------ def _bounds_changed(self, old, new): super(PlotAxis, self)._bounds_changed(old, new) self._layout_needed = True self._invalidate() def _bounds_items_changed(self, event): super(PlotAxis, self)._bounds_items_changed(event) self._layout_needed = True self._invalidate() def _mapper_changed(self, old, new): if old is not None: old.on_trait_change(self.mapper_updated, "updated", remove=True) if new is not None: new.on_trait_change(self.mapper_updated, "updated") self._invalidate() def mapper_updated(self): """ Event handler that is bound to this axis's mapper's **updated** event """ self._invalidate() def _position_changed(self, old, new): super(PlotAxis, self)._position_changed(old, new) self._cache_valid = False def _position_items_changed(self, event): super(PlotAxis, self)._position_items_changed(event) self._cache_valid = False def _position_changed_for_component(self): self._cache_valid = False def _position_items_changed_for_component(self): self._cache_valid = False def _bounds_changed_for_component(self): self._cache_valid = False self._layout_needed = True def _bounds_items_changed_for_component(self): self._cache_valid = False self._layout_needed = True def _origin_changed_for_component(self): self._invalidate() def _updated_fired(self): """If the axis bounds changed, redraw.""" self._cache_valid = False return def _invalidate(self): self._cache_valid = False self.invalidate_draw() if self.component: self.component.invalidate_draw() return def _component_changed(self): if self.mapper is not None: # If there is a mapper set, just leave it be. return # Try to pick the most appropriate mapper for our orientation # and what information we can glean from our component. attrmap = { "left": ("ymapper", "y_mapper", "value_mapper"), "bottom": ("xmapper", "x_mapper", "index_mapper"), } attrmap["right"] = attrmap["left"] attrmap["top"] = attrmap["bottom"] component = self.component attr1, attr2, attr3 = attrmap[self.orientation] for attr in attrmap[self.orientation]: if hasattr(component, attr): self.mapper = getattr(component, attr) break # Keep our origin in sync with the component self.origin = getattr(component, 'origin', 'bottom left') return #------------------------------------------------------------------------ # The following event handlers just invalidate our previously computed # Label instances and backbuffer if any of our visual attributes change. # TODO: refactor this stuff and the caching of contained objects (e.g. Label) #------------------------------------------------------------------------ def _title_changed(self): self.invalidate_draw() if self.component: self.component.invalidate_draw() return def _anytrait_changed(self, name, old, new): """ For every trait that defines a visual attribute we just call _invalidate() when a change is made. """ invalidate_traits = [ 'title_font', 'title_spacing', 'title_color', 'title_angle', 'tick_weight', 'tick_color', 'tick_label_font', 'tick_label_color', 'tick_label_rotate_angle', 'tick_label_alignment', 'tick_label_margin', 'tick_label_offset', 'tick_label_position', 'tick_label_formatter', 'tick_in', 'tick_out', 'tick_visible', 'tick_interval', 'tick_generator', 'orientation', 'origin', 'axis_line_visible', 'axis_line_color', 'axis_line_weight', 'axis_line_style', 'small_haxis_style', 'ensure_labels_bounded', 'ensure_ticks_bounded', ] if name in invalidate_traits: self._invalidate() # ------------------------------------------------------------------------ # Initialization-related methods # ------------------------------------------------------------------------ def _title_angle_default(self): if self.orientation == 'left': return 90.0 if self.orientation == 'right': return 270.0 # Then self.orientation in {'top', 'bottom'} return 0.0 #------------------------------------------------------------------------ # Persistence-related methods #------------------------------------------------------------------------ def __getstate__(self): dont_pickle = [ '_tick_list', '_tick_positions', '_tick_label_list', '_tick_label_positions', '_tick_label_bounding_boxes', '_major_axis_size', '_minor_axis_size', '_major_axis', '_title_orientation', '_title_angle', '_origin_point', '_inside_vector', '_axis_vector', '_axis_pixel_vector', '_end_axis_point', '_ticklabel_cache', '_cache_valid' ] state = super(PlotAxis,self).__getstate__() for key in dont_pickle: if key in state: del state[key] return state def __setstate__(self, state): super(PlotAxis,self).__setstate__(state) self._mapper_changed(None, self.mapper) self._reset_cache() self._cache_valid = False return
class MultiLinePlot(BaseXYPlot): """ A plot consisting of multiple lines. The data to be plotted must come from a two-dimensional array with shape M by N stored in a TraceArrayDataSource object. M is the number of lines to be plotted, and N is the number of points in each line. Constructor Parameters ---------------------- index : instance of an ArrayDataSource These are the 'x' or abscissa coordinates. yindex : instance of ArrayDataSource These are the 'y' coordinates. value : instance of a MultiArrayDataSource Note that the `scale`, `offset` and `normalized_amplitude` attributes of the MultiArrayDataSource control the projection of the traces into the (x,y) plot. In simplest case, `scale=1` and `offset=0`, and `normalized_amplitude` controls the scaling of the traces relative to their base y value. global_min, global_max : float The minimum and maximum values of the data in `value`. For large arrays, computing these could take excessive time, so they must be provided when an instance is created. normalized_amplitude : Float color : ColorTrait color_func : Callable or None If not None, this Callable overrides `color`. The argument to `color_func` will be the integer index of the trace to be rendered. `color_func` must return an RGBA 4-tuple. Default: None orientation : str Must be 'v' or 'h' (for 'vertical' or 'horizontal', respectively). This is the orientation of the index axis (i.e. the 'x' axis). Default: 'h' fast_clip : bool If True, traces whose *base* 'y' coordinate is outside the value axis range are not plotted, even if some of the data in the curve extends into the plot region. Default: False line_width : float Width of the plotted lines. line_style : The style of the trace lines in the plot. The following are from the original LinePlot code, and are untested: selected_color selected_line_style """ # M and N appearing in the comments are as defined in the docstring. yindex = Instance(ArrayDataSource) # amplitude = Float(0.0) # `scale` and `offset` provide a more general transformation, but are currently # untested. scale = Float(1.0) offset = Float(0.0) fast_clip = Bool(False) # The color of the lines. color = black_color_trait # A function that returns the color of lines. Overrides `color` if not None. color_func = Trait(None, None, Callable) # The color to use to highlight the line when selected. selected_color = ColorTrait("lightyellow") # The style of the selected line. selected_line_style = LineStyle("solid") # The name of the key in self.metadata that holds the selection mask metadata_name = Str("selections") # The thickness of the line. line_width = Float(1.0) # The line dash style. line_style = LineStyle use_global_bounds = Bool(True) # Minimum value in the `value` data source. This must be provided # in the call to the constructor. global_min = Float # Maximum value in the `value` data source. This must be provided # in the call to the constructor. global_max = Float # Normalized amplitude is the value exposed to the user. normalized_amplitude = Float(-0.5) amplitude_scale = Property(Float, depends_on=[ 'global_min', 'global_max', 'data', 'use_global_bounds', 'yindex' ]) amplitude = Property( Float, depends_on=['normalized_amplitude', 'amplitude_scale']) #------------------------------------------------------------------------ # Private traits #------------------------------------------------------------------------ # The projected 2D numpy array. _trace_data = Property(Array, depends_on=[ 'index', 'index.data_changed', 'value', 'value.data_changed', 'yindex', 'yindex.data_changed', 'amplitude', 'scale', 'offset' ]) # Cached list of non-NaN arrays of (x,y) data-space points; regardless of # self.orientation, this is always stored as (index_pt, value_pt). This is # different from the default BaseXYPlot definition. _cached_data_pts = List # Cached list of non-NaN arrays of (x,y) screen-space points. _cached_screen_pts = List #------------------------------------------------------------------------ # #------------------------------------------------------------------------ def trait_view(self, obj): """Create a minimalist View, with just the amplitude and color attributes.""" # Minimalist Traits UI View for customizing the plot: only the trace amplitude # and line color are exposed. view = View( HGroup( Item('use_global_bounds'), # Item('normalized_amplitude'), # Item('normalized_amplitude', editor=RangeEditor()), Item('normalized_amplitude', editor=ScrubberEditor(increment=0.2, hover_color=0xFFFFFF, active_color=0xA0CD9E, border_color=0x0000FF)), ), Item("color", label="Trace color", style="simple"), width=480, title="Trace Plot Line Attributes", buttons=["OK", "Cancel"]) return view #------------------------------------------------------------------------ # #------------------------------------------------------------------------ # See base_xy_plot.py for these: ## def hittest(self, screen_pt, threshold=7.0): ## def interpolate(self, index_value): def get_screen_points(self): self._gather_points() scrn_pts_list = [[self.map_screen(ary) for ary in line] for line in self._cached_data_pts] return scrn_pts_list #------------------------------------------------------------------------ # Private methods #------------------------------------------------------------------------ @cached_property def _get_amplitude_scale(self): """ If the amplitude is set to this value, the largest trace deviation from its base y coordinate will be equal to the y coordinate spacing. """ # Note: Like the rest of the current code, this ignores the `scale` attribute. if self.yindex is not None: coordinates = self.yindex.get_data() else: coordinates = [] if len(coordinates) > 1: dy = coordinates[1] - coordinates[0] else: # default coordinate spacing if there is only 1 coordinate dy = 1.0 if self.use_global_bounds: max_abs = max(abs(self.global_min), abs(self.global_max)) else: data = self.value._data max_abs = np.max(np.abs(data)) if max_abs == 0: amp_scale = 0.5 * dy else: amp_scale = 0.5 * dy / max_abs return amp_scale @cached_property def _get_amplitude(self): amplitude = self.normalized_amplitude * self.amplitude_scale return amplitude @cached_property def _get__trace_data(self): """Compute the transformed data.""" # Get the array from `value` data = self.value._data coordinates = self.yindex.get_data() channel_data = self.scale*(self.amplitude*data + coordinates[:,np.newaxis]) \ + self.offset return channel_data def _gather_points(self): """ Collects the data points that are within the bounds of the plot and caches them. """ if self._cache_valid: return if not self.index or not self.value: return index = self.index.get_data() varray = self._trace_data if varray.size == 0: self._cached_data_pts = [] self._cached_valid = True return coordinates = self.yindex.get_data() if self.fast_clip: coord_min = float(coordinates[0]) coord_max = coordinates[-1] slice_min = max( 0, ceil((varray.shape[0] - 1) * (self.value_range.low - coord_min) / (coord_max - coord_min))) slice_max = min( varray.shape[0], 1 + floor((varray.shape[0] - 1) * (self.value_range.high - coord_min) / (coord_max - coord_min))) varray = varray[slice_min:slice_max] # FIXME: The y coordinates must also be sliced to match varray. # Check to see if the data is completely outside the view region. outside = False # Check x coordinates. low, high = self.index.get_bounds() if low > self.index_range.high or high < self.index_range.low: outside = True # Check y coordinates. Use varray because it is nased on the yindex, # but has been shifted up or down depending on the values. ylow, yhigh = varray.min(), varray.max() if ylow > self.value_range.high or yhigh < self.value_range.low: outside = True if outside: self._cached_data_pts = [] self._cached_valid = True return if len(index) == 0 or varray.shape[0] == 0 or varray.shape[1] == 0 \ or len(index) != varray.shape[1]: self._cached_data_pts = [] self._cache_valid = True return size_diff = varray.shape[1] - len(index) if size_diff > 0: warnings.warn('Chaco.LinePlot: value.shape[1] %d - len(index) %d = %d\n' \ % (varray.shape[1], len(index), size_diff)) index_max = len(index) varray = varray[:, :index_max] else: index_max = varray.shape[1] index = index[:index_max] # Split the index and value raw data into non-NaN chunks. # nan_mask is a boolean M by N array. nan_mask = invert(isnan(varray)) & invert(isnan(index)) blocks_list = [] for nm in nan_mask: blocks = [b for b in arg_find_runs(nm, "flat") if nm[b[0]] != 0] blocks_list.append(blocks) line_points = [] for k, blocks in enumerate(blocks_list): points = [] for block in blocks: start, end = block block_index = index[start:end] block_value = varray[k, start:end] index_mask = self.index_mapper.range.mask_data(block_index) runs = [r for r in arg_find_runs(index_mask, "flat") \ if index_mask[r[0]] != 0] # Check to see if our data view region is between two points in the # index data. If so, then we have to reverse map our current view # into the appropriate index and draw the bracketing points. if runs == []: data_pt = self.map_data( (self.x_mapper.low_pos, self.y_mapper.low_pos)) if self.index.sort_order == "none": indices = argsort(index) sorted_index = take(index, indices) sorted_value = take(varray[k], indices) sort = 1 else: sorted_index = index sorted_value = varray[k] if self.index.sort_order == "ascending": sort = 1 else: sort = -1 ndx = bin_search(sorted_index, data_pt, sort) if ndx == -1: # bin_search can return -1 if data_pt is outside the bounds # of the source data continue z = transpose( array((sorted_index[ndx:ndx + 2], sorted_value[ndx:ndx + 2]))) points.append(z) else: # Expand the width of every group of points so we draw the lines # up to their next point, outside the plot area data_end = len(index_mask) for run in runs: start, end = run if start != 0: start -= 1 if end != data_end: end += 1 run_data = transpose( array((block_index[start:end], block_value[start:end]))) points.append(run_data) line_points.append(points) self._cached_data_pts = line_points self._cache_valid = True return # See base_xy_plot.py for: ## def _downsample(self): ## def _downsample_vectorized(self): def _render(self, gc, line_points, selected_points=None): if len(line_points) == 0: return with gc: gc.set_antialias(True) gc.clip_to_rect(self.x, self.y, self.width, self.height) render = self._render_normal if selected_points is not None: gc.set_stroke_color(self.selected_color_) gc.set_line_width(self.line_width + 10.0) gc.set_line_dash(self.selected_line_style_) render(gc, selected_points) if self.color_func is not None: # Existence of self.color_func overrides self.color. color_func = self.color_func else: color_func = lambda k: self.color_ tmp = list(enumerate(line_points)) # Note: the list is reversed for testing with _render_filled. for k, points in reversed(tmp): color = color_func(k) gc.set_stroke_color(color) gc.set_line_width(self.line_width) gc.set_line_dash(self.line_style_) render(gc, points) # Draw the default axes, if necessary self._draw_default_axes(gc) def _render_normal(self, gc, points): for ary in points: if len(ary) > 0: gc.begin_path() gc.lines(ary) gc.stroke_path() return def _render_icon(self, gc, x, y, width, height): with gc: gc.set_stroke_color(self.color_) gc.set_line_width(self.line_width) gc.set_line_dash(self.line_style_) gc.set_antialias(0) gc.move_to(x, y + height / 2) gc.line_to(x + width, y + height / 2) gc.stroke_path() def _alpha_changed(self): self.color_ = self.color_[0:3] + (self.alpha, ) self.invalidate_draw() self.request_redraw() return def _default_color(self): return black_color_trait def _color_changed(self): self.invalidate_draw() self.request_redraw() return def _line_style_changed(self): self.invalidate_draw() self.request_redraw() return def _line_width_changed(self): self.invalidate_draw() self.request_redraw() return def _amplitude_changed(self): self.value.data_changed = True self.invalidate_draw() self.request_redraw() return def __getstate__(self): state = super(MultiLinePlot, self).__getstate__() for key in ['traits_view']: if state.has_key(key): del state[key] return state
class LinePlot(BaseXYPlot): """ A plot consisting of a line. This is the most fundamental object to use to create line plots. However, it is somewhat low-level and therefore creating one properly to do what you want can require some verbose code. The create_line_plot() function in plot_factory.py can hide some of this verbosity for common cases. """ # The color of the line. color = black_color_trait # The RGBA tuple for rendering lines. It is always a tuple of length 4. # It has the same RGB values as color_, and its alpha value is the alpha # value of self.color multiplied by self.alpha. effective_color = Property(Tuple, depends_on=['color', 'alpha']) # The color to use to highlight the line when selected. selected_color = ColorTrait("lightyellow") # The style of the selected line. selected_line_style = LineStyle("solid") # The name of the key in self.metadata that holds the selection mask metadata_name = Str("selections") # The thickness of the line. line_width = Float(1.0) # The line dash style. line_style = LineStyle # The rendering style of the line plot. # # connectedpoints # "normal" style (default); each point is connected to subsequent and # prior points by line segments # hold # each point is represented by a line segment parallel to the abscissa # (index axis) and spanning the length between the point and its # subsequent point. # connectedhold # like "hold" style, but line segments are drawn at each point of the # plot to connect the hold lines of the prior point and the current # point. Also called a "right angle plot". render_style = Enum("connectedpoints", "hold", "connectedhold") # Traits UI View for customizing the plot. traits_view = View(Item("color", style="custom"), "line_width", "line_style", buttons=["OK", "Cancel"]) #------------------------------------------------------------------------ # Private traits #------------------------------------------------------------------------ # Cached list of non-NaN arrays of (x,y) data-space points; regardless of # self.orientation, this is always stored as (index_pt, value_pt). This is # different from the default BaseXYPlot definition. _cached_data_pts = List # Cached list of non-NaN arrays of (x,y) screen-space points. _cached_screen_pts = List def hittest(self, screen_pt, threshold=7.0, return_distance=False): """ Tests whether the given screen point is within *threshold* pixels of any data points on the line. If so, then it returns the (x,y) value of a data point near the screen point. If not, then it returns None. """ # First, check screen_pt is directly on a point in the lineplot ndx = self.map_index(screen_pt, threshold) if ndx is not None: # screen_pt is one of the points in the lineplot data_pt = (self.index.get_data()[ndx], self.value.get_data()[ndx]) if return_distance: scrn_pt = self.map_screen(data_pt) dist = sqrt((screen_pt[0] - scrn_pt[0])**2 + (screen_pt[1] - scrn_pt[1])**2) return (data_pt[0], data_pt[1], dist) else: return data_pt else: # We now must check the lines themselves # Must check all lines within threshold along the major axis, # so determine the bounds of the region of interest in dataspace if self.orientation == "h": dmax = self.map_data((screen_pt[0] + threshold, screen_pt[1])) dmin = self.map_data((screen_pt[0] - threshold, screen_pt[1])) else: dmax = self.map_data((screen_pt[0], screen_pt[1] + threshold)) dmin = self.map_data((screen_pt[0], screen_pt[1] - threshold)) xmin, xmax = self.index.get_bounds() # Now compute the bounds of the region of interest as indexes if dmin < xmin: ndx1 = 0 elif dmin > xmax: ndx1 = len(self.value.get_data()) - 1 else: ndx1 = reverse_map_1d(self.index.get_data(), dmin, self.index.sort_order) if dmax < xmin: ndx2 = 0 elif dmax > xmax: ndx2 = len(self.value.get_data()) - 1 else: ndx2 = reverse_map_1d(self.index.get_data(), dmax, self.index.sort_order) start_ndx = max(0, min( ndx1 - 1, ndx2 - 1, )) end_ndx = min( len(self.value.get_data()) - 1, max(ndx1 + 1, ndx2 + 1)) # Compute the distances to all points in the range of interest start = array([ self.index.get_data()[start_ndx:end_ndx], self.value.get_data()[start_ndx:end_ndx] ]) end = array([ self.index.get_data()[start_ndx + 1:end_ndx + 1], self.value.get_data()[start_ndx + 1:end_ndx + 1] ]) # Convert to screen points s_start = transpose(self.map_screen(transpose(start))) s_end = transpose(self.map_screen(transpose(end))) # t gives the parameter of the closest point to screen_pt # on the line going from s_start to s_end t = _closest_point(screen_pt, s_start, s_end) # Restrict to points on the line segment s_start->s_end t = clip(t, 0, 1) # Gives the corresponding point on the line px, py = _t_to_point(t, s_start, s_end) # Calculate distances dist = sqrt((px - screen_pt[0])**2 + (py - screen_pt[1])**2) # Find the minimum n = argmin(dist) # And return if it is good if dist[n] <= threshold: best_pt = self.map_data((px[n], py[n]), all_values=True) if return_distance: return [best_pt[0], best_pt[1], dist[n]] else: return best_pt return None def interpolate(self, index_value): """ Returns the value of the plot at the given index value in screen space. Raises an IndexError when *index_value* exceeds the bounds of indexes on the value. """ if self.index is None or self.value is None: raise IndexError, "cannot index when data source index or value is None" index_data = self.index.get_data() value_data = self.value.get_data() ndx = reverse_map_1d(index_data, index_value, self.index.sort_order) # quick test to see if this value is already in the index array if index_value == index_data[ndx]: return value_data[ndx] # get x and y values to interpolate between if index_value < index_data[ndx]: x0 = index_data[ndx - 1] y0 = value_data[ndx - 1] x1 = index_data[ndx] y1 = value_data[ndx] else: x0 = index_data[ndx] y0 = value_data[ndx] x1 = index_data[ndx + 1] y1 = value_data[ndx + 1] if x1 != x0: slope = float(y1 - y0) / float(x1 - x0) dx = index_value - x0 yp = y0 + slope * dx else: yp = inf return yp def get_screen_points(self): self._gather_points() if self.use_downsampling: return self._downsample() else: return [self.map_screen(ary) for ary in self._cached_data_pts] #------------------------------------------------------------------------ # Private methods; implements the BaseXYPlot stub methods #------------------------------------------------------------------------ def _gather_points(self): """ Collects the data points that are within the bounds of the plot and caches them. """ if not self._cache_valid: if self.index is None or self.value is None: return index = self.index.get_data() value = self.value.get_data() # Check to see if the data is completely outside the view region for ds, rng in ((self.index, self.index_range), (self.value, self.value_range)): low, high = ds.get_bounds() if low > rng.high or high < rng.low: self._cached_data_pts = [] self._cached_valid = True return if len(index) == 0 or len(value) == 0 or len(index) != len(value): self._cached_data_pts = [] self._cache_valid = True size_diff = len(value) - len(index) if size_diff > 0: warnings.warn('Chaco.LinePlot: len(value) %d - len(index) %d = %d\n' \ % (len(value), len(index), size_diff)) index_max = len(index) value = value[:index_max] else: index_max = len(value) index = index[:index_max] # TODO: restore the functionality of rendering highlighted portions # of the line #selection = self.index.metadata.get(self.metadata_name, None) #if selection is not None and type(selection) in (ndarray, list) and \ # len(selection) > 0: # Split the index and value raw data into non-NaN chunks mask = invert(isnan(value)) & invert(isnan(index)) # throw out index and value points outside the visible region mask = intersect_range(index, self.index_range.low, self.index_range.high, mask) mask = intersect_range(value, self.value_range.low, self.value_range.high, mask) points = [ column_stack([index[start:end], value[start:end]]) for start, end in arg_true_runs(mask) ] self._cached_data_pts = points self._cache_valid = True def _downsample(self): if not self._screen_cache_valid: m = self.index_mapper delta_screen = int(m.high_pos - m.low_pos) if delta_screen == 0: downsampled = [] else: # TODO: implement other downsampling methods from chaco.downsample.lttb import largest_triangle_three_buckets downsampled = [ largest_triangle_three_buckets(p, delta_screen) for p in self._cached_data_pts ] self._cached_screen_pts = [self.map_screen(p) for p in downsampled] self._screen_cache_valid = True return self._cached_screen_pts def _render(self, gc, points, selected_points=None): if len(points) == 0: return with gc: gc.set_antialias(True) gc.clip_to_rect(self.x, self.y, self.width, self.height) render_method_dict = { "hold": self._render_hold, "connectedhold": self._render_connected_hold, "connectedpoints": self._render_normal } render = render_method_dict.get(self.render_style, self._render_normal) if selected_points is not None: gc.set_stroke_color(self.selected_color_) gc.set_line_width(self.line_width + 10.0) gc.set_line_dash(self.selected_line_style_) render(gc, selected_points, self.orientation) # Render using the normal style gc.set_stroke_color(self.effective_color) gc.set_line_width(self.line_width) gc.set_line_dash(self.line_style_) render(gc, points, self.orientation) # Draw the default axes, if necessary self._draw_default_axes(gc) @classmethod def _render_normal(cls, gc, points, orientation): for ary in points: if len(ary) > 0: gc.begin_path() gc.lines(ary) gc.stroke_path() return @classmethod def _render_hold(cls, gc, points, orientation): for starts in points: x, y = starts.T if orientation == "h": ends = transpose(array((x[1:], y[:-1]))) else: ends = transpose(array((x[:-1], y[1:]))) gc.begin_path() gc.line_set(starts[:-1], ends) gc.stroke_path() return @classmethod def _render_connected_hold(cls, gc, points, orientation): for starts in points: x, y = starts.T if orientation == "h": ends = transpose(array((x[1:], y[:-1]))) else: ends = transpose(array((x[:-1], y[1:]))) gc.begin_path() gc.line_set(starts[:-1], ends) gc.line_set(ends, starts[1:]) gc.stroke_path() return def _render_icon(self, gc, x, y, width, height): with gc: gc.set_stroke_color(self.effective_color) gc.set_line_width(self.line_width) gc.set_line_dash(self.line_style_) gc.set_antialias(0) gc.move_to(x, y + height / 2) gc.line_to(x + width, y + height / 2) gc.stroke_path() return def _downsample_vectorized(self): """ Analyzes the screen-space points stored in self._cached_data_pts and replaces them with a downsampled set. """ pts = self._cached_screen_pts #.astype(int) # some boneheaded short-circuits m = self.index_mapper if (pts.shape[0] < 400) or (pts.shape[0] < m.high_pos - m.low_pos): return pts2 = concatenate((array([[0.0, 0.0]]), pts[:-1])) z = abs(pts - pts2) d = z[:, 0] + z[:, 1] #... TODO ... return def _alpha_changed(self): self.invalidate_draw() self.request_redraw() return def _color_changed(self): self.invalidate_draw() self.request_redraw() return def _line_style_changed(self): self.invalidate_draw() self.request_redraw() return def _line_width_changed(self): self.invalidate_draw() self.request_redraw() return def __getstate__(self): state = super(LinePlot, self).__getstate__() for key in ['traits_view']: if state.has_key(key): del state[key] return state @cached_property def _get_effective_color(self): alpha = self.color_[-1] if len(self.color_) == 4 else 1 c = self.color_[:3] + (alpha * self.alpha, ) return c
class StonerPlot(HasTraits, S.DataFile): """A simple x-y plotting program to play with""" plot = Instance(Component) color = ColorTrait("blue") line_style = LineStyle() line_color = ColorTrait("blue") marker = marker_trait() marker_size = Int(4) outline_width = Int(1) xc = Str("X") yc = Str("Y") xm = Enum("Linear Scale", "Log Scale") ym = Enum("Linear Scale", "Log Scale") mappers = {"Linear Scale": LinearMapper, "Log Scale": LogMapper} p_type = Enum('scatter', 'line', 'scatter and line') data = Array metadata = Dict column_headers = List(['X', 'Y']) def _create_plot(self, orientation="h", p_type=ToolbarPlot): """ Creates a plot from a single Nx2 data array or a tuple of two length-N 1-D arrays. The data must be sorted on the index if any reverse-mapping tools are to be used. Pre-existing "index" and "value" datasources can be passed in. """ index = ArrayDataSource(self.column(self.xc), sort_order="none") value = ArrayDataSource(self.column(self.yc)) index_range = DataRange1D() index_range.add(index) index_mapper = self.mappers[self.xm](range=index_range) value_range = DataRange1D() value_range.add(value) value_mapper = self.mappers[self.ym](range=value_range) if self.p_type == "line" and self.outline_width <= 0: self.outline_width = 1 plot = p_type(index=index, value=value, index_mapper=index_mapper, value_mapper=value_mapper, orientation=orientation, border_visible=True, bgcolor="transparent") if issubclass(p_type, ScatterPlot): plot.marker = self.marker plot.marker_size = self.marker_size plot.outline_color = self.line_color plot.color = self.color elif issubclass(p_type, LinePlot): plot.line_wdith = self.outline_width plot.line_style = self.line_style plot.color = self.line_color return plot def _create_plot_component(self): container = OverlayPlotContainer(padding=50, fill_padding=True, bgcolor="white", use_backbuffer=True) types = {"line": LinePlot, "scatter": ScatterPlot} # Create the initial X-series of data if len(self) > 0: # Only create a plot if we ahve datat if self.p_type == "scatter and line": lineplot = self._create_plot(p_type=LinePlot) #lineplot.selected_color = "none" scatter = self._create_plot(p_type=ScatterPlot) scatter.bgcolor = "white" scatter.index_mapper = lineplot.index_mapper scatter.value_mapper = lineplot.value_mapper add_default_grids(scatter) add_default_axes(scatter) container.add(lineplot) container.add(scatter) else: plot = self._create_plot(p_type=types[self.p_type]) add_default_grids(plot) add_default_axes(plot) container.add(plot) scatter = plot scatter.tools.append(PanTool(scatter, drag_button="left")) # The ZoomTool tool is stateful and allows drawing a zoom # box to select a zoom region. zoom = ZoomTool(scatter, tool_mode="box", always_on=True, drag_button="right") scatter.overlays.append(zoom) csr = CursorTool(scatter, color="black", drag_button="left") scatter.overlays.append(csr) self.plot = container return container def _plot_default(self): return self._create_plot_component() def trait_view(self, parent=None): traits_view = View(self.tabs, menubar=self.menubar, width=1024, height=768, resizable=True, title="Stoner Plotter", handler=MenuController) return traits_view def __init__(self, *args, **kargs): super(StonerPlot, self).__init__(*args, **kargs) self.data = numpy.zeros((2, 2)) acols = [(self.column_headers[i], i) for i in range(len(self.column_headers))] acols[:0] = [("index", "index")] self.adapter = ArrayAdapter() self.adapter.columns = acols self.plotgroup = Group(HGroup( VGroup( Item('xc', label='X Column', editor=CheckListEditor(name='column_headers')), Item('xm', label="X Scale")), VGroup( Item('yc', label='Y Column', editor=CheckListEditor(name='column_headers')), Item('ym', label="Y scale")), Item('p_type', label='Plot Type')), HGroup( Item('color', label="Colour", style="simple", width=75, visible_when='"scatter" in p_type'), Item('line_color', label="Line Colour", style="simple", visible_when='outline_width>0', width=75), Item('marker', label="Marker", visible_when='"scatter" in p_type'), Item('line_style', label='Line Style', visible_when="'line' in p_type"), Item('marker_size', label="Marker Size", visible_when='"scatter" in p_type'), Item('outline_width', label="Line Width")), Item('plot', editor=ComponentEditor(), show_label=False), label="Plot", orientation="vertical") self.datagroup = HGroup(Item( 'data', show_label=False, style='readonly', editor=TabularEditor(adapter=self.adapter)), Item('metadata', editor=ValueEditor(), show_label=False, width=0.25), label="Data") self.tabs = Tabbed(self.plotgroup, self.datagroup, orientation="horizontal") self.menubar = MenuBar( Menu( Action(name='E&xit', accelerator="Ctrl+Q", tooltip="E&xit", action='_on_close'), Separator(), Action(name="&Open", accelerator="Ctrl+O", tooltip="&Open Data File", action="load"), # these callbacks Action(name="&Close", accelerator="Ctrl+W", tooltip="&Close Plot", action="close_plot"), # these callbacks name="File")) self._paint() def _load(self): d = S.DataFile(False) self.metadata = d.metadata self.column_headers = d.column_headers self.data = d.data acols = [(self.column_headers[i], i) for i in range(len(self.column_headers))] acols[:0] = [("index", "index")] self.adapter.columns = acols self.xc = self.column_headers[0] self.yc = self.column_headers[1] self._paint() def _paint(self): self.plot = self._create_plot_component() self.renderer = self.plot.components[0] def _set_renderer(self, attr, value): pp = range(len(self.plot.components)) pp.reverse() t = {attr: value} for p in pp: plot = self.plot.components[p] if isinstance(plot, LinePlot) and attr == "outline_color": print value plot.set(**t) else: plot.set(**t) plot.invalidate_and_redraw() def _color_changed(self): self._set_renderer("color", self.color) def _line_color_changed(self): self._set_renderer("outline_color", self.line_color) def _marker_changed(self): self._set_renderer("marker", self.marker) def _line_style_changed(self): self._set_renderer("line_style", self.line_style) def _p_type_changed(self): self._paint() def _marker_size_changed(self): self._set_renderer("marker_size", self.marker_size) def _outline_width_changed(self): self._set_renderer("line_width", self.outline_width) def _xc_changed(self): xc = self.xc data = self.column(xc) self.renderer.xlabel = xc self.renderer.index = ArrayDataSource(data) self.renderer.index_mapper.range.set_bounds(min(data), max(data)) def _yc_changed(self): yc = self.yc self.renderer.ylabel = yc data = self.column(yc) self.renderer.value = ArrayDataSource(data) self.renderer.value_mapper.range.set_bounds(min(data), max(data)) def _xm_changed(self): self._paint() def _ym_changed(self): self._paint()
class LineInspector(BaseTool): """ A simple tool to draw a line parallel to the index or the value axis of an X-Y plot. This tool supports only plots with a 1-D index. """ #: The axis that this tool is parallel to. axis = Enum("index", "value", "index_x", "index_y") #: The possible inspection modes of the tool. #: #: space: #: The tool maps from screen space into the data space of the plot. #: indexed: #: The tool maps from screen space to an index into the plot's index array. inspect_mode = Enum("space", "indexed") #: Respond to user mouse events? is_interactive = Bool(True) #: Does the tool respond to updates in the metadata on the data source #: and update its own position? is_listener = Bool(False) #: If interactive, does the line inspector write the current data space point #: to the appropriate data source's metadata? write_metadata = Bool(False) #: The name of the metadata field to listen or write to. metadata_name = Str("selections") #------------------------------------------------------------------------ # Override default values of inherited traits in BaseTool #------------------------------------------------------------------------ #: This tool is visible (overrides BaseTool). visible = True #: This tool is drawn as an overlay (overrides BaseTool). draw_mode = "overlay" # TODO:STYLE #: Color of the line. color = ColorTrait("black") #: Width in pixels of the line. line_width = Float(1.0) #: Dash style of the line. line_style = LineStyle("solid") # Last recorded position of the mouse _last_position = Trait(None, Any) def draw(self, gc, view_bounds=None): """ Draws this tool on a graphics context. Overrides BaseTool. """ # We draw at different points depending on whether or not we are # interactive. If both listener and interactive are true, then the # selection metadata on the plot component takes precendence. plot = self.component if plot is None: return if self.is_listener: tmp = self._get_screen_pts() elif self.is_interactive: tmp = self._last_position if tmp: sx, sy = tmp else: return if self.axis == "index" or self.axis == "index_x": if plot.orientation == "h" and sx is not None: self._draw_vertical_line(gc, sx) elif sy is not None: self._draw_horizontal_line(gc, sy) else: # self.axis == "value" if plot.orientation == "h" and sy is not None: self._draw_horizontal_line(gc, sy) elif sx is not None: self._draw_vertical_line(gc, sx) return def do_layout(self, *args, **kw): pass def overlay(self, component, gc, view_bounds=None, mode="normal"): """ Draws this component overlaid on a graphics context. """ self.draw(gc, view_bounds) return def normal_mouse_move(self, event): """ Handles the mouse being moved. """ if not self.is_interactive: return plot = self.component if plot is not None: self._last_position = (event.x, event.y) if isinstance(plot, BaseXYPlot): if self.write_metadata: if self.inspect_mode == "space": index_coord, value_coord = \ self._map_to_data(event.x, event.y) plot.index.metadata[self.metadata_name] = index_coord plot.value.metadata[self.metadata_name] = value_coord else: ndx = plot.map_index((event.x, event.y), threshold=5.0, index_only=True) if ndx: plot.index.metadata[self.metadata_name] = ndx plot.value.metadata[self.metadata_name] = ndx elif isinstance(plot, Base2DPlot): if self.write_metadata: try: old_x_data, old_y_data = \ plot.index.metadata[self.metadata_name] except: old_x_data, old_y_data = (None, None) if self.inspect_mode == "space": if plot.orientation == "h": x_coord, y_coord = \ plot.map_data([(event.x, event.y)])[0] else: y_coord, x_coord = \ plot.map_data([(event.x, event.y)])[0] if self.axis == "index_x": metadata = x_coord, old_y_data elif self.axis == "index_y": metadata = old_x_data, y_coord else: raise ValueError(self.axis) else: if plot.orientation == "h": x_ndx, y_ndx = plot.map_index((event.x, event.y), threshold=5.0) else: y_ndx, x_ndx = plot.map_index((event.x, event.y), threshold=5.0) if self.axis == "index_x": metadata = x_ndx, old_y_data elif self.axis == "index_y": metadata = old_x_data, y_ndx plot.index.metadata[self.metadata_name] = metadata plot.request_redraw() return def normal_mouse_leave(self, event): """ Handles the mouse leaving the plot. """ if not self.is_interactive: return self._last_position = None plot = self.component if plot is not None: if self.write_metadata: if isinstance(plot, BaseXYPlot): plot.index.metadata.pop(self.metadata_name, None) plot.value.metadata.pop(self.metadata_name, None) elif isinstance(plot, Base2DPlot): plot.index.metadata.pop(self.metadata_name, None) plot.request_redraw() return #------------------------------------------------------------------------ # Private methods #------------------------------------------------------------------------ def _get_screen_pts(self): """ Returns the screen-space coordinates of the selected point on the plot component as a tuple (x, y). A dimension that doesn't have a selected point has the value None at its index in the tuple, or won't have the key. """ plot = self.component if plot is None: return retval = [None, None] if isinstance(plot, BaseXYPlot): index_coord = plot.index.metadata.get(self.metadata_name, None) value_coord = plot.value.metadata.get(self.metadata_name, None) if index_coord not in (None, []): if self.inspect_mode == "indexed": index_coord = plot.index.get_data()[index_coord] retval[0] = plot.index_mapper.map_screen(index_coord) if value_coord not in (None, []): if self.inspect_mode == "indexed": value_coord = plot.index.get_data()[value_coord] retval[1] = plot.value_mapper.map_screen(value_coord) elif isinstance(plot, Base2DPlot): try: x_coord, y_coord = plot.index.metadata[self.metadata_name] except: x_coord, y_coord = (None, None) if x_coord not in (None, []): if self.inspect_mode == "indexed": x_coord = plot.index.get_data()[0].get_data()[x_coord] retval[0] = plot.index_mapper._xmapper.map_screen(x_coord) if y_coord not in (None, []): if self.inspect_mode == "indexed": y_coord = plot.index.get_data()[1].get_data()[y_coord] retval[1] = plot.index_mapper._ymapper.map_screen(y_coord) if plot.orientation == "h": return retval else: return retval[1], retval[0] def _map_to_data(self, x, y): """ Returns the data space coordinates of the given x and y. Takes into account orientation of the plot and the axis setting. """ plot = self.component if plot.orientation == "h": index = plot.index_mapper.map_data(x) value = plot.value_mapper.map_data(y) else: index = plot.index_mapper.map_data(y) value = plot.value_mapper.map_data(x) return index, value def _draw_vertical_line(self, gc, sx): """ Draws a vertical line through screen point (sx,sy) having the height of the tool's component. """ if sx < self.component.x or sx > self.component.x2: return with gc: gc.set_stroke_color(self.color_) gc.set_line_width(self.line_width) gc.set_line_dash(self.line_style_) gc.move_to(sx, self.component.y) gc.line_to(sx, self.component.y2) gc.stroke_path() return def _draw_horizontal_line(self, gc, sy): """ Draws a horizontal line through screen point (sx,sy) having the width of the tool's component. """ if sy < self.component.y or sy > self.component.y2: return with gc: gc.set_stroke_color(self.color_) gc.set_line_width(self.line_width) gc.set_line_dash(self.line_style_) gc.move_to(self.component.x, sy) gc.line_to(self.component.x2, sy) gc.stroke_path() return
class LineScatterPlot1D(Base1DPlot): """ A 1D scatterplot that draws lines across the renderer """ #: The thickness, in pixels, of the lines line_width = Float(1.0) #: The fill color of the lines. color = black_color_trait #: The line dash style. line_style = LineStyle #------------------------------------------------------------------------ # Selection and selection rendering # A selection on the lot is indicated by setting the index or value # datasource's 'selections' metadata item to a list of indices, or the # 'selection_mask' metadata to a boolean array of the same length as the # datasource. #------------------------------------------------------------------------ #: whether or not to display a selection show_selection = Bool(True) #: the plot data metadata name to watch for selection information selection_metadata_name = Str("selections") #: the thickness, in pixels, of the selected lines selected_line_width = Float(1.0) #: the color of the selected lines selected_color = ColorTrait("yellow") #: The line dash style of the selected line. selected_line_style = LineStyle("solid") #: The fade amount for unselected regions unselected_alpha = Float(0.3) #------------------------------------------------------------------------ # Private methods #------------------------------------------------------------------------ def _draw_plot(self, gc, view_bounds=None, mode="normal"): """ Draw the plot """ coord = self._compute_screen_coord() lines = empty(shape=(len(coord), 4)) if self.orientation == 'v': lines[:, 0] = self.x lines[:, 1] = coord lines[:, 2] = self.x2 lines[:, 3] = coord else: lines[:, 0] = coord lines[:, 1] = self.y lines[:, 2] = coord lines[:, 3] = self.y2 self._render(gc, lines) def _render(self, gc, lines): """ Render a sequence of line values, accounting for selections """ with gc: gc.clip_to_rect(self.x, self.y, self.width, self.height) if not self.index: return name = self.selection_metadata_name md = self.index.metadata if name in md and md[name] is not None and len(md[name]) > 0: selected_mask = md[name][0] selected_lines = lines[selected_mask] unselected_lines = lines[~selected_mask] color = list(self.color_) color[3] *= self.unselected_alpha if unselected_lines.size > 0: self._render_lines(gc, unselected_lines, self.color_, self.line_width, self.line_style_) if selected_lines.size > 0: self._render_lines( gc, selected_lines, self.selected_color_, self.selected_line_width, self.selected_line_style_) else: self._render_lines(gc, lines, self.color_, self.line_width, self.line_style_) def _render_lines(self, gc, lines, color, width, dash): """ Render a collection of lines with a given style """ with gc: gc.set_stroke_color(color) gc.set_line_width(width) gc.set_line_dash(dash) for line in lines: gc.begin_path() line.shape = (2, 2) gc.lines(line) gc.stroke_path()
class MFnPolarLineRenderer(AbstractPlotRenderer): """ A renderer for polar line plots. """ #------------------------------------------------------------------------ # Appearance-related traits #------------------------------------------------------------------------ # The color of the origin axis. origin_axis_color_ = (0, 0, 0, 1) # The width of the origin axis. origin_axis_width = 1.0 # The origin axis is visible. origin_axis_visible = True # The grid is visible. grid_visible = True # The color of the line. color = black_color_trait # The width of the line. line_width = Float(1.0) # The style of the line. line_style = LineStyle("solid") # The style of the grid lines. grid_style = LineStyle("dot") frac_noplot = Float(0.3) def _gather_points(self): """ Collects the data points that are within the plot bounds and caches them """ # This is just a stub for now. We should really find the lines only # inside the screen range here. x = self.index.get_data() y = self.value.get_data() rad = min(self.width / 2.0, self.height / 2.0) sx = x * rad + self.x + self.width / 2.0 sy = y * rad + self.y + self.height / 2.0 points = transpose(array((sx, sy))) self._cached_data_pts = points self._cache_valid = True return def _render(self, gc, points): """ Actually draw the plot. """ gc.save_state() gc.set_antialias(True) self._draw_default_axes(gc) self._draw_default_grid(gc) if len(points) > 0: gc.clip_to_rect(self.x, self.y, self.width, self.height) gc.set_stroke_color(self.color_) gc.set_line_width(self.line_width) gc.set_line_dash(self.line_style_) gc.begin_path() gc.lines(points) gc.stroke_path() gc.restore_state() return def _draw_plot(self, *args, **kw): """ Draws the 'plot' layer. """ # Simple compatibility with new-style rendering loop return self._draw_component(*args, **kw) def _draw_component(self, gc, view_bounds=None, mode='normal'): """ Renders the component. """ self._gather_points() self._render(gc, self._cached_data_pts) def _draw_default_axes(self, gc): if not self.origin_axis_visible: return gc.save_state() gc.set_stroke_color(self.origin_axis_color_) gc.set_line_width(self.origin_axis_width) gc.set_line_dash(self.grid_style_) x_data, y_data = transpose(self._cached_data_pts) x_center = self.x + self.width / 2.0 y_center = self.y + self.height / 2.0 # number of divisions used to divide the cirlce radially # (equals the number of axes to be plotted) n_axes = 2 for theta in range(n_axes * 2): r = min(self.width / 2.0, self.height / 2.0) x = r * cos(theta * pi / n_axes) + x_center y = r * sin(theta * pi / n_axes) + y_center data_pts = array([[x_center, y_center], [x, y]]) start, end = data_pts gc.move_to(int(start[0]), int(start[1])) gc.line_to(int(end[0]), int(end[1])) gc.stroke_path() gc.restore_state() return def _draw_default_grid(self, gc): if not self.grid_visible: return gc.save_state() gc.set_stroke_color(self.origin_axis_color_) gc.set_line_width(self.origin_axis_width) gc.set_line_dash(self.grid_style_) x_data, y_data = transpose(self._cached_data_pts) x_center = self.x + self.width / 2.0 y_center = self.y + self.height / 2.0 print(('self.x', self.x)) print(('self.y', self.y)) print(('2*****self.width/2.0', self.width / 2.0)) print(('2****self.height/2.0', self.height / 2.0)) print(('x_center', x_center)) print(('y_center', y_center)) rad_one = min(self.width / 2.0, self.height / 2.0) rad_unitcircle_plt = 0.3 # number of divisions used to divide the cirlce tangentially ndivs_grid = 4 for i in range(ndivs_grid + 1): plotrange_max = 1.0 plotrange_min = 0.0 rad_i = plotrange_min + \ (plotrange_max - plotrange_min) / ndivs_grid * i rad_i_plt = coord_trans_plt(rad_i, self.frac_noplot, plotrange_min, plotrange_max) * rad_one gc.move_to(self.x, self.y) gc.arc(x_center, y_center, rad_i_plt, 0, 2 * pi) gc.stroke_path() gc.restore_state() return
class Crosshair(BaseTool): """ Display a crosshair at the given SVG coordinates. This will do the appropriate transformations in order to map Enable coordinates to SVG coordinates. """ svg_coords = Tuple(Float, Float) line_color = ColorTrait('black') line_width = Float(1.0) line_style = LineStyle("solid") # Whether the mouse is currently inside the component or not. mouse_in = Bool(False) visible = True draw_mode = 'overlay' def draw(self, gc, view_bounds=None): """ Draws this tool on a graphics context. It is assumed that the graphics context has a coordinate transform that matches the origin of its component. (For containers, this is just the origin; for components, it is the origin of their containers.) """ if not self.mouse_in: return # Convert from SVG coordinates to Enable coordinates. h = self.component.height x, y0 = self.svg_coords y = h - y0 gc.save_state() try: gc.set_stroke_color(self.line_color_) gc.set_line_width(self.line_width) gc.set_line_dash(self.line_style_) gc.move_to(self.component.x, y + 0.5) gc.line_to(self.component.x2, y + 0.5) gc.move_to(x - 0.5, self.component.y) gc.line_to(x - 0.5, self.component.y2) gc.stroke_path() finally: gc.restore_state() def overlay(self, component, gc, view_bounds=None, mode="normal"): """ Draws this component overlaid on a graphics context. """ self.draw(gc, view_bounds) def do_layout(self, *args, **kw): pass def normal_mouse_enter(self, event): self.mouse_in = True def normal_mouse_leave(self, event): self.mouse_in = False def normal_mouse_move(self, event): """ Handles the mouse being moved. """ if self.component is None: return # Map the Enable coordinates of the event to SVG coordinates. h = self.component.height y = h - event.y self.svg_coords = event.x, y event.handled = True @on_trait_change('svg_coords,mouse_in') def ensure_redraw(self): if self.component is not None: self.component.invalidate_and_redraw()
class LinePlot(BaseXYPlot): """ A plot consisting of a line. This is the most fundamental object to use to create line plots. However, it is somewhat low-level and therefore creating one properly to do what you want can require some verbose code. The create_line_plot() function in plot_factory.py can hide some of this verbosity for common cases. """ # The color of the line. color = black_color_trait # The color to use to highlight the line when selected. selected_color = ColorTrait("lightyellow") # The style of the selected line. selected_line_style = LineStyle("solid") # The name of the key in self.metadata that holds the selection mask metadata_name = Str("selections") # The thickness of the line. line_width = Float(1.0) # The line dash style. line_style = LineStyle # The rendering style of the line plot. # # connectedpoints # "normal" style (default); each point is connected to subsequent and # prior points by line segments # hold # each point is represented by a line segment parallel to the abscissa # (index axis) and spanning the length between the point and its # subsequent point. # connectedhold # like "hold" style, but line segments are drawn at each point of the # plot to connect the hold lines of the prior point and the current # point. Also called a "right angle plot". render_style = Enum("connectedpoints", "hold", "connectedhold") # Traits UI View for customizing the plot. traits_view = View(Item("color", style="custom"), "line_width", "line_style", buttons=["OK", "Cancel"]) #------------------------------------------------------------------------ # Private traits #------------------------------------------------------------------------ # Cached list of non-NaN arrays of (x,y) data-space points; regardless of # self.orientation, this is always stored as (index_pt, value_pt). This is # different from the default BaseXYPlot definition. _cached_data_pts = List # Cached list of non-NaN arrays of (x,y) screen-space points. _cached_screen_pts = List def hittest(self, screen_pt, threshold=7.0): """ Tests whether the given screen point is within *threshold* pixels of any data points on the line. If so, then it returns the (x,y) value of a data point near the screen point. If not, then it returns None. Note: This only checks data points and *not* the actual line segments connecting them. """ ndx = self.map_index(screen_pt, threshold) if ndx is not None: return (self.index.get_data()[ndx], self.value.get_data()[ndx]) else: data_x = self.map_data(screen_pt) xmin, xmax = self.index.get_bounds() if xmin <= data_x <= xmax: if self.orientation == "h": sy = screen_pt[1] else: sy = screen_pt[0] interp_val = self.interpolate(data_x) interp_y = self.value_mapper.map_screen(interp_val) if abs(sy - interp_y) <= threshold: return reverse_map_1d(self.index.get_data(), data_x, self.index.sort_order) return None def interpolate(self, index_value): """ Returns the value of the plot at the given index value in screen space. Raises an IndexError when *index_value* exceeds the bounds of indexes on the value. """ if self.index is None or self.value is None: raise IndexError, "cannot index when data source index or value is None" index_data = self.index.get_data() value_data = self.value.get_data() ndx = reverse_map_1d(index_data, index_value, self.index.sort_order) # quick test to see if this value is already in the index array if index_value == index_data[ndx]: return value_data[ndx] # get x and y values to interpolate between if index_value < index_data[ndx]: x0 = index_data[ndx - 1] y0 = value_data[ndx - 1] x1 = index_data[ndx] y1 = value_data[ndx] else: x0 = index_data[ndx] y0 = value_data[ndx] x1 = index_data[ndx + 1] y1 = value_data[ndx + 1] if x1 != x0: slope = float(y1 - y0) / float(x1 - x0) dx = index_value - x0 yp = y0 + slope * dx else: yp = inf return yp def get_screen_points(self): self._gather_points() return [self.map_screen(ary) for ary in self._cached_data_pts] #------------------------------------------------------------------------ # Private methods; implements the BaseXYPlot stub methods #------------------------------------------------------------------------ def _gather_points(self): """ Collects the data points that are within the bounds of the plot and caches them. """ if not self._cache_valid: if not self.index or not self.value: return index = self.index.get_data() value = self.value.get_data() # Check to see if the data is completely outside the view region for ds, rng in ((self.index, self.index_range), (self.value, self.value_range)): low, high = ds.get_bounds() if low > rng.high or high < rng.low: self._cached_data_pts = [] self._cached_valid = True return if len(index) == 0 or len(value) == 0 or len(index) != len(value): self._cached_data_pts = [] self._cache_valid = True size_diff = len(value) - len(index) if size_diff > 0: warnings.warn('Chaco.LinePlot: len(value) %d - len(index) %d = %d\n' \ % (len(value), len(index), size_diff)) index_max = len(index) value = value[:index_max] else: index_max = len(value) index = index[:index_max] # TODO: restore the functionality of rendering highlighted portions # of the line #selection = self.index.metadata.get(self.metadata_name, None) #if selection is not None and type(selection) in (ndarray, list) and \ # len(selection) > 0: # Split the index and value raw data into non-NaN chunks nan_mask = invert(isnan(value)) & invert(isnan(index)) blocks = [ b for b in arg_find_runs(nan_mask, "flat") if nan_mask[b[0]] != 0 ] points = [] for block in blocks: start, end = block block_index = index[start:end] block_value = value[start:end] index_mask = self.index_mapper.range.mask_data(block_index) runs = [r for r in arg_find_runs(index_mask, "flat") \ if index_mask[r[0]] != 0] # Check to see if our data view region is between two points in the # index data. If so, then we have to reverse map our current view # into the appropriate index and draw the bracketing points. if runs == []: data_pt = self.map_data( (self.x_mapper.low_pos, self.y_mapper.low_pos)) if self.index.sort_order == "none": indices = argsort(index) sorted_index = take(index, indices) sorted_value = take(value, indices) sort = 1 else: sorted_index = index sorted_value = value if self.index.sort_order == "ascending": sort = 1 else: sort = -1 ndx = bin_search(sorted_index, data_pt, sort) if ndx == -1: # bin_search can return -1 if data_pt is outside the bounds # of the source data continue points.append( transpose( array((sorted_index[ndx:ndx + 2], sorted_value[ndx:ndx + 2])))) else: # Expand the width of every group of points so we draw the lines # up to their next point, outside the plot area data_end = len(index_mask) for run in runs: start, end = run if start != 0: start -= 1 if end != data_end: end += 1 run_data = transpose( array((block_index[start:end], block_value[start:end]))) points.append(run_data) self._cached_data_pts = points self._cache_valid = True return def _downsample(self): if not self._screen_cache_valid: self._cached_screen_pts = [ self.map_screen(p) for p in self._cached_data_pts ] self._screen_cache_valid = True pt_arrays = self._cached_screen_pts # some boneheaded short-circuits m = self.index_mapper total_numpoints = sum([p.shape for p in pt_arrays]) if (total_numpoints < 400) or (total_numpoints < m.high_pos - m.low_pos): return self._cached_screen_pts # the new point array and a counter of how many actual points we've added # to it new_arrays = [] for pts in pt_arrays: new_pts = zeros(pts.shape, "d") numpoints = 1 new_pts[0] = pts[0] last_x, last_y = pts[0] for x, y in pts[1:]: if (x - last_x)**2 + (y - last_y)**2 > 2: new_pts[numpoints] = (x, y) last_x = x last_y = y numpoints += 1 new_arrays.append(new_pts[:numpoints]) return self._cached_screen_pts def _render(self, gc, points, selected_points=None): if len(points) == 0: return with gc: gc.set_antialias(True) gc.clip_to_rect(self.x, self.y, self.width, self.height) render_method_dict = { "hold": self._render_hold, "connectedhold": self._render_connected_hold, "connectedpoints": self._render_normal } render = render_method_dict.get(self.render_style, self._render_normal) if selected_points is not None: gc.set_stroke_color(self.selected_color_) gc.set_line_width(self.line_width + 10.0) gc.set_line_dash(self.selected_line_style_) render(gc, selected_points, self.orientation) # Render using the normal style gc.set_stroke_color(self.color_) gc.set_line_width(self.line_width) gc.set_line_dash(self.line_style_) render(gc, points, self.orientation) # Draw the default axes, if necessary self._draw_default_axes(gc) @classmethod def _render_normal(cls, gc, points, orientation): for ary in points: if len(ary) > 0: gc.begin_path() gc.lines(ary) gc.stroke_path() return @classmethod def _render_hold(cls, gc, points, orientation): for starts in points: x, y = starts.T if orientation == "h": ends = transpose(array((x[1:], y[:-1]))) else: ends = transpose(array((x[:-1], y[1:]))) gc.begin_path() gc.line_set(starts[:-1], ends) gc.stroke_path() return @classmethod def _render_connected_hold(cls, gc, points, orientation): for starts in points: x, y = starts.T if orientation == "h": ends = transpose(array((x[1:], y[:-1]))) else: ends = transpose(array((x[:-1], y[1:]))) gc.begin_path() gc.line_set(starts[:-1], ends) gc.line_set(ends, starts[1:]) gc.stroke_path() return def _render_icon(self, gc, x, y, width, height): with gc: gc.set_stroke_color(self.color_) gc.set_line_width(self.line_width) gc.set_line_dash(self.line_style_) gc.set_antialias(0) gc.move_to(x, y + height / 2) gc.line_to(x + width, y + height / 2) gc.stroke_path() return def _downsample_vectorized(self): """ Analyzes the screen-space points stored in self._cached_data_pts and replaces them with a downsampled set. """ pts = self._cached_screen_pts #.astype(int) # some boneheaded short-circuits m = self.index_mapper if (pts.shape[0] < 400) or (pts.shape[0] < m.high_pos - m.low_pos): return pts2 = concatenate((array([[0.0, 0.0]]), pts[:-1])) z = abs(pts - pts2) d = z[:, 0] + z[:, 1] #... TODO ... return def _alpha_changed(self): self.color_ = self.color_[0:3] + (self.alpha, ) self.invalidate_draw() self.request_redraw() return def _color_changed(self): self.invalidate_draw() self.request_redraw() return def _line_style_changed(self): self.invalidate_draw() self.request_redraw() return def _line_width_changed(self): self.invalidate_draw() self.request_redraw() return def __getstate__(self): state = super(LinePlot, self).__getstate__() for key in ['traits_view']: if state.has_key(key): del state[key] return state
class PolarLineRenderer(AbstractPlotRenderer): """ A renderer for polar line plots. """ #------------------------------------------------------------------------ # Appearance-related traits #------------------------------------------------------------------------ # The color of the origin axis. origin_axis_color_ = (0,0,0,1) # The width of the origin axis. origin_axis_width = 2.0 # The origin axis is visible. origin_axis_visible=True # The grid is visible. grid_visible= True # The orientation of the plot is horizontal; for any other value, it is # transposed orientation = 'h' # The color of the line. color = black_color_trait # The width of the line. line_width = Float(1.0) # The style of the line. line_style = LineStyle("solid") # The style of the grid lines. grid_style= LineStyle("dot") def _gather_points(self): """ Collects the data points that are within the plot bounds and caches them """ # This is just a stub for now. We should really find the lines only # inside the screen range here. x = self.index.get_data() y = self.value.get_data() rad= min(self.width/2.0,self.height/2.0) sx = x*rad+ self.x + self.width/2.0 sy = y*rad+ self.y + self.height/2.0 points = transpose(array((sx,sy))) self._cached_data_pts = points self._cache_valid = True return def _data_changed(self): self._cache_valid = False return def _update_mappers(self): #Dunno if there is anything else to do here self._cache_valid = False def _render(self, gc, points): """ Actually draw the plot. """ with gc: gc.set_antialias(True) self._draw_default_axes(gc) self._draw_default_grid(gc) if len(points)>0: gc.clip_to_rect(self.x, self.y, self.width, self.height) gc.set_stroke_color(self.color_) gc.set_line_width(self.line_width) gc.set_line_dash(self.line_style_) gc.begin_path() gc.lines(points) gc.stroke_path() return def map_screen(self, data_array): """ Maps an array of data points into screen space and returns it as an array. Implements the AbstractPlotRenderer interface. """ if len(data_array) == 0: return [] elif len(data_array) == 1: xtmp, ytmp = transpose(data_array) x_ary = xtmp y_ary = ytmp else: x_ary, y_ary = transpose(data_array) sx = self.index_mapper.map_screen(x_ary) sy = self.value_mapper.map_screen(y_ary) if self.orientation == 'h': return transpose(array((sx, sy))) else: return transpose(array((sy, sx))) def map_data(self, screen_pt): """ Maps a screen space point into the "index" space of the plot. Implements the AbstractPlotRenderer interface. """ if self.orientation == 'h': x, y = screen_pt else: y,x = screen_pt return array((self.index_mapper.map_data(x), self.value_mapper.map_data(y))) def _downsample(self): return self.map_screen(self._cached_data_pts) def _draw_plot(self, *args, **kw): """ Draws the 'plot' layer. """ # Simple compatibility with new-style rendering loop return self._draw_component(*args, **kw) def _draw_component(self, gc, view_bounds=None, mode='normal'): """ Renders the component. """ self._gather_points() self._render(gc, self._cached_data_pts) def _bounds_changed(self, old, new): super(PolarLineRenderer, self)._bounds_changed(old, new) self._update_mappers() def _bounds_items_changed(self, event): super(PolarLineRenderer, self)._bounds_items_changed(event) self._update_mappers() def _draw_default_axes(self, gc): if not self.origin_axis_visible: return with gc: gc.set_stroke_color(self.origin_axis_color_) gc.set_line_width(self.origin_axis_width) gc.set_line_dash(self.grid_style_) x_data,y_data= transpose(self._cached_data_pts) x_center=self.x + self.width/2.0 y_center=self.y + self.height/2.0 for theta in range(12): r= min(self.width/2.0,self.height/2.0) x= r*cos(theta*pi/6) + x_center y= r*sin(theta*pi/6) + y_center data_pts= array([[x_center,y_center],[x,y]]) start,end = data_pts gc.move_to(int(start[0]), int(start[1])) gc.line_to(int(end[0]), int(end[1])) gc.stroke_path() return def _draw_default_grid(self,gc): if not self.grid_visible: return with gc: gc.set_stroke_color(self.origin_axis_color_) gc.set_line_width(self.origin_axis_width) gc.set_line_dash(self.grid_style_) x_data,y_data = transpose(self._cached_data_pts) x_center = self.x + self.width/2.0 y_center = self.y + self.height/2.0 rad = min(self.width/2.0, self.height/2.0) for r_part in range(1,5): r = rad*r_part/4 gc.arc(x_center, y_center, r, 0, 2*pi) gc.stroke_path() return
class EnableSelectBox(Component): """ Implements generic behavior for a selection box """ # Color for the box's edge stroke_color = ColorTrait((0, 0, 0, 1)) # Color to fill the box with fill_color = ColorTrait((0, 0, 0, .1)) # Style for the box's edge style = LineStyle("solid") # Private traits _original_position = Tuple #--------------------------------------------------------------------------- # EnableSelectBox interface #--------------------------------------------------------------------------- def set_drag_start(self, x, y): """ Sets the fixed coordinate which is the start of the selection. Effectively resets the selection box. """ self.position = [x, y] self._original_position = (x, y) self.bounds = [0, 0] def set_drag_dimensions(self, x2, y2): """ Set the coordinate which is under the mouse, assuming the mouse is being used for selection. """ if x2 < self._original_position[0]: self.x = x2 self.width = self._original_position[0] - x2 else: self.x = self._original_position[0] self.width = x2 - self.x if y2 < self._original_position[1]: self.y = y2 self.height = self._original_position[1] - y2 else: self.y = self._original_position[1] self.height = y2 - self.y def is_component_in(self, c): """ Returns true if any part of the component is considered "inside" the select box. """ return (self.is_in(c.x, c.y) or self.is_in(c.x2, c.y2) or self.is_in(c.x, c.y2) or self.is_in(c.x2, c.y)) def is_completely_in(self, c): """ Only returns true if all of the component is entirely inside the select box. """ return (self.is_in(c.x, c.y) and self.is_in(c.x2, c.y2) and self.is_in(c.x, c.y2) and self.is_in(c.x2, c.y)) #--------------------------------------------------------------------------- # Component interface #--------------------------------------------------------------------------- def _draw_mainlayer(self, gc, view_bounds=None, mode="default"): gc.save_state() gc.set_antialias(1) gc.set_stroke_color(self.stroke_color_) gc.set_fill_color(self.fill_color_) gc.begin_path() gc.rect(self.x, self.y, self.width, self.height) gc.draw_path() gc.restore_state() def _draw_background(self, gc, view_bounds=None, mode="default"): pass
class PlotGrid(AbstractOverlay): """ An overlay that represents a grid. A grid is a set of parallel lines, horizontal or vertical. You can use multiple grids with different settings for the horizontal and vertical lines in a plot. """ #------------------------------------------------------------------------ # Data-related traits #------------------------------------------------------------------------ #: The mapper (and associated range) that drive this PlotGrid. mapper = Instance(AbstractMapper) #: The dataspace interval between grid lines. grid_interval = Trait('auto', 'auto', Float) #: The dataspace value at which to start this grid. If None, then #: uses the mapper.range.low. data_min = Trait(None, None, Float) #: The dataspace value at which to end this grid. If None, then uses #: the mapper.range.high. data_max = Trait(None, None, Float) #: A callable that implements the AbstractTickGenerator Interface. tick_generator = Instance(AbstractTickGenerator) #------------------------------------------------------------------------ # Layout traits #------------------------------------------------------------------------ #: The orientation of the grid lines. "horizontal" means that the grid #: lines are parallel to the X axis and the ticker and grid interval #: refer to the Y axis. orientation = Enum('horizontal', 'vertical') #: Draw the ticks starting at the end of the mapper range? If False, the #: ticks are drawn starting at 0. This setting can be useful to keep the #: grid from from "flashing" as the user resizes the plot area. flip_axis = Bool(False) #: Optional specification of the grid bounds in the dimension transverse #: to the ticking/gridding dimension, i.e. along the direction specified #: by self.orientation. If this is specified but transverse_mapper is #: not specified, then there is no effect. #: #: None : use self.bounds or self.component.bounds (if overlay) #: Tuple : (low, high) extents, used for every grid line #: Callable : Function that takes an array of dataspace grid ticks #: and returns either an array of shape (N,2) of (starts,ends) #: for each grid point or a single tuple (low, high) transverse_bounds = Trait(None, Tuple, Callable) #: Mapper in the direction corresponding to self.orientation, i.e. transverse #: to the direction of self.mapper. This is used to compute the screen #: position of transverse_bounds. If this is not specified, then #: transverse_bounds has no effect, and vice versa. transverse_mapper = Instance(AbstractMapper) #: Dimensions that the grid is resizable in (overrides PlotComponent). resizable = "hv" #------------------------------------------------------------------------ # Appearance traits #------------------------------------------------------------------------ #: The color of the grid lines. line_color = black_color_trait #: The style (i.e., dash pattern) of the grid lines. line_style = LineStyle('solid') #: The thickness, in pixels, of the grid lines. line_width = CInt(1) line_weight = Alias("line_width") #: Default Traits UI View for modifying grid attributes. traits_view = GridView #------------------------------------------------------------------------ # Private traits; mostly cached information #------------------------------------------------------------------------ _cache_valid = Bool(False) _tick_list = Any _tick_positions = Any # An array (N,2) of start,end positions in the transverse direction # i.e. the direction corresponding to self.orientation _tick_extents = Any #_length = Float(0.0) #------------------------------------------------------------------------ # Public methods #------------------------------------------------------------------------ def __init__(self, **traits): # TODO: change this back to a factory in the instance trait some day self.tick_generator = DefaultTickGenerator() super(PlotGrid, self).__init__(**traits) self.bgcolor = "none" #make sure we're transparent return @on_trait_change("bounds,bounds_items,position,position_items") def invalidate(self): """ Invalidate cached information about the grid. """ self._reset_cache() return #------------------------------------------------------------------------ # PlotComponent and AbstractOverlay interface #------------------------------------------------------------------------ def do_layout(self, *args, **kw): """ Tells this component to do layout at a given size. Overrides PlotComponent. """ if self.use_draw_order and self.component is not None: self._layout_as_overlay(*args, **kw) else: super(PlotGrid, self).do_layout(*args, **kw) return #------------------------------------------------------------------------ # Private methods #------------------------------------------------------------------------ def _do_layout(self): """ Performs a layout. Overrides PlotComponent. """ return def _layout_as_overlay(self, size=None, force=False): """ Lays out the axis as an overlay on another component. """ if self.component is not None: self.position = self.component.position self.bounds = self.component.bounds return def _reset_cache(self): """ Clears the cached tick positions. """ self._tick_positions = array([], dtype=float) self._tick_extents = array([], dtype=float) self._cache_valid = False return def _compute_ticks(self, component=None): """ Calculates the positions for the grid lines. """ if (self.mapper is None): self._reset_cache() self._cache_valid = True return if self.data_min is None: datalow = self.mapper.range.low else: datalow = self.data_min if self.data_max is None: datahigh = self.mapper.range.high else: datahigh = self.data_max # Map the low and high data points screenhigh = self.mapper.map_screen(datalow) screenlow = self.mapper.map_screen(datahigh) if (datalow == datahigh) or (screenlow == screenhigh) or \ (datalow in [inf, -inf]) or (datahigh in [inf, -inf]): self._reset_cache() self._cache_valid = True return if component is None: component = self.component if component is not None: bounds = component.bounds position = component.position else: bounds = self.bounds position = self.position if isinstance(self.mapper, LogMapper): scale = 'log' else: scale = 'linear' ticks = self.tick_generator.get_ticks(datalow, datahigh, datalow, datahigh, self.grid_interval, use_endpoints=False, scale=scale) tick_positions = self.mapper.map_screen(array(ticks, float64)) if self.orientation == 'horizontal': self._tick_positions = around( column_stack((zeros_like(tick_positions) + position[0], tick_positions))) elif self.orientation == 'vertical': self._tick_positions = around( column_stack((tick_positions, zeros_like(tick_positions) + position[1]))) else: raise self.NotImplementedError # Compute the transverse direction extents self._tick_extents = zeros((len(ticks), 2), dtype=float) if self.transverse_bounds is None or self.transverse_mapper is None: # No mapping needed, just use the extents if self.orientation == 'horizontal': extents = (position[0], position[0] + bounds[0]) elif self.orientation == 'vertical': extents = (position[1], position[1] + bounds[1]) self._tick_extents[:] = extents elif callable(self.transverse_bounds): data_extents = self.transverse_bounds(ticks) tmapper = self.transverse_mapper if isinstance(data_extents, tuple): self._tick_extents[:] = tmapper.map_screen( asarray(data_extents)) else: extents = array([ tmapper.map_screen(data_extents[:, 0]), tmapper.map_screen(data_extents[:, 1]) ]).T self._tick_extents = extents else: # Already a tuple self._tick_extents[:] = self.transverse_mapper.map_screen( asarray(self.transverse_bounds)) self._cache_valid = True def _draw_overlay(self, gc, view_bounds=None, mode='normal'): """ Draws the overlay layer of a component. Overrides PlotComponent. """ self._draw_component(gc, view_bounds, mode) return def overlay(self, other_component, gc, view_bounds=None, mode="normal"): """ Draws this component overlaid on another component. Overrides AbstractOverlay. """ if not self.visible: return self._compute_ticks(other_component) self._draw_component(gc, view_bounds, mode) self._cache_valid = False return def _draw_component(self, gc, view_bounds=None, mode="normal"): """ Draws the component. This method is preserved for backwards compatibility. Overrides PlotComponent. """ # What we're really trying to do with a grid is plot contour lines in # the space of the plot. In a rectangular plot, these will always be # straight lines. if not self.visible: return if not self._cache_valid: self._compute_ticks() if len(self._tick_positions) == 0: return with gc: gc.set_line_width(self.line_weight) gc.set_line_dash(self.line_style_) gc.set_stroke_color(self.line_color_) gc.set_antialias(False) if self.component is not None: gc.clip_to_rect(*(self.component.position + self.component.bounds)) else: gc.clip_to_rect(*(self.position + self.bounds)) gc.begin_path() if self.orientation == "horizontal": starts = self._tick_positions.copy() starts[:, 0] = self._tick_extents[:, 0] ends = self._tick_positions.copy() ends[:, 0] = self._tick_extents[:, 1] else: starts = self._tick_positions.copy() starts[:, 1] = self._tick_extents[:, 0] ends = self._tick_positions.copy() ends[:, 1] = self._tick_extents[:, 1] if self.flip_axis: starts, ends = ends, starts gc.line_set(starts, ends) gc.stroke_path() return def _mapper_changed(self, old, new): if old is not None: old.on_trait_change(self.mapper_updated, "updated", remove=True) if new is not None: new.on_trait_change(self.mapper_updated, "updated") self.invalidate() return def mapper_updated(self): """ Event handler that is bound to this mapper's **updated** event. """ self.invalidate() return def _position_changed_for_component(self): self.invalidate() def _position_items_changed_for_component(self): self.invalidate() def _bounds_changed_for_component(self): self.invalidate() def _bounds_items_changed_for_component(self): self.invalidate() #------------------------------------------------------------------------ # Event handlers for visual attributes. These mostly just call request_redraw() #------------------------------------------------------------------------ @on_trait_change("visible,line_color,line_style,line_weight") def visual_attr_changed(self): """ Called when an attribute that affects the appearance of the grid is changed. """ if self.component: self.component.invalidate_draw() self.component.request_redraw() else: self.invalidate_draw() self.request_redraw() def _grid_interval_changed(self): self.invalidate() self.visual_attr_changed() def _orientation_changed(self): self.invalidate() self.visual_attr_changed() return ### Persistence ########################################################### #_pickles = ("orientation", "line_color", "line_style", "line_weight", # "grid_interval", "mapper") def __getstate__(self): state = super(PlotGrid, self).__getstate__() for key in [ '_cache_valid', '_tick_list', '_tick_positions', '_tick_extents' ]: if key in state: del state[key] return state def _post_load(self): super(PlotGrid, self)._post_load() self._mapper_changed(None, self.mapper) self._reset_cache() self._cache_valid = False return
class SegmentPlot(BaseXYPlot): """ Plot that draws a collection of line segments. """ #: The single color to use when color_by_data is False. color = black_color_trait(redraw=True) #: The thickness of the line. line_width = Float(1.0, redraw=True) #: The line dash style. line_style = LineStyle(redraw=True) #: The rendering style of the segment plot. #: #: line #: "Normal" direct connection between start and end points. #: orthogonal #: Connect the start and end points by two line segments in orthogonal #: directions. #: quad #: Connect the start and end points by a quadratic Bezier curve. #: cubic #: Connect the start and end points by a cubic Bezier curve. #: #: For non-linear segments, the tangent at the start matches the #: orientation of the plot (ie. horizontal orientation means #: a horizontal tangent). render_style = Enum('line', 'orthogonal', 'quad', 'cubic') #: When rendering certain styles, which orientation to prefer. render_orientation = Enum('index', 'value') #: Whether to draw segments using a constant color or colormapped data. color_by_data = Bool(False) #: The data to use for the segment color. Used only when #: self.color_by_data is True. color_data = Instance(AbstractDataSource, redraw=True) #: The color mapper to use for the segment data. Used only when #: self.color_by_data is True. color_mapper = Instance(AbstractColormap, redraw=True) #: Whether to draw segments using a constant width or mapped width. width_by_data = Bool(False, redraw=True) #: The data to use for segment width. Used only when self.width_by_data #: is True. width_data = Instance(AbstractDataSource, redraw=True) #: Whether to draw segments using a constant width or mapped width. width_mapper = Instance(AbstractMapper, redraw=True) #: Whether or not to shade selected segments. show_selection = True #: the plot data metadata name to watch for selection information selection_metadata_name = Str("selections") #: The color to use for selected segments. Not used if color by data. selection_color = ColorTrait('yellow') #: The alpha fade to use for non-selected segments. selection_alpha = Float(0.3) #: The width multiple to use for non-selected segments. selection_width = Float(1.0) #: RGBA values for rendering individual segments, in the case where #: color_by_data is True. This is a length N array with the rgba_dtype #: and are computed using the current color or color mapper and color_data, #: with the global 'alpha' mixed in. effective_colors = Property(Array, depends_on=[ 'color_by_data', 'alpha', 'color_mapper.updated', 'color_data.data_changed', 'alpha', 'selection_mask', 'selection_color', 'selection_alpha' ]) #: The widths of the individual lines in screen units, if mapped to data. #: The values are computed with the width mapper. screen_widths = Property( Array, depends_on=['width_mapper.updated', 'width_data.data_changed']) selected_mask = Property( depends_on=['selection_metadata_name', 'index.metadata_changed']) # These BaseXYPlot methods either don't make sense or aren't currently # implemented for this plot type. def get_closest_point(self, *args, **kwargs): raise NotImplementedError() def get_closest_line(self, *args, **kwargs): raise NotImplementedError() def hittest(self, *args, **kwargs): raise NotImplementedError() def map_index(self, *args, **kwargs): raise NotImplementedError() def _gather_points(self): """ Collects the data points that are within the bounds of the plot and caches them. """ if self._cache_valid: return if self.index is None or self.value is None: return index = self.index.get_data() value = self.value.get_data() if len(index) == 0 or len(value) == 0 or len(index) != len(value): points = np.zeros((0, 2, 2), dtype=np.float64) else: points = np.column_stack([index, value]).reshape(-1, 2, 2) # TODO filter for segments intersecting the visible region self._cached_data_pts = points self._cache_valid = True def _render(self, gc, segments): """ Render an array of shape (N, 2, 2) of screen-space points as a collection of segments. """ if len(segments) == 0: # nothing to plot return colors = self.effective_colors widths = self.screen_widths with gc: gc.clip_to_rect(self.x, self.y, self.width, self.height) gc.set_line_dash(self.line_style_) gc.set_line_cap(CAP_ROUND) starts = segments[:, 0] ends = segments[:, 1] starts = starts.ravel().view(point_dtype) ends = ends.ravel().view(point_dtype) if self.render_style == 'orthogonal': self._render_orthogonal(gc, starts, ends, colors, widths) elif self.render_style == 'quad': self._render_quad(gc, starts, ends, colors, widths) elif self.render_style == 'cubic': self._render_cubic(gc, starts, ends, colors, widths) else: self._render_line(gc, starts, ends, colors, widths) def _render_line(self, gc, starts, ends, colors, widths): """ Render straight lines connecting the start point and end point. """ if len(widths) == 1 and len(colors) == 1 and colors[0]['a'] == 1.0: # no alpha, can draw a single unconnected path, faster starts = starts.view(float).reshape(-1, 2) ends = ends.view(float).reshape(-1, 2) gc.set_line_width(widths[0]) gc.set_stroke_color(colors[0]) gc.line_set(starts, ends) gc.stroke_path() else: for color, width, start, end in np.broadcast( colors, widths, starts, ends): gc.set_stroke_color(color) gc.set_line_width(float(width)) gc.move_to(start['x'], start['y']) gc.line_to(end['x'], end['y']) gc.stroke_path() def _render_orthogonal(self, gc, starts, ends, colors, widths): """ Render orthogonal lines connecting the start point and end point. Draw the orthogonal line in the direction determined by the orientation. For horizontal orientation, the horizontal segment is drawn first; for vertical orientation the vertical segment is drawn first. """ mids = np.empty(len(starts), dtype=point_dtype) if self.render_orientation == 'index': if self.orientation == 'h': mids['x'] = ends['x'] mids['y'] = starts['y'] else: mids['x'] = starts['x'] mids['y'] = ends['y'] else: if self.orientation == 'h': mids['x'] = starts['x'] mids['y'] = ends['y'] else: mids['x'] = ends['x'] mids['y'] = starts['y'] if len(widths) == 1 and len(colors) == 1 and colors[0]['a'] == 1.0: # no alpha, can draw a single unconnected path, faster starts = starts.view(float).reshape(-1, 2) mids = mids.view(float).reshape(-1, 2) ends = ends.view(float).reshape(-1, 2) gc.set_line_width(widths[0]) gc.set_stroke_color(colors[0]) gc.line_set(starts, mids) gc.line_set(mids, ends) gc.stroke_path() else: for color, width, start, end, mid in np.broadcast( colors, widths, starts, ends, mids): gc.set_stroke_color(color) gc.set_line_width(float(width)) gc.move_to(start['x'], start['y']) gc.line_to(mid['x'], mid['y']) gc.line_to(end['x'], end['y']) gc.stroke_path() def _render_quad(self, gc, starts, ends, colors, widths): """ Render quadratic Bezier curves connecting the start and end points. Draw the orthogonal line in the direction determined by the plot orientation. For horizontal orientation, the start point tangent is horizontal; for vertical orientation the start point tangent is vertical. """ mids = np.empty(len(starts), dtype=point_dtype) if self.render_orientation == 'index': if self.orientation == 'h': mids['x'] = ends['x'] mids['y'] = starts['y'] else: mids['x'] = starts['x'] mids['y'] = ends['y'] else: if self.orientation == 'h': mids['x'] = starts['x'] mids['y'] = ends['y'] else: mids['x'] = ends['x'] mids['y'] = starts['y'] if len(widths) == 1 and len(colors) == 1 and colors[0]['a'] == 1.0: # no alpha, can draw a single unconnected path, faster gc.set_line_width(widths[0]) gc.set_stroke_color(colors[0]) for start, end, mid in np.broadcast(starts, ends, mids): gc.move_to(start['x'], start['y']) gc.quad_curve_to(mid['x'], mid['y'], end['x'], end['y']) gc.stroke_path() else: for color, width, start, end, mid in np.broadcast( colors, widths, starts, ends, mids): gc.set_stroke_color(color) gc.set_line_width(float(width)) gc.move_to(start['x'], start['y']) gc.quad_curve_to(mid['x'], mid['y'], end['x'], end['y']) gc.stroke_path() def _render_cubic(self, gc, starts, ends, colors, widths): """ Render quadratic Bezier curves connecting the start and end points. Draw the orthogonal line in the direction determined by the plot orientation. For horizontal orientation, the start point and end point tangents is are horizontal; for vertical orientation the start and end point tangents are vertical. """ mids_1 = np.empty(len(starts), dtype=point_dtype) mids_2 = np.empty(len(starts), dtype=point_dtype) if self.render_orientation == 'index': if self.orientation == 'h': mids_1['x'] = (starts['x'] + ends['x']) / 2 mids_1['y'] = starts['y'] mids_2['x'] = mids_1['x'] mids_2['y'] = ends['y'] else: mids_1['x'] = starts['x'] mids_1['y'] = (starts['y'] + ends['y']) / 2 mids_2['x'] = ends['x'] mids_2['y'] = mids_1['y'] else: if self.orientation == 'h': mids_1['x'] = starts['x'] mids_1['y'] = (starts['y'] + ends['y']) / 2 mids_2['x'] = ends['x'] mids_2['y'] = mids_1['y'] else: mids_1['x'] = (starts['x'] + ends['x']) / 2 mids_1['y'] = starts['y'] mids_2['x'] = mids_1['x'] mids_2['y'] = ends['y'] if len(widths) == 1 and len(colors) == 1 and colors[0]['a'] == 1.0: # no alpha, can draw a single unconnected path, faster gc.set_line_width(widths[0]) gc.set_stroke_color(colors[0]) for start, end, mid_1, mid_2 in np.broadcast( starts, ends, mids_1, mids_2): gc.move_to(start['x'], start['y']) gc.curve_to(mid_1['x'], mid_1['y'], mid_2['x'], mid_2['y'], end['x'], end['y']) gc.stroke_path() else: for color, width, start, end, mid_1, mid_2 in np.broadcast( colors, widths, starts, ends, mids_1, mids_2): gc.set_stroke_color(color) gc.set_line_width(float(width)) gc.move_to(start['x'], start['y']) gc.curve_to(mid_1['x'], mid_1['y'], mid_2['x'], mid_2['y'], end['x'], end['y']) gc.stroke_path() def _render_icon(self, gc, x, y, width, height): """ Renders a representation of this plot as an icon into the box defined by the parameters. Used by the legend. """ with gc: gc.set_stroke_color(self.color_) gc.set_line_width(self.line_width) if hasattr(self, 'line_style_'): gc.set_line_dash(self.line_style_) gc.move_to(x, y) gc.line_to(width, height) @on_trait_change('alpha, color_data:data_changed, color_mapper:updated, ' 'width_data:data_changed, width_mapper.updated, +redraw') def _attributes_changed(self): self.invalidate_draw() self.request_redraw() @cached_property def _get_effective_colors(self): if self.color_by_data: color_data = self.color_data.get_data() colors = self.color_mapper.map_screen(color_data) else: if self.selected_mask is not None: colors = np.ones((len(self.selected_mask), 4)) colors[self.selected_mask, :len(self.selection_color_ )] = self.selection_color_ colors[~self.selected_mask, :len(self.color_)] = self.color_ else: colors = np.ones((1, 4)) colors[:, :len(self.color_)] = self.color_ if colors.shape[-1] == 4: colors[:, -1] *= self.alpha else: colors = np.column_stack( [colors, np.full(len(colors), self.alpha)]) if self.selected_mask is not None: colors[~self.selected_mask, -1] *= self.selection_alpha colors = colors.astype(np.float32).view(rgba_dtype) colors.shape = (-1, ) return colors @cached_property def _get_screen_widths(self): if self.width_by_data: width_data = self.width_data.get_data() widths = self.width_mapper.map_screen(width_data) else: widths = np.array([self.line_width]) return widths @cached_property def _get_selected_mask(self): name = self.selection_metadata_name md = self.index.metadata selection = md.get(name) if selection is not None and len(selection) > 0: selected_mask = selection[0] return selected_mask return None