17
18
# Specifies the position of a label relative to its target. This can
18
# be one of the text strings indicated, or a tuple or list of floats representing
19
# the (x_offset, y_offset) in screen space of the label's lower left corner.
19
# be one of the text strings indicated, or a tuple or list of floats
20
# representing the (x_offset, y_offset) in screen space of the label's
20
22
LabelPositionTrait = Trait("top right",
21
23
Enum("bottom", "left", "right", "top",
22
"top right", "top left", "bottom left", "bottom right"),
24
"top right", "top left",
25
"bottom left", "bottom right"),
70
73
unit_vec /= norm(unit_vec)
72
75
if unit_vec[0] == 0:
73
perp_vec = array((0.3 * arrowhead_size,0))
76
perp_vec = array((0.3 * arrowhead_size, 0))
74
77
elif unit_vec[1] == 0:
75
perp_vec = array((0,0.3 * arrowhead_size))
78
perp_vec = array((0, 0.3 * arrowhead_size))
77
slope = unit_vec[1]/unit_vec[0]
80
slope = unit_vec[1] / unit_vec[0]
81
perp_slope = -1 / slope
79
82
perp_vec = array((1.0, perp_slope))
80
83
perp_vec *= 0.3 * arrowhead_size / norm(perp_vec)
82
85
pt1 = pt1 + offset1 * unit_vec
83
86
pt2 = pt2 - offset2 * unit_vec
85
arrowhead_l = pt2 - (arrowhead_size*unit_vec + perp_vec)
86
arrowhead_r = pt2 - (arrowhead_size*unit_vec - perp_vec)
88
arrowhead_l = pt2 - (arrowhead_size * unit_vec + perp_vec)
89
arrowhead_r = pt2 - (arrowhead_size * unit_vec - perp_vec)
87
90
arrow = (pt1, pt2, arrowhead_l, arrowhead_r)
89
92
pt1, pt2, arrowhead_l, arrowhead_r = arrow
113
def find_region(px, py, x, y, x2, y2):
114
"""Classify the location of the point (px, py) relative to a rectangle.
116
(x, y) and (x2, y2) are the lower-left and upper-right corners of the
117
rectangle, respectively. (px, py) is classified as "left", "right",
118
"top", "bottom" or "inside", according to the following diagram:
123
left | inside | right
145
else: # x <= px <= x2
110
155
class DataLabel(ToolTip):
111
""" A label on a point in data space, optionally with an arrow to the point.
156
""" A label on a point in data space.
158
Optionally, an arrow is drawn to the point.
114
161
# The symbol to use if **marker** is set to "custom". This attribute must
140
187
# The center x position (average of x and x2)
141
188
xmid = Property(Float, depends_on=['x', 'x2'])
143
190
# The center y position (average of y and y2)
144
191
ymid = Property(Float, depends_on=['y', 'y2'])
193
# 'box' is a simple rectangular box, with an arrow that is a single line
194
# with an arrowhead at the data point.
195
# 'bubble' can be given rounded corners (by setting `corner_radius`), and
196
# the 'arrow' is a thin triangular wedge with its point at the data point.
197
# When label_style is 'bubble', the following traits are ignored:
198
# arrow_size, arrow_color, arrow_root, and arrow_max_length.
199
label_style = Enum('box', 'bubble')
146
201
#----------------------------------------------------------------------
148
203
#----------------------------------------------------------------------
182
238
arrow_color = ColorTrait("black")
184
240
# The position of the base of the arrow on the label. If this
185
# is 'auto', then the label uses **label_position**. Otherwise, it treats
186
# the label as if it were at the label position indicated by this attribute.
241
# is 'auto', then the label uses **label_position**. Otherwise, it
242
# treats the label as if it were at the label position indicated by
187
244
arrow_root = Trait("auto", "auto", "top left", "top right", "bottom left",
188
245
"bottom right", "top center", "bottom center",
189
246
"left center", "right center")
309
if self.label_style == 'box':
310
self._render_box(component, gc, view_bounds=view_bounds,
313
self._render_bubble(component, gc, view_bounds=view_bounds,
317
if self.marker_visible:
318
render_markers(gc, [self._screen_coords],
319
self.marker, self.marker_size,
320
self.marker_color_, self.marker_line_width,
321
self.marker_line_color_, self.custom_symbol)
323
if self.clip_to_plot:
326
def _render_box(self, component, gc, view_bounds=None, mode='normal'):
246
327
# draw the arrow if necessary
247
328
if self.arrow_visible:
248
329
if self._cached_arrow is None:
253
334
arrow_root = self.label_position
255
336
arrow_root = self.arrow_root
256
ox, oy = self._root_positions.get(
257
self._position_root_map.get(arrow_root, "DUMMY"),
258
(self.x+self.width/2, self.y+self.height/2)
337
pos = self._position_root_map.get(arrow_root, "DUMMY")
338
ox, oy = self._root_positions.get(pos,
339
(self.x + self.width / 2,
340
self.y + self.height / 2))
261
342
if type(ox) == str:
262
343
ox = getattr(self, ox)
263
344
oy = getattr(self, oy)
264
self._cached_arrow = draw_arrow(gc, (ox, oy), self._screen_coords,
266
arrowhead_size=self.arrow_size,
268
offset2=self.marker_size+3,
269
minlen=self.arrow_min_length,
270
maxlen=self.arrow_max_length)
345
self._cached_arrow = draw_arrow(gc, (ox, oy),
348
arrowhead_size=self.arrow_size,
350
offset2=self.marker_size + 3,
351
minlen=self.arrow_min_length,
352
maxlen=self.arrow_max_length)
272
354
draw_arrow(gc, None, None, self.arrow_color_,
273
355
arrow=self._cached_arrow,
277
359
# layout and render the label itself
278
360
ToolTip.overlay(self, component, gc, view_bounds, mode)
281
if self.marker_visible:
282
render_markers(gc, [self._screen_coords], self.marker, self.marker_size,
283
self.marker_color_, self.marker_line_width,
284
self.marker_line_color_, self.custom_symbol)
286
if self.clip_to_plot:
362
def _render_bubble(self, component, gc, view_bounds=None, mode='normal'):
363
""" Render the bubble label in the graphics context. """
364
# (px, py) is the data point in screen space.
365
px, py = self._screen_coords
367
# (x, y) is the lower left corner of the label.
370
# (x2, y2) is the upper right corner of the label.
373
# r is the corner radius.
374
r = self.corner_radius
376
if self.arrow_visible:
377
# FIXME: Make 'gap_width' a configurable trait (and give it a
380
gap_width = min(max_gap_width,
383
region = find_region(px, py, x, y, x2, y2)
385
# Figure out where the "arrow" connects to the "bubble".
386
if region == 'left' or region == 'right':
387
gap_start = py - gap_width / 2
388
if gap_start < y + r:
390
elif gap_start > y2 - r - gap_width:
391
gap_start = y2 - r - gap_width
392
by = gap_start + 0.5 * gap_width
398
gap_start = px - gap_width / 2
399
if gap_start < x + r:
401
elif gap_start > x2 - r - gap_width:
402
gap_start = x2 - r - gap_width
403
bx = gap_start + 0.5 * gap_width
409
arrow_len = sqrt((px - bx) ** 2 + (py - by) ** 2)
410
arrow_visible = (self.arrow_visible and
411
(arrow_len >= self.arrow_min_length))
414
if self.border_visible:
415
gc.set_line_width(self.border_width)
416
gc.set_stroke_color(self.border_color_)
419
gc.set_stroke_color((0, 0, 0, 0))
420
gc.set_fill_color(self.bgcolor_)
422
# Start at the lower left, on the left edge where the curved
423
# part of the box ends.
426
# Draw the left side and the upper left curved corner.
427
if arrow_visible and region == 'left':
428
gc.line_to(x, gap_start)
430
gc.line_to(x, gap_start + gap_width)
431
gc.arc_to(x, y2, x + r, y2, r)
433
# Draw the top and the upper right curved corner.
434
if arrow_visible and region == 'top':
435
gc.line_to(gap_start, y2)
437
gc.line_to(gap_start + gap_width, y2)
438
gc.arc_to(x2, y2, x2, y2 - r, r)
440
# Draw the right side and the lower right curved corner.
441
if arrow_visible and region == 'right':
442
gc.line_to(x2, gap_start + gap_width)
444
gc.line_to(x2, gap_start)
445
gc.arc_to(x2, y, x2 - r, y, r)
447
# Draw the bottom and the lower left curved corner.
448
if arrow_visible and region == 'bottom':
449
gc.line_to(gap_start + gap_width, y)
451
gc.line_to(gap_start, y)
452
gc.arc_to(x, y, x, y + r, r)
454
# Finish the "bubble".
457
self._draw_overlay(gc)
289
459
def _do_layout(self, size=None):
290
460
"""Computes the size and position of the label and arrow.
318
488
self.outer_y = sy
319
489
if "center" in orientation:
320
490
if " " not in orientation:
321
self.x = sx - (self.width/2)
322
self.y = sy - (self.height/2)
491
self.x = sx - (self.width / 2)
492
self.y = sy - (self.height / 2)
324
self.x = sx - (self.outer_width/2) - 1
325
self.y = sy - (self.outer_height/2) - 1
494
self.x = sx - (self.outer_width / 2) - 1
495
self.y = sy - (self.outer_height / 2) - 1
327
497
self.x = sx + self.label_position[0]
328
498
self.y = sy + self.label_position[1]
355
526
for comp, attach in ((old, False), (new, True)):
356
527
if comp is not None:
357
528
if hasattr(comp, 'index_mapper'):
358
self._modify_mapper_listeners(comp.index_mapper, attach=attach)
529
self._modify_mapper_listeners(comp.index_mapper,
359
531
if hasattr(comp, 'value_mapper'):
360
self._modify_mapper_listeners(comp.value_mapper, attach=attach)
532
self._modify_mapper_listeners(comp.value_mapper,
363
536
def _modify_mapper_listeners(self, mapper, attach=True):
364
537
if mapper is not None:
365
mapper.on_trait_change(self._handle_mapper, 'updated', remove=not attach)
538
mapper.on_trait_change(self._handle_mapper, 'updated',
368
542
def _handle_mapper(self):
369
# This gets fired whenever a mapper on our plot fires its 'updated' event.
543
# This gets fired whenever a mapper on our plot fires its
370
545
self._layout_needed = True
372
@on_trait_change("arrow_size,arrow_root,arrow_min_length,arrow_max_length")
547
@on_trait_change("arrow_size,arrow_root,arrow_min_length," +
373
549
def _invalidate_arrow(self):
374
550
self._cached_arrow = None
375
551
self._layout_needed = True
377
@on_trait_change("label_position,position,position_items,bounds,bounds_items")
553
@on_trait_change("label_position,position,position_items,bounds," +
378
555
def _invalidate_layout(self):
379
556
self._layout_needed = True
382
558
def _get_xmid(self):
383
559
return 0.5 * (self.x + self.x2)
385
561
def _get_ymid(self):
386
562
return 0.5 * (self.y + self.y2)