Exemplo n.º 1
0
class Overlay(AbstractOverlay):

    text = Str
    font = KivaFont("DEFAULT 16")
    alpha = Float(0.5)
    margin = Int(8)

    def __init__(self, text="", *args, **kw):
        super(Overlay, self).__init__(*args, **kw)
        self.text = text

    def overlay(self, component, gc, view_bounds=None, mode="normal"):
        with gc:
            gc.set_font(self.font)
            twidth, theight = gc.get_text_extent(self.text)[2:]
            tx = component.x + (component.width - twidth) / 2.0
            ty = component.y + (component.height - theight) / 2.0

            # Draw a small, light rectangle representing this overlay
            gc.set_fill_color((1.0, 1.0, 1.0, self.alpha))
            gc.rect(tx-self.margin, ty-self.margin,
                         twidth+2*self.margin, theight+2*self.margin)
            gc.fill_path()

            gc.set_text_position(tx, ty)
            gc.show_text(self.text)
Exemplo n.º 2
0
class AtmInterceptOverlay(AbstractOverlay):
    line_width = Float(1.5)
    font = KivaFont("modern 10")
    line_style = LineStyle('dash')
    label = Str
    value = Float

    def overlay(self, component, gc, view_bounds=None, mode="normal"):
        x, y = component.map_screen((0, self.value))
        xo = component.x
        if x < xo:
            x = xo + 5

        with gc:
            txt = self.label
            gc.set_font(self.font)
            w, h = gc.get_full_text_extent(txt)[:2]

            gc.clip_to_rect(component.x - w - 5, component.y, component.width, component.height)

            gc.set_line_width(self.line_width)
            gc.set_line_dash(self.line_style_)
            gc.move_to(xo, y)
            gc.line_to(x, y)
            gc.draw_path()

            gc.set_text_position(xo - w - 2, y)
            gc.show_text(txt)
Exemplo n.º 3
0
class CrosshairsOverlay(SimpleCrosshairsOverlay):
    circle_only = False
    font = KivaFont("modern 10")
    tag = None

    def overlay(self, component, gc, *args, **kw):
        with gc:
            gc.clip_to_rect(component.x, component.y,
                            component.width, component.height)
            # if component.crosshairs_kind == 'UserRadius':
            #     radius = component.crosshairs_radius
            # else:
            #     radius = component.beam_radius
            #
            # radius = component.get_wh(radius, 0)[0]
            radius = component.get_crosshairs_radius(screen=True, tag=self.tag)
            # sdp = component.show_desired_position
            # dp = component.desired_position
            sdp, dp = component.get_desired_position(tag=self.tag)
            # get offset in screen space
            ox, oy = component.get_screen_offset(tag=self.tag)

            if sdp and dp is not None:
                pos_off = dp[0] + ox, dp[1] + oy
                self._draw_radius_ch(gc, component, pos_off, radius,
                                     color=component.desired_position_color)

            mx = component.x + (component.x2 - component.x) / 2.0
            my = component.y + (component.y2 - component.y) / 2.0

            circle_only = bool(self.tag)
            if component.get_show_laser_position(self.tag):
                if ox or oy:
                    pos_off = mx + ox, my + oy
                    color = component.get_crosshairs_color(self.tag, True)
                    self._draw_radius_ch(gc, component, pos_off, radius,
                                         circle_only=circle_only,
                                         color=color)
                else:
                    color = component.get_crosshairs_color(self.tag)
                    self._draw_radius_ch(gc, component, (mx, my), radius,
                                         circle_only=circle_only,
                                         color=color)

            if component.show_hole_label:
                h = component.get_current_hole()
                if h is not None:
                    x, y = mx + ox + radius, my + oy + radius
                    color = component.hole_label_color
                    self.set_color(gc, color, fill=True)

                    gc.set_text_position(x, y)
                    gc.set_font(self.component.hole_label_font)
                    gc.show_text(h.id)
Exemplo n.º 4
0
class ToolkitEditorFactory(EditorFactory):
    """ wxPython editor factory for Enable RGBA color editors.
    """
    #---------------------------------------------------------------------------
    #  Trait definitions:
    #---------------------------------------------------------------------------

    # Should the color be updated automatically?
    auto_set = Bool(True)
    # Initial color space mode
    mode = Enum('rgb', 'hsv', 'hsv2', 'hsv3', cols=2)
    # Should the alpha channel be edited?
    edit_alpha = Bool(True)
    # Text to display in the color well
    text = Str('%R')
    # Font to use when displaying text
    font = KivaFont('modern 10')

    #---------------------------------------------------------------------------
    #  Traits view definition:
    #---------------------------------------------------------------------------

    traits_view = View([[
        'mapped{Is the value mapped?}',
        'auto_set{Should the value be set while dragging a slider?}',
        'edit_alpha{Should the alpha channel be edited?}', '|[Options]>'
    ], ['mode{Inital mode}@', '|[Color Space]'],
                        ['text', 'font@', '|[Color well]']])

    #---------------------------------------------------------------------------
    #  'Editor' factory methods:
    #---------------------------------------------------------------------------

    def simple_editor(self, ui, object, name, description, parent):
        return ColorEditor(
            parent,
            factory=self,
            ui=ui,
            object=object,
            name=name,
            description=description,
            style='simple')

    def custom_editor(self, ui, object, name, description, parent):
        return ColorEditor(
            parent,
            factory=self,
            ui=ui,
            object=object,
            name=name,
            description=description,
            style='custom')
Exemplo n.º 5
0
class MFTableOverlay(AbstractOverlay):
    dacs = List
    one_amu_dac = Float
    isotopes = List
    font = KivaFont('Helvetica 10')

    def overlay(self, component, gc, view_bounds=None, mode="normal"):
        with gc:
            gc.clip_to_rect(component.x, component.y, component.width,
                            component.height)
            y, y2 = component.y, component.y2

            color = (0.615, 0.823, 0.929, 0.631)
            gc.set_stroke_color(color)
            gc.set_fill_color(color)
            a, b = component.map_screen([(0, 0), (self.one_amu_dac, 0)])
            w = b[0] - a[0]
            h = y2 - y

            screen_dacs = []
            for d in self.dacs:
                x = component.map_screen([(d, 0)])[0][0]
                screen_dacs.append(x)
                gc.rect(x - w / 2., y, w, h)
            gc.draw_path()

            gc.set_line_width(2.5)
            gc.set_stroke_color((0, 0, 0))
            for x in screen_dacs:
                gc.move_to(x, y)
                gc.line_to(x, y2)
            gc.draw_path()

        with gc:
            gc.set_font(self.font)
            for x, iso in zip(screen_dacs, self.isotopes):
                gc.set_text_position(x, y2 + 5)
                gc.show_text(iso)
Exemplo n.º 6
0
class ChacoPlotItem(Item):
    """ A Traits UI Item for a Chaco plot, for use in Traits UI Views.

    NOTE: ComponentEditor is preferred over this class, as it is more flexible.
    """
    # Name of the trait that references the index data source.
    index = Str
    # Name of the trait that references the value data source.
    value = Str
    # Title of the plot (overlaid on the plot container).
    title = Str("Plot Editor")

    # Bounds of the x-axis, used if **x_auto** is False.
    x_bounds = AxisBounds
    # Set the x-axis bounds automatically?
    x_auto = Bool(True)
    # Bounds of the y-axis, used if **y_auto** is False.
    y_bounds = AxisBounds
    # Set the y-axis bounds automatically?
    y_auto = Bool(True)

    # The orientation of the index axis.
    orientation = Enum("h", "v")

    # If these are None, then the index/value trait names are used

    # Label of the x-axis; if None, the **index** name is used.
    x_label = Trait(None, None, Str)
    # Name of the trait on the object containing the label of the x-axis.
    # This takes precedence over **x_label**.
    x_label_trait = Trait(None, None, Str)
    # Font for the label of the x-axis.
    x_label_font = KivaFont("modern 10")
    # Color of the label of the x-axis.
    x_label_color = black_color_trait
    # Label of the y-axis; if None, the **value** name is used.
    y_label = Trait(None, None, Str)
    # Name of the trait on the object containing the label of the y-axis.
    # This takes precedence over **y_label**.
    y_label_trait = Trait(None, None, Str)
    # Font for the label of the y-axis.
    y_label_font = KivaFont("modern 10")
    # Color of the label of the y-axis.
    y_label_color = black_color_trait

    # General plot properties

    # Foreground olor of the plot.
    color = ColorTrait("blue")
    # Background color of the plot.
    bgcolor = white_color_trait
    # Background color of the plot (deprecated).
    bg_color = Property  # backwards compatibility; deprecated
    # Color of the background padding.
    padding_bg_color = ColorTrait("sys_window")

    # Border properties

    # Width of the plot border
    border_width = Int(1)
    # Is the border visible?
    border_visible = Bool(False)
    # Line style of the border.
    border_dash = LineStyle
    # Color of the border.
    border_color = black_color_trait

    # The type of the plot.
    type = Enum("line", "scatter")
    # The type of the plot as a string.
    type_trait = Str

    # plot-specific properties.  These might not apply to all plot types.

    # Type of marker (for plots that use markers).
    marker = MarkerTrait
    # Size of marker (for plots that use markers).
    marker_size = Int(4)
    # Marker outline color (for plots that user markers).
    outline_color = black_color_trait

    def __init__(self, index, value, type="line", **traits):
        self.index = index
        self.value = value
        self.type = type
        self.name = index
        super(ChacoPlotItem, self).__init__(**traits)

        self.editor = ChacoEditorFactory()

        self.editor.plotitem = self

        return

    def _set_bg_color(self, val):
        self.bgcolor = val

    def _get_bg_color(self):
        return self.bgcolor
Exemplo n.º 7
0
class MeanIndicatorOverlay(AbstractOverlay, Movable):
    color = Color
    label = Instance(PlotLabel)
    text = Str
    font = KivaFont('modern 15')
    x = Float
    error = Float
    nsigma = Int

    marker = Str('vertical')
    end_cap_length = Int(4)
    label_tool = Any

    def clear(self):
        self.altered_screen_point = None

    def hittest(self, pt, tol=5):
        x, y = pt
        if self.get_current_point():
            gx, gy = self.get_current_point()
            #print abs(gx-x)<tol , abs(gy-y)<tol
            return abs(gx - x) < tol and abs(gy - y) < tol
            #print x,y, gx, gy

    def _text_changed(self):
        label = self.label

        if label is None:
            label = XYPlotLabel(component=self.component,
                                font=self.font,
                                text=self.text,
                                color=self.color,
                                id='{}_label'.format(self.id))
            self.label = label
            self.overlays.append(label)
            tool = LabelMoveTool(component=label)
            self.tools.append(tool)
            self.label_tool = tool
        else:
            label.text = self.text
            #print self.label

    def _color_changed(self):
        color = self.color
        #if isinstance(color, str):
        #    color=color_table[color]
        self._color = map(lambda x: x / 255., color.toTuple())
        #self._color=color

    def overlay(self, other_component, gc, view_bounds=None, mode="normal"):
        if self.label:
            self.label.font.face_name = ''

        with gc:
            oc = other_component
            gc.clip_to_rect(oc.x, oc.y, oc.x2, oc.y2)
            points = self._gather_data()
            #print points, self.x, self.y
            marker = self.marker

            color = self._color
            line_width = 1
            outline_color = self._color
            if marker != 'vertical':
                marker_size = 3
                render_markers(gc, points, marker, marker_size, color,
                               line_width, outline_color)
            else:
                render_vertical_marker(gc, points, color, line_width,
                                       outline_color)

            x, y = self.get_current_point()

            # e = self.error / 2.0 * max(1, self.nsigma)
            e = self.error * max(1, self.nsigma)
            p1, p2 = self.component.map_screen([(self.x - e, 0),
                                                (self.x + e, 0)])

            render_error_bar(gc,
                             p1[0],
                             p2[0],
                             y,
                             self._color,
                             end_caps=self.end_cap_length)

        for o in self.overlays:
            o.overlay(other_component, gc, view_bounds=view_bounds, mode=mode)

    def get_current_point(self):
        data_pt = self.altered_screen_point
        #print 'adsfsadf', data_pt, #len(data_pt)
        if data_pt is None:
            data_pt = self.current_screen_point
        return data_pt

    def _gather_data(self):
        if self.altered_screen_point is None:
            comp = self.component
            x = comp.map_screen([(self.x, 0)])[0, 0]
            if self.label:
                if not self.label.altered_screen_point:
                    self.label.sx = x
                    self.label.sy = self.y
            self.current_screen_point = (x, self.y)

            return [(x, self.y)]
        else:
            if self.label:
                if not self.label.altered_screen_point:
                    self.label.sx, self.label.sy = self.altered_screen_point
            return [self.altered_screen_point]

    def set_x(self, x):
        self.x = x
        comp = self.component
        x = comp.map_screen([(self.x, 0)])[0, 0]
        if self.label:
            if not self.label.altered_screen_point:
                self.label.sx = x
                self.label.sy = self.y

        if self.altered_screen_point:
            self.altered_screen_point = (x, self.altered_screen_point[1])
        else:
            self.current_screen_point = (x, self.y)
