~ubuntu-branches/ubuntu/utopic/python-chaco/utopic

« back to all changes in this revision

Viewing changes to chaco/data_label.py

  • Committer: Package Import Robot
  • Author(s): Andrew Starr-Bochicchio
  • Date: 2014-06-01 17:04:08 UTC
  • mfrom: (7.2.5 sid)
  • Revision ID: package-import@ubuntu.com-20140601170408-m86xvdjd83a4qon0
Tags: 4.4.1-1ubuntu1
* Merge from Debian unstable. Remaining Ubuntu changes:
 - Let the binary-predeb target work on the usr/lib/python* directory
   as we don't have usr/share/pyshared anymore.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
""" Defines the DataLabel class and related trait and function.
2
2
"""
3
3
# Major library imports
 
4
from math import sqrt
4
5
from numpy import array, asarray, inf
5
6
from numpy.linalg import norm
6
7
 
15
16
 
16
17
 
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
 
21
# lower left corner.
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"),
23
26
                           Tuple, List)
24
27
 
25
28
 
66
69
        pt1 = asarray(pt1)
67
70
        pt2 = asarray(pt2)
68
71
 
69
 
        unit_vec = (pt2-pt1)
 
72
        unit_vec = pt2 - pt1
70
73
        unit_vec /= norm(unit_vec)
71
74
 
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))
76
79
        else:
77
 
            slope = unit_vec[1]/unit_vec[0]
78
 
            perp_slope = -1/slope
 
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)
81
84
 
82
85
        pt1 = pt1 + offset1 * unit_vec
83
86
        pt2 = pt2 - offset2 * unit_vec
84
87
 
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)
88
91
    else:
89
92
        pt1, pt2, arrowhead_l, arrowhead_r = arrow
107
110
    return arrow
108
111
 
109
112
 
 
113
def find_region(px, py, x, y, x2, y2):
 
114
    """Classify the location of the point (px, py) relative to a rectangle.
 
115
 
 
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:
 
119
 
 
120
            \     top      /
 
121
             \            /
 
122
              +----------+
 
123
         left |  inside  | right
 
124
              +----------+
 
125
             /            \ 
 
126
            /    bottom    \ 
 
127
 
 
128
    """
 
129
    if px < x:
 
130
        dx = x - px
 
131
        if py > y2 + dx:
 
132
            region = 'top'
 
133
        elif py < y - dx:
 
134
            region = 'bottom'
 
135
        else:
 
136
            region = 'left'
 
137
    elif px > x2:
 
138
        dx = px - x2
 
139
        if py > y2 + dx:
 
140
            region = 'top'
 
141
        elif py < y - dx:
 
142
            region = 'bottom'
 
143
        else:
 
144
            region = 'right'
 
145
    else:  # x <= px <= x2
 
146
        if py > y2:
 
147
            region = 'top'
 
148
        elif py < y:
 
149
            region = 'bottom'
 
150
        else:
 
151
            region = 'inside'
 
152
    return region
 
153
 
 
154
 
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.
 
157
 
 
158
    Optionally, an arrow is drawn to the point.
