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)
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)
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)
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')
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)
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
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)
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
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
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()
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()
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
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))
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)
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
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
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()
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()
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)
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()
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
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