Exemplo n.º 8
0
class VUMeter(Component):

    # Value expressed in dB
    db = Property(Float)

    # Value expressed as a percent.
    percent = Range(low=0.0)

    # The maximum value to be display in the VU Meter, expressed as a percent.
    max_percent = Float(150.0)

    # Angle (in degrees) from a horizontal line through the hinge of the
    # needle to the edge of the meter axis.
    angle = Float(45.0)

    # Values of the percentage-based ticks; these are drawn and labeled along
    # the bottom of the curve axis.
    percent_ticks = List(list(sm.range(0, 101, 20)))

    # Text to write in the middle of the VU Meter.
    text = Str("VU")

    # Font used to draw `text`.
    text_font = KivaFont("modern 48")

    # Font for the db tick labels.
    db_tick_font = KivaFont("modern 16")

    # Font for the percent tick labels.
    percent_tick_font = KivaFont("modern 12")

    # beta is the fraction of the of needle that is "hidden".
    # beta == 0 puts the hinge point of the needle on the bottom
    # edge of the window.  Values that result in a decent looking
    # meter are 0 < beta < .65.
    # XXX needs a better name!
    _beta = Float(0.3)

    # _outer_radial_margin is the radial extent beyond the circular axis
    # to include  in calculations of the space required for the meter.
    # This allows room for the ticks and labels.
    _outer_radial_margin = Float(60.0)

    # The angle (in radians) of the span of the curve axis.
    _phi = Property(Float, depends_on=['angle'])

    # This is the radius of the circular axis (in screen coordinates).
    _axis_radius = Property(Float, depends_on=['_phi', 'width', 'height'])

    #---------------------------------------------------------------------
    # Trait Property methods
    #---------------------------------------------------------------------

    def _get_db(self):
        db = percent_to_db(self.percent)
        return db

    def _set_db(self, value):
        self.percent = db_to_percent(value)

    def _get__phi(self):
        phi = math.pi * (180.0 - 2 * self.angle) / 180.0
        return phi

    def _get__axis_radius(self):
        M = self._outer_radial_margin
        beta = self._beta
        w = self.width
        h = self.height
        phi = self._phi

        R1 = w / (2 * math.sin(phi / 2)) - M
        R2 = (h - M) / (1 - beta * math.cos(phi / 2))
        R = min(R1, R2)
        return R

    #---------------------------------------------------------------------
    # Trait change handlers
    #---------------------------------------------------------------------

    def _anytrait_changed(self):
        self.request_redraw()

    #---------------------------------------------------------------------
    # Component API
    #---------------------------------------------------------------------

    def _draw_mainlayer(self, gc, view_bounds=None, mode="default"):

        beta = self._beta
        phi = self._phi

        w = self.width

        M = self._outer_radial_margin
        R = self._axis_radius

        # (ox, oy) is the position of the "hinge point" of the needle
        # (i.e. the center of rotation).  For beta > ~0, oy is negative,
        # so this point is below the visible region.
        ox = self.x + self.width // 2
        oy = -beta * R * math.cos(phi / 2) + 1

        left_theta = math.radians(180 - self.angle)
        right_theta = math.radians(self.angle)

        # The angle of the 100% position.
        nominal_theta = self._percent_to_theta(100.0)

        # The color of the axis for percent > 100.
        red = (0.8, 0, 0)

        with gc:
            gc.set_antialias(True)

            # Draw everything relative to the center of the circles.
            gc.translate_ctm(ox, oy)

            # Draw the primary ticks and tick labels on the curved axis.
            gc.set_fill_color((0, 0, 0))
            gc.set_font(self.db_tick_font)
            for db in [-20, -10, -7, -5, -3, -2, -1, 0, 1, 2, 3]:
                db_percent = db_to_percent(db)
                theta = self._percent_to_theta(db_percent)
                x1 = R * math.cos(theta)
                y1 = R * math.sin(theta)
                x2 = (R + 0.3 * M) * math.cos(theta)
                y2 = (R + 0.3 * M) * math.sin(theta)
                gc.set_line_width(2.5)
                gc.move_to(x1, y1)
                gc.line_to(x2, y2)
                gc.stroke_path()

                text = str(db)
                if db > 0:
                    text = '+' + text
                self._draw_rotated_label(gc, text, theta, R + 0.4 * M)

            # Draw the secondary ticks on the curve axis.
            for db in [-15, -9, -8, -6, -4, -0.5, 0.5]:
                ##db_percent = 100 * math.pow(10.0, db / 20.0)
                db_percent = db_to_percent(db)
                theta = self._percent_to_theta(db_percent)
                x1 = R * math.cos(theta)
                y1 = R * math.sin(theta)
                x2 = (R + 0.2 * M) * math.cos(theta)
                y2 = (R + 0.2 * M) * math.sin(theta)
                gc.set_line_width(1.0)
                gc.move_to(x1, y1)
                gc.line_to(x2, y2)
                gc.stroke_path()

            # Draw the percent ticks and label on the bottom of the
            # curved axis.
            gc.set_font(self.percent_tick_font)
            gc.set_fill_color((0.5, 0.5, 0.5))
            gc.set_stroke_color((0.5, 0.5, 0.5))
            percents = self.percent_ticks
            for tick_percent in percents:
                theta = self._percent_to_theta(tick_percent)
                x1 = (R - 0.15 * M) * math.cos(theta)
                y1 = (R - 0.15 * M) * math.sin(theta)
                x2 = R * math.cos(theta)
                y2 = R * math.sin(theta)
                gc.set_line_width(2.0)
                gc.move_to(x1, y1)
                gc.line_to(x2, y2)
                gc.stroke_path()

                text = str(tick_percent)
                if tick_percent == percents[-1]:
                    text = text + "%"
                self._draw_rotated_label(gc, text, theta, R - 0.3 * M)

            if self.text:
                gc.set_font(self.text_font)
                tx, ty, tw, th = gc.get_text_extent(self.text)
                gc.set_fill_color((0, 0, 0, 0.25))
                gc.set_text_matrix(affine.affine_from_rotation(0))
                gc.set_text_position(-0.5 * tw, (0.75 * beta + 0.25) * R)
                gc.show_text(self.text)

            # Draw the red curved axis.
            gc.set_stroke_color(red)
            w = 10
            gc.set_line_width(w)
            gc.arc(0, 0, R + 0.5 * w - 1, right_theta, nominal_theta)
            gc.stroke_path()

            # Draw the black curved axis.
            w = 4
            gc.set_line_width(w)
            gc.set_stroke_color((0, 0, 0))
            gc.arc(0, 0, R + 0.5 * w - 1, nominal_theta, left_theta)
            gc.stroke_path()

            # Draw the filled arc at the bottom.
            gc.set_line_width(2)
            gc.set_stroke_color((0, 0, 0))
            gc.arc(0, 0, beta * R, math.radians(self.angle),
                   math.radians(180 - self.angle))
            gc.stroke_path()
            gc.set_fill_color((0, 0, 0, 0.25))
            gc.arc(0, 0, beta * R, math.radians(self.angle),
                   math.radians(180 - self.angle))
            gc.fill_path()

            # Draw the needle.
            percent = self.percent
            # If percent exceeds max_percent, the needle is drawn at max_percent.
            if percent > self.max_percent:
                percent = self.max_percent
            needle_theta = self._percent_to_theta(percent)
            gc.rotate_ctm(needle_theta - 0.5 * math.pi)
            self._draw_vertical_needle(gc)

    #---------------------------------------------------------------------
    # Private methods
    #---------------------------------------------------------------------

    def _draw_vertical_needle(self, gc):
        """ Draw the needle of the meter, pointing straight up. """
        beta = self._beta
        R = self._axis_radius
        end_y = beta * R
        blob_y = R - 0.6 * self._outer_radial_margin
        tip_y = R + 0.2 * self._outer_radial_margin
        lw = 5

        with gc:
            gc.set_alpha(1)
            gc.set_fill_color((0, 0, 0))

            # Draw the needle from the bottom to the blob.
            gc.set_line_width(lw)
            gc.move_to(0, end_y)
            gc.line_to(0, blob_y)
            gc.stroke_path()

            # Draw the thin part of the needle from the blob to the tip.
            gc.move_to(lw, blob_y)
            control_y = blob_y + 0.25 * (tip_y - blob_y)
            gc.quad_curve_to(0.2 * lw, control_y, 0, tip_y)
            gc.quad_curve_to(-0.2 * lw, control_y, -lw, blob_y)
            gc.line_to(lw, blob_y)
            gc.fill_path()

            # Draw the blob on the needle.
            gc.arc(0, blob_y, 6.0, 0, 2 * math.pi)
            gc.fill_path()

    def _draw_rotated_label(self, gc, text, theta, radius):

        tx, ty, tw, th = gc.get_text_extent(text)

        rr = math.sqrt(radius**2 + (0.5 * tw)**2)
        dtheta = math.atan2(0.5 * tw, radius)
        text_theta = theta + dtheta
        x = rr * math.cos(text_theta)
        y = rr * math.sin(text_theta)

        rot_theta = theta - 0.5 * math.pi
        with gc:
            gc.set_text_matrix(affine.affine_from_rotation(rot_theta))
            gc.set_text_position(x, y)
            gc.show_text(text)

    def _percent_to_theta(self, percent):
        """ Convert percent to the angle theta, in radians.

        theta is the angle of the needle measured counterclockwise from
        the horizontal (i.e. the traditional angle of polar coordinates).
        """
        angle = (self.angle + (180.0 - 2 * self.angle) *
                 (self.max_percent - percent) / self.max_percent)
        theta = math.radians(angle)
        return theta

    def _db_to_theta(self, db):
        """ Convert db to the angle theta, in radians. """
        percent = db_to_percent(db)
        theta = self._percent_to_theta(percent)
        return theta