112
159
    """
113
160
 
114
161
    # The symbol to use if **marker** is set to "custom". This attribute must
139
186
 
140
187
    # The center x position (average of x and x2)
141
188
    xmid = Property(Float, depends_on=['x', 'x2'])
142
 
    
 
189
 
143
190
    # The center y position (average of y and y2)
144
191
    ymid = Property(Float, depends_on=['y', 'y2'])
145
192
 
 
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')
 
200
 
146
201
    #----------------------------------------------------------------------
147
202
    # Marker traits
148
203
    #----------------------------------------------------------------------
154
209
    # keys.
155
210
    marker = MarkerTrait
156
211
 
157
 
    # The pixel size of the marker (doesn't include the thickness of the outline).
 
212
    # The pixel size of the marker (doesn't include the thickness of the
 
213
    # outline).
158
214
    marker_size = Int(4)
159
215
 
160
 
    # The thickness, in pixels, of the outline to draw around the marker.  If
161
 
    # this is 0, no outline will be drawn.
 
216
    # The thickness, in pixels, of the outline to draw around the marker.
 
217
    # If this is 0, no outline will be drawn.
162
218
    marker_line_width = Float(1.0)
163
219
 
164
220
    # The color of the inside of the marker.
182
238
    arrow_color = ColorTrait("black")
183
239
 
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
 
243
    # this attribute.
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")
196
253
    # the arrow will be drawn regardless of how long it is.
197
254
    arrow_max_length = Float(inf)
198
255
 
 
256
    #----------------------------------------------------------------------
 
257
    # Bubble traits
 
258
    #----------------------------------------------------------------------
 
259
 
 
260
    # The radius (in screen coordinates) of the curved corners of the "bubble".
 
261
    corner_radius = Float(10)
 
262
 
199
263
    #-------------------------------------------------------------------------
200
264
    # Private traits
201
265
    #-------------------------------------------------------------------------
205
269
 
206
270
    _cached_arrow = Any
207
271
 
208
 
    # When **arrow_root** is 'auto', this determines the location on the data label
209
 
    # from which the arrow is drawn, based on the position of the label relative
210
 
    # to its data point.
 
272
    # When **arrow_root** is 'auto', this determines the location on the data
 
273
    # label from which the arrow is drawn, based on the position of the label
 
274
    # relative to its data point.
211
275
    _position_root_map = {
212
276
        "top left": "bottom right",
213
277
        "top right": "bottom left",
230
294
        "right center": ("x2", "ymid"),
231
295
        }
232
296
 
233
 
 
234
297
    def overlay(self, component, gc, view_bounds=None, mode="normal"):
235
298
        """ Draws the tooltip overlaid on another component.
236
299
 
243
306
 
244
307
        self.do_layout()
245
308
 
 
309
        if self.label_style == 'box':
 
310
            self._render_box(component, gc, view_bounds=view_bounds,
 
311
                             mode=mode)
 
312
        else:
 
313
            self._render_bubble(component, gc, view_bounds=view_bounds,
 
314
                                mode=mode)
 
315
 
 
316
        # draw the marker
 
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)
 
322
 
 
323
        if self.clip_to_plot:
 
324
            gc.restore_state()
 
325
 
 
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
254
335
                    else:
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)
259
 
                                 )
 
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))
260
341
 
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,
265
 
                                                self.arrow_color_,
266
 
                                                arrowhead_size=self.arrow_size,
267
 
                                                offset1=3,
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),
 
346
                                            self._screen_coords,
 
347
                                            self.arrow_color_,
 
348
                                            arrowhead_size=self.arrow_size,
 
349
                                            offset1=3,
 
350
                                            offset2=self.marker_size + 3,
 
351
                                            minlen=self.arrow_min_length,
 
352
                                            maxlen=self.arrow_max_length)
271
353
            else:
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)
279
361
 
280
 
        # draw the marker
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)
285
 
 
286
 
        if self.clip_to_plot:
287
 
            gc.restore_state()
 
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
 
366
 
 
367
        # (x, y) is the lower left corner of the label.
 
368
        x = self.x
 
369
        y = self.y
 
370
        # (x2, y2) is the upper right corner of the label.
 
371
        x2 = self.x2
 
372
        y2 = self.y2
 
373
        # r is the corner radius.
 
374
        r = self.corner_radius
 
375
 
 
376
        if self.arrow_visible:
 
377
            # FIXME: Make 'gap_width' a configurable trait (and give it a
 
378
            #        better name).
 
379
            max_gap_width = 10
 
380
            gap_width = min(max_gap_width,
 
381
                            abs(x2 - x - 2 * r),
 
382
                            abs(y2 - y - 2 * r))
 
383
            region = find_region(px, py, x, y, x2, y2)
 
