Exemple #1
0
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
Exemple #3
0
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()
Exemple #5
0
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
Exemple #6
0
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
Exemple #7
0
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
Exemple #8
0
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()
Exemple #9
0
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
Exemple #12
0
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()
Exemple #13
0
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
Exemple #14
0
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
Exemple #16
0
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
Exemple #17
0
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