Exemplo n.º 9
0
class Label(HasTraits):
    """ A label used by overlays.

    Label is not a Component; it's just an object encapsulating text settings
    and appearance attributes.  It can be used by components that need text
    labels to store state, perform layout, and render the text.
    """

    # The anchor point is the position on the label that is placed at the
    # label's position.  The label is also rotated relative to this point.
    # "Left" refers to the left edge of the text's bounding box (including
    # margin), while "center" refers to the horizontal and vertical center
    # of the bounding box.
    # TODO: Implement this and test thoroughly
    #anchor = Enum("left", "right", "top", "bottom", "center",
    #              "top left", "top right", "bottom left", "bottom right")

    # The label text.  Carriage returns (\n) are always connverted into
    # line breaks.
    text = Str

    # The angle of rotation of the label.
    rotate_angle = Float(0)

    # The color of the label text.
    color = black_color_trait

    # The background color of the label.
    bgcolor = transparent_color_trait

    # The width of the label border. If it is 0, then it is not shown.
    border_width = Int(0)

    # The color of the border.
    border_color = black_color_trait

    # Whether or not the border is visible
    border_visible = Bool(True)

    # The font of the label text.
    font = KivaFont("modern 10")

    # Number of pixels of margin around the label, for both X and Y dimensions.
    margin = Int(2)

    # Number of pixels of spacing between lines of text.
    line_spacing = Int(5)

    # Number of pixels to limit the width of the label to. Lines which are
    # too long will be broken to fit on word boundaries. Line width is
    # calculated without considering the value of `margin`.
    # A `max_width` of 0.0 means that lines will not be broken.
    max_width = Float(0.0)

    #------------------------------------------------------------------------
    # Private traits
    #------------------------------------------------------------------------

    _bounding_box = List()
    _position_cache_valid = Bool(False)
    _text_needs_fitting = Bool(False)
    _line_xpos = Any()
    _line_ypos = Any()
    _rot_matrix = Any()

    def __init__(self, **traits):
        super(Label, self).__init__(**traits)
        self._bounding_box = [0, 0]
        return

    def get_width_height(self, gc):
        """ Returns the width and height of the label, in the rotated frame of
        reference.
        """
        self._fit_text_to_max_width(gc)
        self._calc_line_positions(gc)
        width, height = self._bounding_box
        return width, height

    def get_bounding_box(self, gc):
        """ Returns a rectangular bounding box for the Label as (width,height).
        """
        width, height = self.get_width_height(gc)
        if self.rotate_angle in (90.0, 270.0):
            return (height, width)
        elif self.rotate_angle in (0.0, 180.0):
            return (width, height)
        else:
            angle = self.rotate_angle
            return (abs(width*cos(angle))+abs(height*sin(angle)),
                    abs(height*sin(angle))+abs(width*cos(angle)))

    def get_bounding_poly(self, gc):
        """ Returns a list [(x0,y0), (x1,y1),...] of tuples representing a
        polygon that bounds the label.
        """
        width, height = self.get_width_height(gc)
        offset = array(self.get_bounding_box(gc))/2.
        # unrotated points relative to centre
        base_points = [
            array([[-width/2.], [-height/2.]]),
            array([[-width/2.], [height/2.]]),
            array([[width/2.], [height/2.]]),
            array([[width/2.], [-height/2.]]),
            array([[-width/2.], [-height/2.]]),
        ]
        # rotate about centre, and offset to bounding box coords
        points = [dot(self.get_rotation_matrix(), point).transpose()[0]+offset
                  for point in base_points]
        return points

    def get_rotation_matrix(self):
        return array([[cos(self.rotate_angle), -sin(self.rotate_angle)],
                     [sin(self.rotate_angle), cos(self.rotate_angle)]])

    def draw(self, gc):
        """ Draws the label.

        This method assumes the graphics context has been translated to the
        correct position such that the origin is at the lower left-hand corner
        of this text label's box.
        """
        # Make sure `max_width` is respected
        self._fit_text_to_max_width(gc)

        # For this version we're not supporting rotated text.
        self._calc_line_positions(gc)

        with gc:
            bb_width, bb_height = self.get_bounding_box(gc)

            # Rotate label about center of bounding box
            width, height = self._bounding_box
            gc.translate_ctm(bb_width/2.0, bb_height/2.0)
            gc.rotate_ctm(pi/180.0*self.rotate_angle)
            gc.translate_ctm(-width/2.0, -height/2.0)

            # Draw border and fill background
            if self.bgcolor != "transparent":
                gc.set_fill_color(self.bgcolor_)
                gc.draw_rect((0, 0, width, height), FILL)
            if self.border_visible and self.border_width > 0:
                gc.set_stroke_color(self.border_color_)
                gc.set_line_width(self.border_width)
                border_offset = (self.border_width-1)/2.0
                gc.rect(border_offset, border_offset,
                        width-2*border_offset, height-2*border_offset)
                gc.stroke_path()

            gc.set_fill_color(self.color_)
            gc.set_stroke_color(self.color_)
            gc.set_font(self.font)
            if self.font.size <= 8.0:
                gc.set_antialias(0)
            else:
                gc.set_antialias(1)

            lines = self.text.split("\n")
            if self.border_visible:
                gc.translate_ctm(self.border_width, self.border_width)
            width, height = self.get_width_height(gc)

            for i, line in enumerate(lines):
                if line == "":
                    continue
                x_offset = round(self._line_xpos[i])
                y_offset = round(self._line_ypos[i])
                gc.set_text_position(x_offset, y_offset)
                gc.show_text(line)

    #------------------------------------------------------------------------
    # Trait handlers
    #------------------------------------------------------------------------

    def _text_changed(self):
        self._text_needs_fitting = (self.max_width > 0.0)

    @on_trait_change("font,margin,text,rotate_angle")
    def _invalidate_position_cache(self):
        self._position_cache_valid = False

    #------------------------------------------------------------------------
    # Private methods
    #------------------------------------------------------------------------

    def _fit_text_to_max_width(self, gc):
        """ Break the text into lines whose width is no greater than
        `max_width`.
        """
        if self._text_needs_fitting:
            lines = []

            with gc:
                gc.set_font(self.font)
                for line in self.text.split('\n'):
                    if line == "":
                        lines.append(line)
                        continue

                    width = gc.get_full_text_extent(line)[0]
                    if width > self.max_width:
                        line_words = []
                        for word in line.split():
                            line_words.append(word)
                            test_line = ' '.join(line_words)
                            width = gc.get_full_text_extent(test_line)[0]
                            if width > self.max_width:
                                if len(line_words) > 1:
                                    lines.append(' '.join(line_words[:-1]))
                                    line_words = [word]
                                else:
                                    lines.append(word)
                                    line_words = []
                        if len(line_words) > 0:
                            lines.append(' '.join(line_words))
                    else:
                        lines.append(line)
            self.trait_setq(text='\n'.join(lines))
            self._text_needs_fitting = False

    def _calc_line_positions(self, gc):
        if not self._position_cache_valid:
            with gc:
                gc.set_font(self.font)
                # The bottommost line starts at postion (0, 0).
                x_pos = []
                y_pos = []
                self._bounding_box = [0, 0]
                margin = self.margin
                prev_y_pos = margin
                prev_y_height = -self.line_spacing
                max_width = 0
                for line in self.text.split("\n")[::-1]:
                    if line != "":
                        (width, height, descent, leading) = \
                            gc.get_full_text_extent(line)
                        ascent = height - abs(descent)
                        if width > max_width:
                            max_width = width
                        new_y_pos = prev_y_pos + prev_y_height \
                            + self.line_spacing
                    else:
                        # For blank lines, we use the height of the previous
                        # line, if there is one.  The width is 0.
                        leading = 0
                        if prev_y_height != -self.line_spacing:
                            new_y_pos = prev_y_pos + prev_y_height \
                                + self.line_spacing
                            ascent = prev_y_height
                        else:
                            new_y_pos = prev_y_pos
                            ascent = 0
                    x_pos.append(-leading + margin)
                    y_pos.append(new_y_pos)
                    prev_y_pos = new_y_pos
                    prev_y_height = ascent

            self._line_xpos = x_pos[::-1]
            self._line_ypos = y_pos[::-1]
            border_width = self.border_width if self.border_visible else 0
            self._bounding_box[0] = max_width + 2*margin + 2*border_width
            self._bounding_box[1] = prev_y_pos + prev_y_height + margin \
                + 2*border_width
            self._position_cache_valid = True
        return