384
 
 
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:
 
389
                    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
 
393
                if region == 'left':
 
394
                    bx = x
 
395
                else:
 
396
                    bx = x2
 
397
            else:
 
398
                gap_start = px - gap_width / 2
 
399
                if gap_start < x + r:
 
400
                    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
 
404
                if region == 'top':
 
405
                    by = y2
 
406
                else:
 
407
                    by = y
 
408
 
 
409
        arrow_len = sqrt((px - bx) ** 2 + (py - by) ** 2)
 
410
        arrow_visible = (self.arrow_visible and
 
411
                         (arrow_len >= self.arrow_min_length))
 
412
 
 
413
        with gc:
 
414
            if self.border_visible:
 
415
                gc.set_line_width(self.border_width)
 
416
                gc.set_stroke_color(self.border_color_)
 
417
            else:
 
418
                gc.set_line_width(0)
 
419
                gc.set_stroke_color((0, 0, 0, 0))
 
420
            gc.set_fill_color(self.bgcolor_)
 
421
 
 
422
            # Start at the lower left, on the left edge where the curved
 
423
            # part of the box ends.
 
424
            gc.move_to(x, y + r)
 
425
 
 
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)
 
429
                gc.line_to(px, py)
 
430
                gc.line_to(x, gap_start + gap_width)
 
431
            gc.arc_to(x, y2, x + r, y2, r)
 
432
 
 
433
            # Draw the top and the upper right curved corner.
 
434
            if arrow_visible and region == 'top':
 
435
                gc.line_to(gap_start, y2)
 
436
                gc.line_to(px, py)
 
437
                gc.line_to(gap_start + gap_width, y2)
 
438
            gc.arc_to(x2, y2, x2, y2 - r, r)
 
439
 
 
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)
 
443
                gc.line_to(px, py)
 
444
                gc.line_to(x2, gap_start)
 
445
            gc.arc_to(x2, y, x2 - r, y, r)
 
446
 
 
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)
 
450
                gc.line_to(px, py)
 
451
                gc.line_to(gap_start, y)
 
452
            gc.arc_to(x, y, x, y + r, r)
 
453
 
 
454
            # Finish the "bubble".
 
455
            gc.draw_path()
 
456
 
 
457
            self._draw_overlay(gc)
288
458
 
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)
323
493
                else:
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
326
496
        else:
327
497
            self.x = sx + self.label_position[0]
328
498
            self.y = sy + self.label_position[1]
347
517
        pt = self.data_point
348
518
        if pt is not None:
349
519
            if self.show_label_coords:
350
 
                self.lines = [self.label_text, self.label_format % {"x": pt[0], "y": pt[1]}]
 
520
                self.lines = [self.label_text,
 
521
                              self.label_format % {"x": pt[0], "y": pt[1]}]
351
522
            else:
352
523
                self.lines = [self.label_text]
353
524
 
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,
 
530
                                                  attach=attach)
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,
 
533
                                                  attach=attach)
361
534
        return
362
535
 
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',
 
539
                                   remove=not attach)
366
540
        return
367
541
 
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
 
544
        # 'updated' event.
370
545
        self._layout_needed = True
371
546
 
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," +
 
548
                     "arrow_max_length")
373
549
    def _invalidate_arrow(self):
374
550
        self._cached_arrow = None
375
551
        self._layout_needed = True
376
552
 
377
 
    @on_trait_change("label_position,position,position_items,bounds,bounds_items")
 
553
    @on_trait_change("label_position,position,position_items,bounds," +
 
554
                     "bounds_items")
378
555
    def _invalidate_layout(self):
379
556
        self._layout_needed = True
380
557
 
381
 
 
382
558
    def _get_xmid(self):
383
559
        return 0.5 * (self.x + self.x2)
384
 
    
 
560
 
385
561
    def _get_ymid(self):
386
562
        return 0.5 * (self.y + self.y2)