Exemplo n.º 10
0
class tcPlot(BarPlot):
    """custom plot to draw the timechart
    probably not very 'chacotic' We draw the chart as a whole
    """
    # 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 9')
    # The font of the title.
    title_font_large = KivaFont('modern 15')
    # The font of the title.
    title_font_huge = KivaFont('modern 20')
    # The spacing between the axis line and the title
    title_spacing = Trait('auto', 'auto', Float)
    # The color of the title.
    title_color = ColorTrait("black")
    not_on_screen = List
    on_screen = List
    options = TimeChartOptions()
    range_tools = RangeSelectionTools()
    redraw_timer = None

    def invalidate(self):
        self.invalidate_draw()
        self.request_redraw()

    def immediate_invalidate(self):
        self.invalidate_draw()
        self.request_redraw_delayed()

    def request_redraw_delayed(self):
        self.redraw_timer.Stop()
        BarPlot.request_redraw(self)

    def request_redraw(self):
        if self.redraw_timer == None:
            self.redraw_timer = timer.Timer(30, self.request_redraw_delayed)
        self.redraw_timer.Start()

    def auto_zoom_y(self):
        if self.value_range.high != self.max_y + 1 or self.value_range.low != self.min_y:
            self.value_range.high = self.max_y + 1
            self.value_range.low = self.min_y
            self.invalidate_draw()
            self.request_redraw()

    def _gather_timechart_points(self, start_ts, end_ts, y, step):
        low_i = searchsorted(end_ts, self.index_mapper.range.low)
        high_i = searchsorted(start_ts, self.index_mapper.range.high)
        if low_i == high_i:
            return array([])

        start_ts = start_ts[low_i:high_i]
        end_ts = end_ts[low_i:high_i]
        points = column_stack(
            (start_ts, end_ts, zeros(high_i - low_i) + (y + step),
             ones(high_i - low_i) + (y - step),
             array(list(range(low_i, high_i)))))
        return points

    def _draw_label(self, gc, label, text, x, y):
        label.text = text
        l_w, l_h = label.get_width_height(gc)
        offset = array((x, y - l_h / 2))
        gc.translate_ctm(*offset)
        label.draw(gc)
        gc.translate_ctm(*(-offset))
        return l_w, l_h

    def _draw_timechart(self, gc, tc, label, base_y):
        bar_middle_y = self.first_bar_y + (base_y + .5) * self.bar_height
        points = self._gather_timechart_points(tc.start_ts, tc.end_ts, base_y,
                                               .2)
        overview = None
        if self.options.use_overview:
            if points.size > 500:
                overview = tc.get_overview_ts(self.overview_threshold)
                points = self._gather_timechart_points(overview[0],
                                                       overview[1], base_y, .2)

        if self.options.remove_pids_not_on_screen and points.size == 0:
            return 0
        if bar_middle_y + self.bar_height < self.y or bar_middle_y - self.bar_height > self.y + self.height:
            return 1  #quickly decide we are not on the screen
        self._draw_bg(gc, base_y, tc.bg_color)
        # we are too short in height, dont display all the labels
        if self.last_label >= bar_middle_y:
            # draw label
            l_w, l_h = self._draw_label(gc, label, tc.name, self.x,
                                        bar_middle_y)
            self.last_label = bar_middle_y - 8
        else:
            l_w, l_h = 0, 0
        if points.size != 0:
            # draw the middle line from end of label to end of screen
            if l_w != 0:  # we did not draw label because too short on space
                gc.set_alpha(0.2)
                gc.move_to(self.x + l_w, bar_middle_y)
                gc.line_to(self.x + self.width, bar_middle_y)
                gc.draw_path()
            gc.set_alpha(0.5)
            # map the bars start and stop locations into screen space
            lower_left_pts = self.map_screen(points[:, (0, 2)])
            upper_right_pts = self.map_screen(points[:, (1, 3)])
            bounds = upper_right_pts - lower_left_pts

            if overview:  # critical path, we only draw unicolor rects
                #calculate the mean color
                #print points.size
                gc.set_fill_color(get_aggcolor_by_id(get_color_id("overview")))
                gc.set_alpha(.9)
                rects = column_stack((lower_left_pts, bounds))
                gc.rects(rects)
                gc.draw_path()
            else:
                # lets display them more nicely
                rects = column_stack((lower_left_pts, bounds, points[:, (4)]))
                last_t = -1
                gc.save_state()
                for x, y, sx, sy, i in rects:
                    t = tc.types[int(i)]

                    if last_t != t:
                        # only draw when we change color. agg will then simplify the path
                        # note that a path only can only have one color in agg.
                        gc.draw_path()
                        gc.set_fill_color(get_aggcolor_by_id(int(t)))
                        last_t = t
                    gc.rect(x, y, sx, sy)
                # draw last path
                gc.draw_path()
                if tc.has_comments:
                    for x, y, sx, sy, i in rects:
                        if sx < 8:  # not worth calculatig text size
                            continue
                        label.text = tc.get_comment(i)
                        l_w, l_h = label.get_width_height(gc)
                        if l_w < sx:
                            offset = array(
                                (x, y + self.bar_height * .6 / 2 - l_h / 2))
                            gc.translate_ctm(*offset)
                            label.draw(gc)
                            gc.translate_ctm(*(-offset))

            if tc.max_latency > 0:  # emphase events where max_latency is reached
                ts = tc.max_latency_ts
                if ts.size > 0:
                    points = self._gather_timechart_points(ts, ts, base_y, 0)
                    if points.size > 0:
                        # map the bars start and stop locations into screen space
                        gc.set_alpha(1)
                        lower_left_pts = self.map_screen(points[:, (0, 2)])
                        upper_right_pts = self.map_screen(points[:, (1, 3)])
                        bounds = upper_right_pts - lower_left_pts
                        rects = column_stack((lower_left_pts, bounds))
                        gc.rects(rects)
                        gc.draw_path()
                        #print('gc.draw_path() ', __file__)
                        #assert True, 'draw_path'
        return 1

    def _draw_freqchart(self, gc, tc, label, y):
        self._draw_bg(gc, y, tc.bg_color)
        low_i = searchsorted(tc.start_ts, self.index_mapper.range.low)
        high_i = searchsorted(tc.start_ts, self.index_mapper.range.high)

        if low_i > 0:
            low_i -= 1
        if high_i < len(tc.start_ts):
            high_i += 1
        if low_i >= high_i - 1:
            return array([])
        start_ts = tc.start_ts[low_i:high_i - 1]
        end_ts = tc.start_ts[low_i + 1:high_i]
        values = (tc.types[low_i:high_i - 1] / (float(tc.max_types))) + y
        starts = column_stack((start_ts, values))
        ends = column_stack((end_ts, values))
        starts = self.map_screen(starts)
        ends = self.map_screen(ends)
        gc.begin_path()
        gc.line_set(starts, ends)
        gc.stroke_path()
        for i in range(len(starts)):
            x1, y1 = starts[i]
            x2, y2 = ends[i]
            sx = x2 - x1
            if sx > 8:
                label.text = str(tc.types[low_i + i])
                l_w, l_h = label.get_width_height(gc)
                if l_w < sx:
                    if x1 < 0: x1 = 0
                    offset = array((x1, y1))
                    gc.translate_ctm(*offset)
                    label.draw(gc)
                    gc.translate_ctm(*(-offset))

    def _draw_wake_ups(self, gc, processes_y):
        low_i = searchsorted(self.proj.wake_events['time'],
                             self.index_mapper.range.low)
        high_i = searchsorted(self.proj.wake_events['time'],
                              self.index_mapper.range.high)
        gc.set_stroke_color((0, 0, 0, .6))
        for i in range(low_i, high_i):
            waker, wakee, ts = self.proj.wake_events[i]
            if wakee in processes_y and waker in processes_y:
                y1 = processes_y[wakee]
                y2 = processes_y[waker]
                x, y = self.map_screen(array((ts, y1)))
                gc.move_to(x, y)
                y2 = processes_y[waker]
                x, y = self.map_screen(array((ts, y2)))
                gc.line_to(x, y)
                x, y = self.map_screen(array((ts, (y1 + y2) / 2)))
                if y1 > y2:
                    y += 5
                    dy = -5
                else:
                    y -= 5
                    dy = +5
                gc.move_to(x, y)
                gc.line_to(x - 3, y + dy)
                gc.move_to(x, y)
                gc.line_to(x + 3, y + dy)

        gc.draw_path()

    def _draw_bg(self, gc, y, color):
        gc.set_alpha(1)
        gc.set_line_width(0)
        gc.set_fill_color(color)
        this_bar_y = self.map_screen(array((0, y)))[1]
        gc.rect(self.x, this_bar_y, self.width, self.bar_height)
        gc.draw_path()
        gc.set_line_width(self.line_width)
        gc.set_alpha(0.5)

    def _draw_plot(self, gc, view_bounds=None, mode="normal"):
        gc.save_state()
        gc.clip_to_rect(self.x, self.y, self.width, self.height)
        gc.set_antialias(1)
        gc.set_stroke_color(self.line_color_)
        gc.set_line_width(self.line_width)
        self.first_bar_y = self.map_screen(array((0, 0)))[1]
        self.last_label = self.height + self.y
        self.bar_height = self.map_screen(array((0, 1)))[1] - self.first_bar_y
        self.max_y = y = self.proj.num_cpu * 2 + self.proj.num_process - 1
        if self.bar_height > 15:
            font = self.title_font_large
        else:
            font = self.title_font
        label = Label(text="",
                      font=font,
                      color=self.title_color,
                      rotate_angle=0)
        # we unmap four pixels on screen, and find the nearest greater power of two
        # this by rounding the log2, and then exponentiate again
        # as the overview data is cached, this avoids using too much memory

        four_pixels = self.index_mapper.map_data(array((0, 4)))
        if len(four_pixels) == 1:
            self.overview_threshold = 1 << int(
                log(1 + int(four_pixels[0] - four_pixels[0]), 2))
        else:
            self.overview_threshold = 1 << int(
                log(1 + int(four_pixels[1] - four_pixels[0]), 2))

        for i in range(len(self.proj.c_states)):
            tc = self.proj.c_states[i]
            if self.options.show_c_states:
                self._draw_timechart(gc, tc, label, y)
                y -= 1
            tc = self.proj.p_states[i]
            if self.options.show_p_states:
                self._draw_freqchart(gc, tc, label, y)
                y -= 1
        processes_y = {0xffffffffffffffff: y + 1}
        not_on_screen = []
        on_screen = []

        for tc in self.proj.processes:
            if tc.show == False:
                continue
            processes_y[(tc.comm, tc.pid)] = y + .5
            if self._draw_timechart(
                    gc, tc, label,
                    y) or not self.options.remove_pids_not_on_screen:
                y -= 1
                on_screen.append(tc)
            else:
                not_on_screen.append(tc)
        self.not_on_screen = not_on_screen
        self.on_screen = on_screen
        if self.options.show_wake_events:
            self._draw_wake_ups(gc, processes_y)

        message = ""
        if self.proj.filename == "dummy":
            message = "please load a trace file in the 'file' menu"
        elif len(self.proj.processes) == 0:
            message = "no processes??! is your trace empty?"
        if message:
            label.text = message
            label.font = self.title_font_huge
            gc.translate_ctm(100, (self.y + self.height) / 2)
            label.draw(gc)

        gc.restore_state()
        self.min_y = y
        if self.options.auto_zoom_y:
            self.options.auto_zoom_timer.Start()

    def _on_hide_others(self):
        for i in self.not_on_screen:
            i.show = False
        self.invalidate_draw()
        self.request_redraw()

    def _on_hide_onscreen(self):
        for i in self.on_screen:
            i.show = False
        self.invalidate_draw()
        self.request_redraw()
Exemplo n.º 11
0
class Button(Component):

    color = ColorTrait((0.6, 0.6, 0.6, 1.0))

    down_color = ColorTrait("gray")

    border_color = ColorTrait((0.4, 0.4, 0.4, 1.0))

    # important for rendering rounded buttons properly, since the default for
    # the Component parent class is 'white'
    bgcolor = "clear"

    label = Str

    label_font = KivaFont("modern 11 bold")

    label_color = ColorTrait("white")

    label_shadow = ColorTrait("gray")

    shadow_text = Bool(True)

    label_padding = Int(5)

    height = Int(20)

    button_state = Enum("up", "down")

    end_radius = Int(10)

    # Default size of the button if no label is present
    bounds=[32,32]

    # Cached value of the measured sizes of self.label
    _text_extents = Tuple

    def perform(self, event):
        """
        Called when the button is depressed.  'event' is the Enable mouse event
        that triggered this call.
        """
        pass

    def _draw_mainlayer(self, gc, view_bounds, mode="default"):
        if self.button_state == "up":
            self.draw_up(gc, view_bounds)
        else:
            self.draw_down(gc, view_bounds)
        return

    def draw_up(self, gc, view_bounds):
        with gc:
            gc.set_fill_color(self.color_)
            self._draw_actual_button(gc)
        return

    def draw_down(self, gc, view_bounds):
        with gc:
            gc.set_fill_color(self.down_color_)
            self._draw_actual_button(gc)
        return

    def _draw_actual_button(self, gc):
        gc.set_stroke_color(self.border_color_)
        gc.begin_path()

        gc.move_to(self.x + self.end_radius, self.y)

        gc.arc_to(self.x + self.width, self.y,
                self.x + self.width,
                self.y + self.end_radius, self.end_radius)
        gc.arc_to(self.x + self.width,
                self.y + self.height,
                self.x + self.width - self.end_radius,
                self.y + self.height, self.end_radius)
        gc.arc_to(self.x, self.y + self.height,
                self.x, self.y,
                self.end_radius)
        gc.arc_to(self.x, self.y,
                self.x + self.width + self.end_radius,
                self.y, self.end_radius)

        gc.draw_path()
        self._draw_label(gc)

    def _draw_label(self, gc):
        if self.label != "":
            if self._text_extents is None or len(self._text_extents) == 0:
                self._recompute_font_metrics()
            x,y,w,h = self._text_extents
            gc.set_font(self.label_font)
            text_offset = 0.0

            if self.shadow_text:
                # Draw shadow text
                gc.set_fill_color(self.label_shadow_)
                x_pos = self.x + (self.width-w-x)/2 + 0.5
                y_pos = self.y + (self.height-h-y)/2 - 0.5
                gc.show_text_at_point(self.label, x_pos, y_pos)
                text_offset = 0.5

            # Draw foreground text to button
            gc.set_fill_color(self.label_color_)
            x_pos = self.x + (self.width-w-x)/2 - text_offset
            y_pos = self.y + (self.height-h-y)/2 + text_offset
            gc.show_text_at_point(self.label, x_pos, y_pos)

        return

    def normal_left_down(self, event):
        self.button_state = "down"
        self.request_redraw()
        event.handled = True
        return

    def normal_left_up(self, event):
        self.button_state = "up"
        self.request_redraw()
        self.perform(event)
        event.handled = True
        return

    def _recompute_font_metrics(self):
        if self.label != "":
            metrics = font_metrics_provider()
            metrics.set_font(self.label_font)
            self._text_extents = metrics.get_text_extent(self.label)

    def _label_font_changed(self, old, new):
        self._recompute_font_metrics()

    def _label_changed(self, old, new):
        self._recompute_font_metrics()
Exemplo n.º 12
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
Exemplo n.º 13
0
def __line_style_trait(value='solid', **metadata):
    return Trait(value,
                 __line_style_trait_values,
                 editor=LineStyleEditor,
                 **metadata)


# A mapped trait for use in specification of line style attributes.
LineStyle = TraitFactory(__line_style_trait)

#-------------------------------------------------------------------------------
#  Trait definitions:
#-------------------------------------------------------------------------------

# Font trait:
font_trait = KivaFont(default_font_name)

# Bounds trait
bounds_trait = CList([0.0, 0.0])  # (w,h)
coordinate_trait = CList([0.0, 0.0])  # (x,y)

#bounds_trait = Trait((0.0, 0.0, 20.0, 20.0), valid_bounds, editor=bounds_editor)

# Component minimum size trait
# PZW: Make these just floats, or maybe remove them altogether.
ComponentMinSize = Range(0.0, 99999.0)
ComponentMaxSize = ComponentMinSize(99999.0)

# Pointer shape trait:
Pointer = Trait('arrow', TraitPrefixList(pointer_shapes))
Exemplo n.º 14
0
class Legend(AbstractOverlay):
    """ A legend for a plot.
    """
    # The font to use for the legend text.
    font = KivaFont("modern 12")

    # The amount of space between the content of the legend and the border.
    border_padding = Int(10)

    # The border is visible (overrides Enable Component).
    border_visible = True

    # The color of the text labels
    color = black_color_trait

    # The background color of the legend (overrides AbstractOverlay).
    bgcolor = white_color_trait

    # The position of the legend with respect to its overlaid component.  (This
    # attribute applies only if the legend is used as an overlay.)
    #
    # * ur = Upper Right
    # * ul = Upper Left
    # * ll = Lower Left
    # * lr = Lower Right
    align = Enum("ur", "ul", "ll", "lr")

    # The amount of space between legend items.
    line_spacing = Int(3)

    # The size of the icon or marker area drawn next to the label.
    icon_bounds = List([24, 24])

    # Amount of spacing between each label and its icon.
    icon_spacing = Int(5)

    # Map of labels (strings) to plot instances or lists of plot instances.  The
    # Legend determines the appropriate rendering of each plot's marker/line.
    plots = Dict

    # The list of labels to show and the order to show them in.  If this
    # list is blank, then the keys of self.plots is used and displayed in
    # alphabetical order.  Otherwise, only the items in the **labels**
    # list are drawn in the legend.  Labels are ordered from top to bottom.
    labels = List

    # Whether or not to hide plots that are not visible.  (This is checked during
    # layout.)  This option *will* filter out the items in **labels** above, so
    # if you absolutely, positively want to set the items that will always
    # display in the legend, regardless of anything else, then you should turn
    # this option off.  Otherwise, it usually makes sense that a plot renderer
    # that is not visible will also not be in the legend.
    hide_invisible_plots = Bool(True)

    # If hide_invisible_plots is False, we can still choose to render the names
    # of invisible plots with an alpha.
    invisible_plot_alpha = Float(0.33)

    # The renderer that draws the icons for the legend.
    composite_icon_renderer = Instance(AbstractCompositeIconRenderer)

    # Action that the legend takes when it encounters a plot whose icon it
    # cannot render:
    #
    # * 'skip': skip it altogether and don't render its name
    # * 'blank': render the name but leave the icon blank (color=self.bgcolor)
    # * 'questionmark': render a "question mark" icon
    error_icon = Enum("skip", "blank", "questionmark")

    # Should the legend clip to the bounds it needs, or to its parent?
    clip_to_component = Bool(False)

    # The legend is not resizable (overrides PlotComponent).
    resizable = "hv"

    # An optional title string to show on the legend.
    title = Str('')

    # If True, title is at top, if False then at bottom.
    title_at_top = Bool(True)

    # The legend draws itself as in one pass when its parent is drawing
    # the **draw_layer** (overrides PlotComponent).
    unified_draw = True
    # The legend is drawn on the overlay layer of its parent (overrides
    # PlotComponent).
    draw_layer = "overlay"

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

    # A cached list of Label instances
    _cached_labels = List

    # A cached array of label sizes.
    _cached_label_sizes = ArrayOrNone()

    # A cached list of label names.
    _cached_label_names = CList

    # A list of the visible plots.  Each plot corresponds to the label at
    # the same index in _cached_label_names.  This list does not necessarily
    # correspond to self.plots.value() because it is sorted according to
    # the plot name and it potentially excludes invisible plots.
    _cached_visible_plots = CList

    # A cached array of label positions relative to the legend's origin
    _cached_label_positions = ArrayOrNone()

    def is_in(self, x, y):
        """ overloads from parent class because legend alignment
            and padding does not cooperatate with the basic implementation

            This may just be caused byt a questionable implementation of the
            legend tool, but it works by adjusting the padding. The Component
            class implementation of is_in uses the outer positions which
            includes the padding
        """
        in_x = (x >= self.x) and (x <= self.x + self.width)
        in_y = (y >= self.y) and (y <= self.y + self.height)

        return in_x and in_y

    def overlay(self, component, gc, view_bounds=None, mode="normal"):
        """ Draws this component overlaid on another component.

        Implements AbstractOverlay.
        """
        self.do_layout()
        valign, halign = self.align
        if valign == "u":
            y = component.y2 - self.outer_height
        else:
            y = component.y
        if halign == "r":
            x = component.x2 - self.outer_width
        else:
            x = component.x
        self.outer_position = [x, y]

        if self.clip_to_component:
            c = self.component
            with gc:
                gc.clip_to_rect(c.x, c.y, c.width, c.height)
                PlotComponent._draw(self, gc, view_bounds, mode)
        else:
            PlotComponent._draw(self, gc, view_bounds, mode)

        return

    # The following two methods implement the functionality of the Legend
    # to act as a first-class component instead of merely as an overlay.
    # The make the Legend use the normal PlotComponent render methods when
    # it does not have a .component attribute, so that it can have its own
    # overlays (e.g. a PlotLabel).
    #
    # The core legend rendering method is named _draw_as_overlay() so that
    # it can be called from _draw_plot() when the Legend is not an overlay,
    # and from _draw_overlay() when the Legend is an overlay.

    def _draw_plot(self, gc, view_bounds=None, mode="normal"):
        if self.component is None:
            self._draw_as_overlay(gc, view_bounds, mode)
        return

    def _draw_overlay(self, gc, view_bounds=None, mode="normal"):
        if self.component is not None:
            self._draw_as_overlay(gc, view_bounds, mode)
        else:
            PlotComponent._draw_overlay(self, gc, view_bounds, mode)
        return

    def _draw_as_overlay(self, gc, view_bounds=None, mode="normal"):
        """ Draws the overlay layer of a component.

        Overrides PlotComponent.
        """
        # Determine the position we are going to draw at from our alignment
        # corner and the corresponding outer_padding parameters.  (Position
        # refers to the lower-left corner of our border.)

        # First draw the border, if necesssary.  This sort of duplicates
        # the code in PlotComponent._draw_overlay, which is unfortunate;
        # on the other hand, overlays of overlays seem like a rather obscure
        # feature.

        with gc:
            gc.clip_to_rect(int(self.x), int(self.y), int(self.width),
                            int(self.height))
            edge_space = self.border_width + self.border_padding
            icon_width, icon_height = self.icon_bounds

            icon_x = self.x + edge_space
            text_x = icon_x + icon_width + self.icon_spacing
            y = self.y2 - edge_space

            if self._cached_label_positions is not None:
                if len(self._cached_label_positions) > 0:
                    self._cached_label_positions[:, 0] = icon_x

            for i, label_name in enumerate(self._cached_label_names):
                # Compute the current label's position
                label_height = self._cached_label_sizes[i][1]
                y -= label_height
                self._cached_label_positions[i][1] = y

                # Try to render the icon
                icon_y = y + (label_height - icon_height) / 2
                #plots = self.plots[label_name]
                plots = self._cached_visible_plots[i]
                render_args = (gc, icon_x, icon_y, icon_width, icon_height)

                try:
                    if isinstance(plots, list) or isinstance(plots, tuple):
                        # TODO: How do we determine if a *group* of plots is
                        # visible or not?  For now, just look at the first one
                        # and assume that applies to all of them
                        if not plots[0].visible:
                            # TODO: the get_alpha() method isn't supported on the Mac kiva backend
                            #old_alpha = gc.get_alpha()
                            old_alpha = 1.0
                            gc.set_alpha(self.invisible_plot_alpha)
                        else:
                            old_alpha = None
                        if len(plots) == 1:
                            plots[0]._render_icon(*render_args)
                        else:
                            self.composite_icon_renderer.render_icon(
                                plots, *render_args)
                    elif plots is not None:
                        # Single plot
                        if not plots.visible:
                            #old_alpha = gc.get_alpha()
                            old_alpha = 1.0
                            gc.set_alpha(self.invisible_plot_alpha)
                        else:
                            old_alpha = None
                        plots._render_icon(*render_args)
                    else:
                        old_alpha = None  # Or maybe 1.0?

                    icon_drawn = True
                except:
                    icon_drawn = self._render_error(*render_args)

                if icon_drawn:
                    # Render the text
                    gc.translate_ctm(text_x, y)
                    gc.set_antialias(0)
                    self._cached_labels[i].draw(gc)
                    gc.set_antialias(1)
                    gc.translate_ctm(-text_x, -y)

                    # Advance y to the next label's baseline
                    y -= self.line_spacing
                if old_alpha is not None:
                    gc.set_alpha(old_alpha)

        return

    def _render_error(self, gc, icon_x, icon_y, icon_width, icon_height):
        """ Renders an error icon or performs some other action when a
        plot is unable to render its icon.

        Returns True if something was actually drawn (and hence the legend
        needs to advance the line) or False if nothing was drawn.
        """
        if self.error_icon == "skip":
            return False
        elif self.error_icon == "blank" or self.error_icon == "questionmark":
            with gc:
                gc.set_fill_color(self.bgcolor_)
                gc.rect(icon_x, icon_y, icon_width, icon_height)
                gc.fill_path()
            return True
        else:
            return False

    def get_preferred_size(self):
        """
        Computes the size and position of the legend based on the maximum size of
        the labels, the alignment, and position of the component to overlay.
        """
        # Gather the names of all the labels we will create
        if len(self.plots) == 0:
            return [0, 0]

        plot_names, visible_plots = list(
            sm.map(list, sm.zip(*sorted(self.plots.items()))))
        label_names = self.labels
        if len(label_names) == 0:
            if len(self.plots) > 0:
                label_names = plot_names
            else:
                self._cached_labels = []
                self._cached_label_sizes = []
                self._cached_label_names = []
                self._cached_visible_plots = []
                self.outer_bounds = [0, 0]
                return [0, 0]

        if self.hide_invisible_plots:
            visible_labels = []
            visible_plots = []
            for name in label_names:
                # If the user set self.labels, there might be a bad value,
                # so ensure that each name is actually in the plots dict.
                if name in self.plots:
                    val = self.plots[name]
                    # Rather than checking for a list/TraitListObject/etc., we just check
                    # for the attribute first
                    if hasattr(val, 'visible'):
                        if val.visible:
                            visible_labels.append(name)
                            visible_plots.append(val)
                    else:
                        # If we have a list of renderers, add the name if any of them are
                        # visible
                        for renderer in val:
                            if renderer.visible:
                                visible_labels.append(name)
                                visible_plots.append(val)
                                break
            label_names = visible_labels

        # Create the labels
        labels = [self._create_label(text) for text in label_names]

        # For the legend title
        if self.title_at_top:
            labels.insert(0, self._create_label(self.title))
            label_names.insert(0, 'Legend Label')
            visible_plots.insert(0, None)
        else:
            labels.append(self._create_label(self.title))
            label_names.append(self.title)
            visible_plots.append(None)

        # We need a dummy GC in order to get font metrics
        dummy_gc = font_metrics_provider()
        label_sizes = array(
            [label.get_width_height(dummy_gc) for label in labels])

        if len(label_sizes) > 0:
            max_label_width = max(label_sizes[:, 0])
            total_label_height = sum(
                label_sizes[:, 1]) + (len(label_sizes) - 1) * self.line_spacing
        else:
            max_label_width = 0
            total_label_height = 0

        legend_width = max_label_width + self.icon_spacing + self.icon_bounds[0] \
                        + self.hpadding + 2*self.border_padding
        legend_height = total_label_height + self.vpadding + 2 * self.border_padding

        self._cached_labels = labels
        self._cached_label_sizes = label_sizes
        self._cached_label_positions = zeros_like(label_sizes)
        self._cached_label_names = label_names
        self._cached_visible_plots = visible_plots

        if "h" not in self.resizable:
            legend_width = self.outer_width
        if "v" not in self.resizable:
            legend_height = self.outer_height
        return [legend_width, legend_height]

    def get_label_at(self, x, y):
        """ Returns the label object at (x,y) """
        for i, pos in enumerate(self._cached_label_positions):
            size = self._cached_label_sizes[i]
            corner = pos + size
            if (pos[0] <= x <= corner[0]) and (pos[1] <= y <= corner[1]):
                return self._cached_labels[i]
        else:
            return None

    def _do_layout(self):
        if self.component is not None or len(self._cached_labels) == 0 or \
                self._cached_label_sizes is None or len(self._cached_label_names) == 0:
            width, height = self.get_preferred_size()
            self.outer_bounds = [width, height]
        return

    def _create_label(self, text):
        """ Returns a new Label instance for the given text.  Subclasses can
        override this method to customize the creation of labels.
        """
        return Label(text=text,
                     font=self.font,
                     margin=0,
                     color=self.color_,
                     bgcolor="transparent",
                     border_width=0)

    def _composite_icon_renderer_default(self):
        return CompositeIconRenderer()

    #-- trait handlers --------------------------------------------------------
    def _anytrait_changed(self, name, old, new):
        if name in ("font", "border_padding", "padding", "line_spacing",
                    "icon_bounds", "icon_spacing", "labels", "plots",
                    "plots_items", "labels_items", "border_width", "align",
                    "position", "position_items", "bounds", "bounds_items",
                    "label_at_top"):
            self._layout_needed = True
        if name == "color":
            self.get_preferred_size()
        return

    def _plots_changed(self):
        """ Invalidate the caches.
        """
        self._cached_labels = []
        self._cached_label_sizes = None
        self._cached_label_names = []
        self._cached_visible_plots = []
        self._cached_label_positions = None

    def _title_at_top_changed(self, old, new):
        """ Trait handler for when self.title_at_top changes. """
        if old == True:
            indx = 0
        else:
            indx = -1
        if old != None:
            self._cached_labels.pop(indx)
            self._cached_label_names.pop(indx)
            self._cached_visible_plots.pop(indx)

        # For the legend title
        if self.title_at_top:
            self._cached_labels.insert(0, self._create_label(self.title))
            self._cached_label_names.insert(0, '__legend_label__')
            self._cached_visible_plots.insert(0, None)
        else:
            self._cached_labels.append(self._create_label(self.title))
            self._cached_label_names.append(self.title)
            self._cached_visible_plots.append(None)
Exemplo n.º 15
0
class Button(Component):

    color = ColorTrait("lightblue")

    down_color = ColorTrait("darkblue")

    border_color = ColorTrait("blue")

    label = Str

    label_font = KivaFont("modern 12")

    label_color = ColorTrait("white")

    down_label_color = ColorTrait("white")

    button_state = Enum("up", "down")

    # A reference to the radio group that this button belongs to
    radio_group = Any

    # Default size of the button if no label is present
    bounds=[32,32]

    # Generally, buttons are not resizable
    resizable = ""

    _got_mousedown = Bool(False)

    def perform(self, event):
        """
        Called when the button is depressed.  'event' is the Enable mouse event
        that triggered this call.
        """
        pass

    def _draw_mainlayer(self, gc, view_bounds, mode="default"):
        if self.button_state == "up":
            self.draw_up(gc, view_bounds)
        else:
            self.draw_down(gc, view_bounds)
        return

    def draw_up(self, gc, view_bounds):
        with gc:
            gc.set_fill_color(self.color_)
            gc.set_stroke_color(self.border_color_)
            gc.draw_rect((int(self.x), int(self.y), int(self.width)-1, int(self.height)-1), FILL_STROKE)
            self._draw_label(gc)
        return

    def draw_down(self, gc, view_bounds):
        with gc:
            gc.set_fill_color(self.down_color_)
            gc.set_stroke_color(self.border_color_)
            gc.draw_rect((int(self.x), int(self.y), int(self.width)-1, int(self.height)-1), FILL_STROKE)
            self._draw_label(gc, color=self.down_label_color_)
        return

    def _draw_label(self, gc, color=None):
        if self.label != "":
            gc.set_font(self.label_font)
            x,y,w,h = gc.get_text_extent(self.label)
            if color is None:
                color = self.label_color_
            gc.set_fill_color(color)
            gc.set_stroke_color(color)
            gc.show_text(self.label, (self.x+(self.width-w-x)/2,
                                  self.y+(self.height-h-y)/2))
        return

    def normal_left_down(self, event):
        self.button_state = "down"
        self._got_mousedown = True
        self.request_redraw()
        event.handled = True
        return

    def normal_left_up(self, event):
        self.button_state = "up"
        self._got_mousedown = False
        self.request_redraw()
        self.perform(event)
        event.handled = True
        return
Exemplo n.º 16
0
class TextGrid(Component):
    """
    A 2D grid of string values
    """

    # A 2D array of strings
    string_array = Array

    # The cell size can be set to a tuple (w,h) or to "auto".
    cell_size = Property

    #------------------------------------------------------------------------
    # Appereance traits
    #------------------------------------------------------------------------

    # The font to use for the text of the grid
    font = KivaFont("modern 14")

    # The color of the text
    text_color = black_color_trait

    # The padding around each cell
    cell_padding = Int(5)

    # The thickness of the border between cells
    cell_border_width = Int(1)

    # The color of the border between cells
    cell_border_color = black_color_trait

    # The dash style of the border between cells
    cell_border_style = LineStyle("solid")

    # Text color of highlighted items
    highlight_color = ColorTrait("red")

    # Cell background color of highlighted items
    highlight_bgcolor = ColorTrait("lightgray")

    # A list of tuples of the (i,j) of selected cells
    selected_cells = List

    #------------------------------------------------------------------------
    # Private traits
    #------------------------------------------------------------------------

    # Are our cached extent values still valid?
    _cache_valid = Bool(False)

    # The maximum width and height of all cells, as a tuple (w,h)
    _cached_cell_size = Tuple

    # The maximum (leading, descent) of all the text strings (positive value)
    _text_offset = Array

    # An array NxMx2 of the x,y positions of the lower-left coordinates of
    # each cell
    _cached_cell_coords = Array

    # "auto" or a tuple
    _cell_size = Trait("auto", Any)

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

    def __init__(self, **kwtraits):
        super(Component, self).__init__(**kwtraits)
        self.selected_cells = []
        return

    #------------------------------------------------------------------------
    # AbstractComponent interface
    #------------------------------------------------------------------------

    def _draw_mainlayer(self, gc, view_bounds=None, mode="default"):
        text_color = self.text_color_
        highlight_color = self.highlight_color_
        highlight_bgcolor = self.highlight_bgcolor_
        padding = self.cell_padding
        border_width = self.cell_border_width

        with gc:
            gc.set_stroke_color(text_color)
            gc.set_fill_color(text_color)
            gc.set_font(self.font)
            gc.set_text_position(0, 0)

            width, height = self._get_actual_cell_size()
            numrows, numcols = self.string_array.shape

            # draw selected backgrounds
            # XXX should this be in the background layer?
            for j, row in enumerate(self.string_array):
                for i, text in enumerate(row):
                    if (i, j) in self.selected_cells:
                        gc.set_fill_color(highlight_bgcolor)
                        ll_x, ll_y = self._cached_cell_coords[i, j + 1]
                        # render this a bit big, but covered by border
                        gc.rect(ll_x, ll_y, width + 2 * padding + border_width,
                                height + 2 * padding + border_width)
                        gc.fill_path()
                        gc.set_fill_color(text_color)

            self._draw_grid_lines(gc)

            for j, row in enumerate(self.string_array):
                for i, text in enumerate(row):
                    x,y = self._cached_cell_coords[i,j+1] + self._text_offset + \
                        padding + border_width/2.0

                    if (i, j) in self.selected_cells:
                        gc.set_fill_color(highlight_color)
                        gc.set_stroke_color(highlight_color)
                        gc.set_text_position(x, y)
                        gc.show_text(text)
                        gc.set_stroke_color(text_color)
                        gc.set_fill_color(text_color)
                    else:
                        gc.set_text_position(x, y)
                        gc.show_text(text)

        return

    #------------------------------------------------------------------------
    # Private methods
    #------------------------------------------------------------------------

    def _draw_grid_lines(self, gc):
        gc.set_stroke_color(self.cell_border_color_)
        gc.set_line_dash(self.cell_border_style_)
        gc.set_line_width(self.cell_border_width)

        # Skip the leftmost and bottommost cell coords (since Y axis is reversed,
        # the bottommost coord is the last one)
        x_points = self._cached_cell_coords[:, 0, 0]
        y_points = self._cached_cell_coords[0, :, 1]

        for x in x_points:
            gc.move_to(x, self.y)
            gc.line_to(x, self.y + self.height)
            gc.stroke_path()

        for y in y_points:
            gc.move_to(self.x, y)
            gc.line_to(self.x + self.width, y)
            gc.stroke_path()
        return

    def _compute_cell_sizes(self):
        if not self._cache_valid:
            gc = font_metrics_provider()
            max_w = 0
            max_h = 0
            min_l = 0
            min_d = 0
            for text in self.string_array.ravel():
                gc.set_font(self.font)
                l, d, w, h = gc.get_text_extent(text)
                if -l + w > max_w:
                    max_w = -l + w
                if -d + h > max_h:
                    max_h = -d + h
                if l < min_l:
                    min_l = l
                if d < min_d:
                    min_d = d

            self._cached_cell_size = (max_w, max_h)
            self._text_offset = array([-min_l, -min_d])
            self._cache_valid = True
        return

    def _compute_positions(self):

        if self.string_array is None or len(self.string_array.shape) != 2:
            return

        width, height = self._get_actual_cell_size()
        numrows, numcols = self.string_array.shape

        cell_width = width + 2 * self.cell_padding + self.cell_border_width
        cell_height = height + 2 * self.cell_padding + self.cell_border_width

        x_points = arange(
            numcols + 1) * cell_width + self.cell_border_width / 2.0 + self.x
        y_points = arange(
            numrows + 1) * cell_height + self.cell_border_width / 2.0 + self.y

        tmp = dstack(
            (repeat(x_points[:, newaxis], numrows + 1,
                    axis=1), repeat(y_points[:, newaxis].T,
                                    numcols + 1,
                                    axis=0)))

        # We have to reverse the y-axis (e.g. the 0th row needs to be at the
        # highest y-position).
        self._cached_cell_coords = tmp[:, ::-1]
        return

    def _update_bounds(self):
        if self.string_array is not None and len(self.string_array.shape) == 2:
            rows, cols = self.string_array.shape
            margin = 2 * self.cell_padding + self.cell_border_width
            width, height = self._get_actual_cell_size()
            self.bounds = [
                cols * (width + margin) + self.cell_border_width,
                rows * (height + margin) + self.cell_border_width
            ]

        else:
            self.bounds = [0, 0]

    def _get_actual_cell_size(self):
        if self._cell_size == "auto":
            if not self._cache_valid:
                self._compute_cell_sizes()
            return self._cached_cell_size

        else:
            if not self._cache_valid:
                # actually computing the text offset
                self._compute_cell_sizes()
            return self._cell_size

    #------------------------------------------------------------------------
    # Event handlers
    #------------------------------------------------------------------------

    def normal_left_down(self, event):
        self.selected_cells = [self._get_index_for_xy(event.x, event.y)]
        self.request_redraw()

    def _get_index_for_xy(self, x, y):
        width, height = array(self._get_actual_cell_size()) + 2*self.cell_padding \
                            + self.cell_border_width

        numrows, numcols = self.string_array.shape
        i = int((x - self.padding_left) / width)
        j = numrows - (int((y - self.padding_bottom) / height) + 1)
        shape = self.string_array.shape
        if 0 <= i < shape[1] and 0 <= j < shape[0]:
            return i, j
        else:
            return None

    #------------------------------------------------------------------------
    # Trait events, property setters and getters
    #------------------------------------------------------------------------

    def _string_array_changed(self, old, new):
        if self._cell_size == "auto":
            self._cache_valid = False
            self._compute_cell_sizes()
        self._compute_positions()
        self._update_bounds()

    @on_trait_change('cell_border_width,cell_padding')
    def cell_properties_changed(self):
        self._compute_positions()
        self._update_bounds()

    def _set_cell_size(self, newsize):
        self._cell_size = newsize
        if newsize == "auto":
            self._compute_cell_sizes()
        self._compute_positions()
        self._update_bounds()

    def _get_cell_size(self):
        return self._cell_size
Exemplo n.º 17
0
class TextPlot1D(Base1DPlot):
    """ A plot that positions textual labels in 1D """

    #: text values corresponding to indices
    value = Instance(ArrayDataSource)

    #: The font of the tick labels.
    text_font = KivaFont('modern 10')

    #: The color of the tick labels.
    text_color = black_color_trait

    #: The rotation of the tick labels.
    text_rotate_angle = Float(0)

    #: The margin around the tick labels.
    text_margin = Int(2)

    #: the anchor point of the text (corner is better for 45 degree rotation)
    text_alignment = Enum('edge', 'corner')

    #: alignment of text relative to non-index direction
    alignment = Enum("center", "left", "right", "top", "bottom")

    #: offset of text relative to non-index direction in pixels
    text_offset = Float

    #------------------------------------------------------------------------
    # Private traits
    #------------------------------------------------------------------------

    #: private trait holding position of text relative to non-index direction
    _text_position = Float

    #: flag for whether the cache of Label instances is valid
    _label_cache_valid = Bool(False)

    #: cache of Label instances for faster rendering
    _label_cache = List

    #: cache of bounding boxes of labels
    _label_box_cache = List

    #------------------------------------------------------------------------
    # Private methods
    #------------------------------------------------------------------------

    def _compute_labels(self, gc):
        """Generate the Label instances for the plot. """
        self._label_cache = [
            Label(text=text,
                  font=self.text_font,
                  color=self.text_color,
                  rotate_angle=self.text_rotate_angle,
                  margin=self.text_margin) for text in self.value.get_data()
        ]
        self._label_box_cache = [
            array(label.get_bounding_box(gc), float)
            for label in self._label_cache
        ]
        self._label_cache_valid = True

    def _draw_plot(self, gc, view_bounds=None, mode="normal"):
        """ Draw the text at the specified index values """

        if len(self.index.get_data()) == 0:
            return
        if not self._label_cache_valid:
            self._compute_labels(gc)

        coord = self._compute_screen_coord()
        pts = empty(shape=(len(coord), 2))

        if self.orientation == 'v':
            pts[:, 1] = coord
            pts[:, 0] = self._text_position
        else:
            pts[:, 0] = coord
            pts[:, 1] = self._text_position

        self._render(gc, pts, self._label_cache)

    def _render(self, gc, pts, labels):
        with gc:
            gc.clip_to_rect(self.x, self.y, self.width, self.height)
            for pt, label in izip(pts, labels):
                with gc:
                    gc.translate_ctm(*pt)
                    label.draw(gc)

    def _get_text_position(self):
        """ Compute the text label position in the non-index direction """
        x, y = self.position
        w, h = self.bounds

        if self.orientation == 'v':
            y, h = x, w

        if self.alignment == 'center':
            position = y + h / 2.0
        elif self.alignment in ['left', 'bottom']:
            position = y
        elif self.alignment in ['right', 'top']:
            position = y + h

        position += self.text_offset
        return position

    #------------------------------------------------------------------------
    # Trait handlers
    #------------------------------------------------------------------------

    def __text_position_default(self):
        return self._get_text_position()

    #------------------------------------------------------------------------
    # Trait events
    #------------------------------------------------------------------------

    @on_trait_change("index.data_changed")
    def _invalidate(self):
        self._cache_valid = False
        self._screen_cache_valid = False
        self._label_cache_valid = False

    @on_trait_change("value.data_changed")
    def _invalidate_labels(self):
        self._label_cache_valid = False

    def _bounds_changed(self, old, new):
        super(TextPlot1D, self)._bounds_changed(old, new)
        self._text_position = self._get_text_position()

    def _bounds_items_changed(self, event):
        super(TextPlot1D, self)._bounds_items_changed(event)
        self._text_position = self._get_text_position()

    def _orientation_changed(self):
        super(TextPlot1D, self)._orientation_changed()
        self._text_position = self._get_text_position()

    def _direction_changed(self):
        super(TextPlot1D, self)._direction_changed()
        self._text_position = self._get_text_position()

    def _alignment_changed(self):
        self._text_position = self._get_text_position()
Exemplo n.º 18
0
class TextBoxOverlay(AbstractOverlay):
    """ Draws a box with a text in it
    """
    #### Configuration traits ##################################################
    # The text to display in the box.
    text = Str

    # The font to use for the text.
    font = KivaFont("swiss 12")

    # The background color for the box (overrides AbstractOverlay).
    bgcolor = ColorTrait("transparent")

    # The alpha value to apply to **bgcolor**
    alpha = Trait(1.0, None, Float)

    # The color of the outside box.
    border_color = ColorTrait("dodgerblue")

    # The color of the text in the tooltip
    text_color = black_color_trait

    # The thickness of box border.
    border_size = Int(1)

    # Number of pixels of padding around the text within the box.
    padding = Int(5)

    # Alignment of the text in the box:
    #
    # * "ur": upper right
    # * "ul": upper left
    # * "ll": lower left
    # * "lr": lower right
    align = Enum("ur", "ul", "ll", "lr")
    # This allows subclasses to specify an alternate position for the root
    # of the text box.	Must be a sequence of length 2.
    alternate_position = Any

    #### Public 'AbstractOverlay' interface ####################################
    def overlay(self, component, gc, view_bounds=None, mode="normal"):
        """ Draws the box overlaid on another component.

        Overrides AbstractOverlay.
        """
        if not self.visible:
            return

        # draw the label on a transparent box. This allows us to draw
        # different shapes and put the text inside it without the label
        # filling a rectangle on top of it
        label = Label(text=self.text,
                      font=self.font,
                      bgcolor="transparent",
                      color=self.text_color,
                      margin=5)
        width, height = label.get_width_height(gc)
        valign, halign = self.align
        if self.alternate_position:
            x, y = self.alternate_position
            if valign == "u":
                y += self.padding
            else:
                y -= self.padding + height
            if halign == "r":
                x += self.padding
            else:
                x -= self.padding + width
        else:
            if valign == "u":
                y = component.y2 - self.padding - height
            else:
                y = component.y + self.padding
            if halign == "r":
                x = component.x2 - self.padding - width
            else:
                x = component.x + self.padding
        # attempt to get the box entirely within the component
        if x + width > component.width:
            x = max(0, component.width - width)
        if y + height > component.height:
            y = max(0, component.height - height)
        elif y < 0:
            y = 0
        # apply the alpha channel
        color = self.bgcolor_
        if self.bgcolor != "transparent":
            if self.alpha:
                color = list(self.bgcolor_)
                if len(color) == 4:
                    color[3] = self.alpha
                else:
                    color += [self.alpha]
        gc.save_state()
        try:
            gc.translate_ctm(x, y)

            gc.set_line_width(self.border_size)
            gc.set_stroke_color(self.border_color_)
            gc.set_fill_color(color)

            # draw a rounded rectangle
            x = y = 0
            end_radius = 8.0
            gc.begin_path()
            gc.move_to(x + end_radius, y)
            gc.arc_to(x + width, y, x + width, y + end_radius, end_radius)
            gc.arc_to(x + width, y + height, x + width - end_radius,
                      y + height, end_radius)
            gc.arc_to(x, y + height, x, y, end_radius)
            gc.arc_to(x, y, x + width + end_radius, y, end_radius)
            gc.draw_path()

            label.draw(gc)
        finally:
            gc.restore_state()
Exemplo n.º 19
0
class TextBoxOverlay(AbstractOverlay):
    """ Draws a box with text in it.
    """

    #### Configuration traits #################################################

    #: The text to display in the box.
    text = Str

    #: The font to use for the text.
    font = KivaFont("modern 12")

    #: The background color for the box (overrides AbstractOverlay).
    bgcolor = ColorTrait("transparent")

    #: The alpha value to apply to **bgcolor**
    alpha = Trait(1.0, None, Float)

    #: The color of the outside box.
    border_color = ColorTrait("dodgerblue")

    #: The color of the text.
    text_color = ColorTrait("black")

    #: The thickness of box border.
    border_size = Int(1)

    #: The border visibility. Defaults to true to duplicate previous behavior.
    border_visible = Bool(True)

    #: Number of pixels of padding around the text within the box.
    padding = Int(5)

    #: The maximum width of the displayed text. This affects the width of the
    #: text only, not the text box, which includes margins around the text and
    #: `padding`.
    #: A `max_text_width` of 0.0 means that the width will not be restricted.
    max_text_width = Float(0.0)

    #: Alignment of the text in the box:
    #:
    #: * "ur": upper right
    #: * "ul": upper left
    #: * "ll": lower left
    #: * "lr": lower right
    align = Enum("ur", "ul", "ll", "lr")

    #: This allows subclasses to specify an alternate position for the root
    #: of the text box.  Must be a sequence of length 2.
    alternate_position = Any

    #### Public 'AbstractOverlay' interface ###################################

    def overlay(self, component, gc, view_bounds=None, mode="normal"):
        """ Draws the box overlaid on another component.

        Overrides AbstractOverlay.
        """

        if not self.visible:
            return

        # draw the label on a transparent box. This allows us to draw
        # different shapes and put the text inside it without the label
        # filling a rectangle on top of it
        label = Label(text=self.text, font=self.font, bgcolor="transparent",
                      color=self.text_color, max_width=self.max_text_width,
                      margin=5)
        width, height = label.get_width_height(gc)

        valign, halign = self.align

        if self.alternate_position:
            x, y = self.alternate_position
            if valign == "u":
                y += self.padding
            else:
                y -= self.padding + height

            if halign == "r":
                x += self.padding
            else:
                x -= self.padding + width
        else:
            if valign == "u":
                y = component.y2 - self.padding - height
            else:
                y = component.y + self.padding

            if halign == "r":
                x = component.x2 - self.padding - width
            else:
                x = component.x + self.padding

        # attempt to get the box entirely within the component
        x_min, y_min, x_max, y_max = (component.x,
                                      component.y,
                                      component.x + component.width,
                                      component.y + component.height)
        if x + width > x_max:
            x = max(x_min, x_max - width)
        if y + height > y_max:
            y = max(y_min, y_max - height)
        elif y < y_min:
            y = y_min

        # apply the alpha channel
        color = self.bgcolor_
        if self.bgcolor != "transparent":
            if self.alpha:
                color = list(self.bgcolor_)
                if len(color) == 4:
                    color[3] = self.alpha
                else:
                    color += [self.alpha]

        with gc:
            gc.translate_ctm(x, y)

            gc.set_line_width(self.border_size)
            gc.set_stroke_color(self.border_color_)
            gc.set_fill_color(color)

            if self.border_visible:
                # draw a rounded rectangle.
                x = y = 0
                end_radius = 8.0
                gc.begin_path()
                gc.move_to(x + end_radius, y)
                gc.arc_to(x + width, y,
                          x + width,
                          y + end_radius, end_radius)
                gc.arc_to(x + width,
                          y + height,
                          x + width - end_radius,
                          y + height, end_radius)
                gc.arc_to(x, y + height,
                          x, y,
                          end_radius)
                gc.arc_to(x, y,
                          x + width + end_radius,
                          y, end_radius)
                gc.draw_path()

            label.draw(gc)
Exemplo n.º 20
0
class ToolTip(AbstractOverlay):
    """ An overlay that is a toolip.
    """
    # The font to render the tooltip.
    font = KivaFont('modern 10')

    # The color of the text in the tooltip
    text_color = black_color_trait

    # The ammount of space between the border and the text.
    border_padding = Int(4)

    # The number of pixels between lines.
    line_spacing = Int(4)

    # List of text strings to put in the tooltip.
    lines = List

    # Angle to rotate (counterclockwise) in degrees. NB this will *only*
    # currently affect text, so probably only useful if borders and background
    # are disabled
    rotate_angle = Float(0.0)

    # Should the tooltip automatically reposition itself to remain visible
    # and unclipped on its overlaid component?
    auto_adjust = Bool(True)

    # The tooltip is a fixed size. (Overrides PlotComponent.)
    resizable = ""

    # Use a visible border. (Overrides Enable Component.)
    border_visible = True

    # Use a white background color (overrides AbstractOverlay).
    bgcolor = white_color_trait

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

    _font_metrics_provider = Any()

    _text_props_valid = Bool(False)

    _max_line_width = Float(0.0)

    _total_line_height = Float(0.0)

    def draw(self, gc, view_bounds=None, mode='normal'):
        """ Draws the plot component.

        Overrides PlotComponent.
        """
        self.overlay(self, gc, view_bounds=view_bounds, mode='normal')
        return

    def overlay(self, component, gc, view_bounds=None, mode='normal'):
        """ Draws the tooltip overlaid on another component.

        Overrides AbstractOverlay.
        """
        self.do_layout()
        PlotComponent._draw(self, gc, view_bounds, mode)
        return

    def _draw_overlay(self, gc, view_bounds=None, mode='normal'):
        """ Draws the overlay layer of a component.

        Overrides PlotComponent.
        """
        with gc:
            edge_space = self.border_width + self.border_padding
            gc.translate_ctm(self.x + edge_space, self.y)
            y = self.height - edge_space
            for i, label in enumerate(self._cached_labels):
                label_height = self._cached_line_sizes[i][1]
                y -= label_height
                gc.translate_ctm(0, y)
                label.draw(gc)
                gc.translate_ctm(0, -y)
                y -= self.line_spacing
        return

    def _do_layout(self):
        """Computes the size of the tooltip, and creates the label objects
        for each line.

        Overrides PlotComponent.
        """
        if not self._text_props_valid:
            self._recompute_text()

        outer_bounds = [
            self._max_line_width + 2 * self.border_padding + self.hpadding,
            self._total_line_height + 2 * self.border_padding + self.vpadding
        ]

        self.outer_bounds = outer_bounds

        if self.auto_adjust and self.component is not None:
            new_pos = list(self.outer_position)
            for dimindex in (0, 1):
                pos = self.position[dimindex]
                extent = outer_bounds[dimindex]
                c_min = self.component.position[dimindex]
                c_max = c_min + self.component.bounds[dimindex]
                # Is the tooltip just too wide/tall?
                if extent > (c_max - c_min):
                    new_pos[dimindex] = c_min
                # Does it extend over the c_max edge?  (right/top)
                elif pos + extent > c_max:
                    new_pos[dimindex] = c_max - extent

                # Does it extend over the c_min edge? This is not an elif so
                # that we can fix the situation where the c_max edge adjustment
                # above pushes the position negative.
                if new_pos[dimindex] < c_min:
                    new_pos[dimindex] = c_min

            self.outer_position = new_pos

        self._layout_needed = False

    def _recompute_text(self):
        labels = [
            Label(text=line,
                  font=self.font,
                  margin=0,
                  bgcolor='transparent',
                  border_width=0,
                  color=self.text_color,
                  rotate_angle=self.rotate_angle) for line in self.lines
        ]
        dummy_gc = self._font_metrics_provider
        line_sizes = array(
            [label.get_width_height(dummy_gc) for label in labels])
        self._cached_labels = labels
        self._cached_line_sizes = line_sizes
        self._max_line_width = max(line_sizes[:, 0])
        self._total_line_height = sum(line_sizes[:,1]) + \
                                  len(line_sizes-1)*self.line_spacing
        self._layout_needed = True
        return

    def __font_metrics_provider_default(self):
        return font_metrics_provider()

    @on_trait_change("font,text_color,lines,lines_items")
    def _invalidate_text_props(self):
        self._text_props_valid = False
        self._layout_needed = True

    @on_trait_change("border_padding,line_spacing,lines,lines_items,padding")
    def _invalidate_layout(self):
        self._layout_needed = True
        self.request_redraw()
Exemplo n.º 21
0
class Label(Component):
    """ A text label """

    # The label text.  Carriage returns (\n) are always connverted into
    # line breaks.
    text = Str

    # The angle of rotation of the label.  Only multiples of 90 are supported.
    rotate_angle = Float(0)

    # The color of the label text.
    color = black_color_trait

    # The background color of the label.
    bgcolor = transparent_color_trait

    # The width of the label border. If it is 0, then it is not shown.
    border_width = Int(0)

    # The color of the border.
    border_color = black_color_trait

    # The font of the label text.
    font = KivaFont("modern 10")

    # Number of pixels of margin around the label, for both X and Y dimensions.
    margin = Int(2)

    # Number of pixels of spacing between lines of text.
    line_spacing = Int(5)

    # The horizontal placement of text within the bounds of the label
    hjustify = Enum("left", "center", "right")

    # The vertical placement of text within the bounds of the label
    vjustify = Enum("bottom", "center", "top")

    # By default, labels are not resizable
    resizable = ""

    #------------------------------------------------------------------------
    # Private traits
    #------------------------------------------------------------------------

    _bounding_box = List()
    _position_cache_valid = Bool(False)

    def __init__(self, text="", **kwtraits):
        if 'text' not in kwtraits:
            kwtraits['text'] = text
        HasTraits.__init__(self, **kwtraits)
        self._bounding_box = [0, 0]
        return

    def _calc_line_positions(self, gc):
        if not self._position_cache_valid:
            with gc:
                gc.set_font(self.font)
                # The bottommost line starts at postion (0,0).
                x_pos = []
                y_pos = []
                self._bounding_box = [0, 0]
                margin = self.margin
                prev_y_pos = margin
                prev_y_height = -self.line_spacing
                max_width = 0
                for line in self.text.split("\n")[::-1]:
                    if line != "":
                        (width, height, descent,
                         leading) = gc.get_full_text_extent(line)
                        if width > max_width:
                            max_width = width
                        new_y_pos = prev_y_pos + prev_y_height - descent + self.line_spacing
                    else:
                        # For blank lines, we use the height of the previous line, if there
                        # is one.  The width is 0.
                        leading = 0
                        if prev_y_height != -self.line_spacing:
                            new_y_pos = prev_y_pos + prev_y_height + self.line_spacing
                            height = prev_y_height
                        else:
                            new_y_pos = prev_y_pos
                            height = 0
                    x_pos.append(-leading + margin)
                    y_pos.append(new_y_pos)
                    prev_y_pos = new_y_pos
                    prev_y_height = height

            width = max_width + 2 * margin + 2 * self.border_width
            height = prev_y_pos + prev_y_height + margin + 2 * self.border_width
            self._bounding_box = [width, height]

            if self.hjustify == "left":
                x_pos = x_pos[::-1]
            else:
                x_pos = asarray(x_pos[::-1], dtype=float)
                if self.hjustify == "center":
                    x_pos += (self.width - width) / 2.0
                elif self.hjustify == "right":
                    x_pos += self.width - width
            self._line_xpos = x_pos

            if self.vjustify == "bottom":
                y_pos = y_pos[::-1]
            else:
                y_pos = asarray(y_pos[::-1], dtype=float)
                if self.vjustify == "center":
                    y_pos += (self.height - height) / 2.0
                elif self.vjustify == "top":
                    y_pos += self.height - height
            self._line_ypos = y_pos

            self._position_cache_valid = True
        return

    def get_width_height(self, gc):
        """ Returns the width and height of the label, in the rotated frame of
        reference.
        """
        self._calc_line_positions(gc)
        width, height = self._bounding_box
        return width, height

    def get_bounding_box(self, gc):
        """ Returns a rectangular bounding box for the Label as (width,height).
        """
        # FIXME: Need to deal with non 90 deg rotations
        width, height = self.get_width_height(gc)
        if self.rotate_angle in (90.0, 270.0):
            return (height, width)
        elif self.rotate_angle in (0.0, 180.0):
            return (width, height)
        else:
            raise NotImplementedError

    def get_bounding_poly(self, gc):
        """
        Returns a list [(x0,y0), (x1,y1),...] of tuples representing a polygon
        that bounds the label.
        """
        raise NotImplementedError

    def _draw_mainlayer(self, gc, view_bounds=None, mode="normal"):
        """ Draws the label.

        This method assumes the graphics context has been translated to the
        correct position such that the origin is at the lower left-hand corner
        of this text label's box.
        """
        # For this version we're not supporting rotated text.
        # temp modified for only one line

        self._calc_line_positions(gc)
        with gc:
            gc.translate_ctm(*self.position)

            # Draw border and fill background
            width, height = self._bounding_box
            if self.bgcolor != "transparent":
                gc.set_fill_color(self.bgcolor_)
                gc.draw_rect((0, 0, width, height), FILL)
            if self.border_width > 0:
                gc.set_stroke_color(self.border_color_)
                gc.set_line_width(self.border_width)
                border_offset = (self.border_width - 1) / 2.0
                gc.draw_rect(
                    (border_offset, border_offset, width - 2 * border_offset,
                     height - 2 * border_offset), STROKE)

            gc.set_fill_color(self.color_)
            gc.set_stroke_color(self.color_)
            gc.set_font(self.font)
            if self.font.size <= 8.0:
                gc.set_antialias(0)
            else:
                gc.set_antialias(1)

            gc.rotate_ctm(pi / 180.0 * self.rotate_angle)

            #margin = self.margin
            lines = self.text.split("\n")
            gc.translate_ctm(self.border_width, self.border_width)
            width, height = self.get_width_height(gc)

            for i, line in enumerate(lines):
                if line == "":
                    continue

                if self.rotate_angle == 90. or self.rotate_angle == 270.:
                    x_offset = round(self._line_ypos[i])
                    # this should really be "... - height/2" but
                    # that looks wrong
                    y_offset = round(self._line_xpos[i] - height)
                else:
                    x_offset = round(self._line_xpos[i])
                    y_offset = round(self._line_ypos[i])
                gc.set_text_position(0, 0)
                gc.translate_ctm(x_offset, y_offset)

                gc.show_text(line)
                gc.translate_ctm(-x_offset, -y_offset)

        return

    def _font_changed(self):
        self._position_cache_valid = False

    def _margin_changed(self):
        self._position_cache_valid = False

    def _text_changed(self):
        self._position_cache_valid = False

    def _rotate_angle_changed(self):
        self._position_cache_valid = False
Exemplo n.º 22
0
class TextPlot(BaseXYPlot):
    """ A plot that positions textual labels in 2D """

    #: text values corresponding to indices
    text = Instance(ArrayDataSource)

    #: The font of the tick labels.
    text_font = KivaFont('modern 10')

    #: The color of the tick labels.
    text_color = black_color_trait

    #: The rotation of the tick labels.
    text_rotate_angle = Float(0)

    #: The margin around the label.
    text_margin = Int(2)

    #: horizontal position of text relative to target point
    h_position = Enum("center", "left", "right")

    #: vertical position of text relative to target point
    v_position = Enum("center", "top", "bottom")

    #: offset of text relative to non-index direction in pixels
    text_offset = Tuple(Float, Float)

    #------------------------------------------------------------------------
    # Private traits
    #------------------------------------------------------------------------

    #: flag for whether the cache of Label instances is valid
    _label_cache_valid = Bool(False)

    #: cache of Label instances for faster rendering
    _label_cache = List

    #: cache of bounding boxes of labels
    _label_box_cache = List

    #------------------------------------------------------------------------
    # Private methods
    #------------------------------------------------------------------------

    def _compute_labels(self, gc):
        """Generate the Label instances for the plot. """
        self._label_cache = [
            Label(
                text=text,
                font=self.text_font,
                color=self.text_color,
                rotate_angle=self.text_rotate_angle,
                margin=self.text_margin
            ) for text in self.text.get_data()
        ]
        self._label_box_cache = [
            array(label.get_bounding_box(gc), float)
            for label in self._label_cache
        ]
        self._label_cache_valid = True

    def _gather_points(self):
        """ Abstract method to collect data points that are within the range of
        the plot, and cache them.
        """
        if self._cache_valid:
            return

        if not self.index or not self.value:
            return

        index, index_mask = self.index.get_data_mask()
        value, value_mask = self.value.get_data_mask()

        if len(index) == 0 or len(value) == 0 or len(index) != len(value):
            self._cached_data_pts = []
            self._cached_point_mask = []
            self._cache_valid = True
            return

        index_range_mask = self.index_mapper.range.mask_data(index)
        value_range_mask = self.value_mapper.range.mask_data(value)

        nan_mask = (
            isfinite(index) & index_mask & isfinite(value) & value_mask
        )
        point_mask = nan_mask & index_range_mask & value_range_mask

        if not self._cache_valid:
            if not point_mask.all():
                points = column_stack([index[point_mask], value[point_mask]])
            else:
                points = column_stack([index, value])
            self._cached_data_pts = points
            self._cached_point_mask = point_mask
            self._cache_valid = True

    def _render(self, gc, pts):
        if not self._label_cache_valid:
            self._compute_labels(gc)

        labels = [
            label
            for label, mask in zip(self._label_cache, self._cached_point_mask)
            if mask
        ]
        boxes = [
            label
            for label, mask in
            zip(self._label_box_cache, self._cached_point_mask) if mask
        ]
        offset = empty((2, ), float)

        with gc:
            gc.clip_to_rect(self.x, self.y, self.width, self.height)
            for pt, label, box in sm.zip(pts, labels, boxes):
                with gc:
                    if self.h_position == "center":
                        offset[0] = -box[0] / 2 + self.text_offset[0]
                    elif self.h_position == "right":
                        offset[0] = self.text_offset[0]
                    elif self.h_position == "left":
                        offset[0] = -box[0] / 2 + self.text_offset[0]
                    if self.v_position == "center":
                        offset[1] = -box[1] / 2 + self.text_offset[1]
                    elif self.v_position == "top":
                        offset[1] = self.text_offset[1]
                    elif self.v_position == "bottom":
                        offset[1] = -box[1] / 2 - self.text_offset[1]

                    pt += offset
                    gc.translate_ctm(*pt)

                    label.draw(gc)

    #------------------------------------------------------------------------
    # Trait events
    #------------------------------------------------------------------------

    @on_trait_change("index.data_changed")
    def _invalidate(self):
        self._cache_valid = False
        self._screen_cache_valid = False
        self._label_cache_valid = False

    @on_trait_change("value.data_changed")
    def _invalidate_labels(self):
        self._label_cache_valid = False