Revision: 8103 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=8103&view=rev Author: leejjoon Date: 2010-01-29 17:33:21 +0000 (Fri, 29 Jan 2010)
Log Message: ----------- refactor draggable legend to support annotation Modified Paths: -------------- trunk/matplotlib/lib/matplotlib/legend.py trunk/matplotlib/lib/matplotlib/offsetbox.py trunk/matplotlib/lib/matplotlib/text.py Added Paths: ----------- trunk/matplotlib/examples/animation/draggable_legend.py Added: trunk/matplotlib/examples/animation/draggable_legend.py =================================================================== --- trunk/matplotlib/examples/animation/draggable_legend.py (rev 0) +++ trunk/matplotlib/examples/animation/draggable_legend.py 2010-01-29 17:33:21 UTC (rev 8103) @@ -0,0 +1,43 @@ +import matplotlib.pyplot as plt + + +ax = plt.subplot(111) +ax.plot([1,2,3], label="test") + +l = ax.legend() +d1 = l.draggable() + +xy = 1, 2 +txt = ax.annotate("Test", xy, xytext=(-30, 30), + textcoords="offset points", + bbox=dict(boxstyle="round",fc=(0.2, 1, 1)), + arrowprops=dict(arrowstyle="->")) +d2 = txt.draggable() + + +from matplotlib._png import read_png +from matplotlib.cbook import get_sample_data + +from matplotlib.offsetbox import OffsetImage, AnnotationBbox + +fn = get_sample_data("lena.png", asfileobj=False) +arr_lena = read_png(fn) + +imagebox = OffsetImage(arr_lena, zoom=0.2) + +ab = AnnotationBbox(imagebox, xy, + xybox=(120., -80.), + xycoords='data', + boxcoords="offset points", + pad=0.5, + arrowprops=dict(arrowstyle="->", + connectionstyle="angle,angleA=0,angleB=90,rad=3") + ) + + +ax.add_artist(ab) + +d3 = ab.draggable() + + +plt.show() Modified: trunk/matplotlib/lib/matplotlib/legend.py =================================================================== --- trunk/matplotlib/lib/matplotlib/legend.py 2010-01-29 16:22:51 UTC (rev 8102) +++ trunk/matplotlib/lib/matplotlib/legend.py 2010-01-29 17:33:21 UTC (rev 8103) @@ -33,56 +33,28 @@ from matplotlib.patches import Patch, Rectangle, Shadow, FancyBboxPatch from matplotlib.collections import LineCollection, RegularPolyCollection, \ CircleCollection -from matplotlib.transforms import Bbox, BboxBase, TransformedBbox, BboxTransformTo +from matplotlib.transforms import Bbox, BboxBase, TransformedBbox, BboxTransformTo, BboxTransformFrom -from matplotlib.offsetbox import HPacker, VPacker, TextArea, DrawingArea +from matplotlib.offsetbox import HPacker, VPacker, TextArea, DrawingArea, DraggableOffsetBox -class DraggableLegend: - """helper code for a draggable legend -- see Legend.draggable""" - +class DraggableLegend(DraggableOffsetBox): def __init__(self, legend): - self.legend = legend - self.gotLegend = False + self.legend=legend + DraggableOffsetBox.__init__(self, legend, legend._legend_box) - c1 = legend.figure.canvas.mpl_connect('motion_notify_event', self.on_motion) - c2 = legend.figure.canvas.mpl_connect('pick_event', self.on_pick) - c3 = legend.figure.canvas.mpl_connect('button_release_event', self.on_release) - legend.set_picker(self.my_legend_picker) - self.cids = [c1, c2, c3] - - def on_motion(self, evt): - if self.gotLegend: - dx = evt.x - self.mouse_x - dy = evt.y - self.mouse_y - loc_in_canvas = self.legend_x + dx, self.legend_y + dy - loc_in_norm_axes = self.legend.parent.transAxes.inverted().transform_point(loc_in_canvas) - self.legend._loc = tuple(loc_in_norm_axes) - self.legend.figure.canvas.draw() - - def my_legend_picker(self, legend, evt): + def artist_picker(self, legend, evt): return self.legend.legendPatch.contains(evt) - def on_pick(self, evt): - legend = self.legend - if evt.artist == legend: - bbox = self.legend.get_window_extent() - self.mouse_x = evt.mouseevent.x - self.mouse_y = evt.mouseevent.y - self.legend_x = bbox.xmin - self.legend_y = bbox.ymin - self.gotLegend = 1 + def finalize_offset(self): + loc_in_canvas = self.get_loc_in_canvas() - def on_release(self, event): - if self.gotLegend: - self.gotLegend = False + bbox = self.legend.get_bbox_to_anchor() + _bbox_transform = BboxTransformFrom(bbox) + self.legend._loc = tuple(_bbox_transform.transform_point(loc_in_canvas)) + - def disconnect(self): - 'disconnect the callbacks' - for cid in self.cids: - self.legend.figure.canvas.mpl_disconnect(cid) - class Legend(Artist): """ Place a legend on the axes at location loc. Labels are a @@ -323,7 +295,6 @@ 'Falling back on "upper right".') loc = 1 - self._loc = loc self._mode = mode self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform) @@ -357,6 +328,8 @@ # init with null renderer self._init_legend_box(handles, labels) + self._loc = loc + self.set_title(title) self._last_fontsize_points = self._fontsize @@ -373,6 +346,28 @@ a.set_transform(self.get_transform()) + def _set_loc(self, loc): + # find_offset function will be provided to _legend_box and + # _legend_box will draw itself at the location of the return + # value of the find_offset. + self._loc_real = loc + if loc == 0: + _findoffset = self._findoffset_best + else: + _findoffset = self._findoffset_loc + + #def findoffset(width, height, xdescent, ydescent): + # return _findoffset(width, height, xdescent, ydescent, renderer) + + self._legend_box.set_offset(_findoffset) + + self._loc_real = loc + + def _get_loc(self): + return self._loc_real + + _loc = property(_get_loc, _set_loc) + def _findoffset_best(self, width, height, xdescent, ydescent, renderer): "Helper function to locate the legend at its best position" ox, oy = self._find_best_position(width, height, renderer) @@ -401,19 +396,6 @@ renderer.open_group('legend') - # find_offset function will be provided to _legend_box and - # _legend_box will draw itself at the location of the return - # value of the find_offset. - if self._loc == 0: - _findoffset = self._findoffset_best - else: - _findoffset = self._findoffset_loc - - def findoffset(width, height, xdescent, ydescent): - return _findoffset(width, height, xdescent, ydescent, renderer) - - self._legend_box.set_offset(findoffset) - fontsize = renderer.points_to_pixels(self._fontsize) # if mode == fill, set the width of the legend_box to the Modified: trunk/matplotlib/lib/matplotlib/offsetbox.py =================================================================== --- trunk/matplotlib/lib/matplotlib/offsetbox.py 2010-01-29 16:22:51 UTC (rev 8102) +++ trunk/matplotlib/lib/matplotlib/offsetbox.py 2010-01-29 17:33:21 UTC (rev 8103) @@ -20,7 +20,7 @@ import matplotlib.text as mtext import numpy as np from matplotlib.transforms import Bbox, BboxBase, TransformedBbox, \ - IdentityTransform + IdentityTransform, BboxTransformFrom from matplotlib.font_manager import FontProperties from matplotlib.patches import FancyBboxPatch, FancyArrowPatch @@ -168,14 +168,14 @@ """ self._offset = xy - def get_offset(self, width, height, xdescent, ydescent): + def get_offset(self, width, height, xdescent, ydescent, renderer): """ Get the offset accepts extent of the box """ if callable(self._offset): - return self._offset(width, height, xdescent, ydescent) + return self._offset(width, height, xdescent, ydescent, renderer) else: return self._offset @@ -222,7 +222,7 @@ get the bounding box in display space. ''' w, h, xd, yd, offsets = self.get_extent_offsets(renderer) - px, py = self.get_offset(w, h, xd, yd) + px, py = self.get_offset(w, h, xd, yd, renderer) return mtransforms.Bbox.from_bounds(px-xd, py-yd, w, h) def draw(self, renderer): @@ -233,7 +233,7 @@ width, height, xdescent, ydescent, offsets = self.get_extent_offsets(renderer) - px, py = self.get_offset(width, height, xdescent, ydescent) + px, py = self.get_offset(width, height, xdescent, ydescent, renderer) for c, (ox, oy) in zip(self.get_visible_children(), offsets): c.set_offset((px+ox, py+oy)) @@ -946,7 +946,7 @@ ''' self._update_offset_func(renderer) w, h, xd, yd = self.get_extent(renderer) - ox, oy = self.get_offset(w, h, xd, yd) + ox, oy = self.get_offset(w, h, xd, yd, renderer) return Bbox.from_bounds(ox-xd, oy-yd, w, h) @@ -996,7 +996,7 @@ width, height, xdescent, ydescent = self.get_extent(renderer) - px, py = self.get_offset(width, height, xdescent, ydescent) + px, py = self.get_offset(width, height, xdescent, ydescent, renderer) self.get_child().set_offset((px, py)) self.get_child().draw(renderer) @@ -1121,12 +1121,15 @@ # self.offset_transform.translate(xy[0], xy[1]) + def get_offset(self): """ return offset of the container. """ return self._offset + def get_children(self): + return [self.image] def get_window_extent(self, renderer): ''' @@ -1243,9 +1246,9 @@ def contains(self,event): t,tinfo = self.offsetbox.contains(event) - if self.arrow is not None: - a,ainfo=self.arrow.contains(event) - t = t or a + #if self.arrow_patch is not None: + # a,ainfo=self.arrow_patch.contains(event) + # t = t or a # self.arrow_patch is currently not checked as this can be a line - JJ @@ -1380,7 +1383,151 @@ +class DraggableBase(object): + """ + helper code for a draggable artist (legend, offsetbox) + The derived class must override following two method. + def saveoffset(self): + pass + + def update_offset(self, dx, dy): + pass + + *saveoffset* is called when the object is picked for dragging and it is + meant to save reference position of the artist. + + *update_offset* is called during the dragging. dx and dy is the pixel + offset from the point where the mouse drag started. + + Optionally you may override following two methods. + + def artist_picker(self, artist, evt): + return self.ref_artist.contains(evt) + + def finalize_offset(self): + pass + + *artist_picker* is a picker method that will be + used. *finalize_offset* is called when the mouse is released. In + current implementaion of DraggableLegend and DraggableAnnotation, + *update_offset* places the artists simply in display + coordinates. And *finalize_offset* recalculate their position in + the normalized axes coordinate and set a relavant attribute. + + """ + def __init__(self, ref_artist): + self.ref_artist = ref_artist + self.got_artist = False + + self.canvas = self.ref_artist.figure.canvas + c2 = self.canvas.mpl_connect('pick_event', self.on_pick) + c3 = self.canvas.mpl_connect('button_release_event', self.on_release) + + ref_artist.set_picker(self.artist_picker) + self.cids = [c2, c3] + + def on_motion(self, evt): + if self.got_artist: + dx = evt.x - self.mouse_x + dy = evt.y - self.mouse_y + self.update_offset(dx, dy) + + def on_pick(self, evt): + if evt.artist == self.ref_artist: + + self.save_offset() + self.mouse_x = evt.mouseevent.x + self.mouse_y = evt.mouseevent.y + self.got_artist = True + + self._c1 = self.canvas.mpl_connect('motion_notify_event', self.on_motion) + + def on_release(self, event): + if self.got_artist: + self.finalize_offset() + self.got_artist = False + self.canvas.mpl_disconnect(self._c1) + + def disconnect(self): + 'disconnect the callbacks' + for cid in self.cids: + self.canvas.mpl_disconnect(cid) + + def artist_picker(self, artist, evt): + return self.ref_artist.contains(evt) + + def save_offset(self): + pass + + def update_offset(self, dx, dy): + pass + + def finalize_offset(self): + pass + + +class DraggableOffsetBox(DraggableBase): + def __init__(self, ref_artist, offsetbox): + DraggableBase.__init__(self, ref_artist) + self.offsetbox = offsetbox + + def save_offset(self): + offsetbox = self.offsetbox + renderer = offsetbox.figure._cachedRenderer + w, h, xd, yd = offsetbox.get_extent(renderer) + offset = offsetbox.get_offset(w, h, xd, yd, renderer) + self.offsetbox_x, self.offsetbox_y = offset + + def update_offset(self, dx, dy): + loc_in_canvas = self.offsetbox_x + dx, self.offsetbox_y + dy + self.offsetbox.set_offset(loc_in_canvas) + self.offsetbox.figure.canvas.draw() + + def get_loc_in_canvas(self): + + offsetbox=self.offsetbox + renderer = offsetbox.figure._cachedRenderer + w, h, xd, yd = offsetbox.get_extent(renderer) + ox, oy = offsetbox._offset + loc_in_canvas = (ox-xd, oy-yd) + + return loc_in_canvas + + +class DraggableAnnotation(DraggableBase): + def __init__(self, annotation): + DraggableBase.__init__(self, annotation) + self.annotation = annotation + + def save_offset(self): + ann = self.annotation + x, y = ann.xytext + if isinstance(ann.textcoords, tuple): + xcoord, ycoord = ann.textcoords + x1, y1 = ann._get_xy(x, y, xcoord) + x2, y2 = ann._get_xy(x, y, ycoord) + ox0, oy0 = x1, y2 + else: + ox0, oy0 = ann._get_xy(x, y, ann.textcoords) + + self.ox, self.oy = ox0, oy0 + self.annotation.textcoords = "figure pixels" + + def update_offset(self, dx, dy): + ann = self.annotation + ann.xytext = self.ox + dx, self.oy + dy + x, y = ann.xytext + xy = ann._get_xy(x, y, ann.textcoords) + self.canvas.draw() + + def finalize_offset(self): + loc_in_canvas = self.annotation.xytext + self.annotation.textcoords = "axes fraction" + pos_axes_fraction = self.annotation.axes.transAxes.inverted().transform_point(loc_in_canvas) + self.annotation.xytext = tuple(pos_axes_fraction) + + if __name__ == "__main__": fig = plt.figure(1) Modified: trunk/matplotlib/lib/matplotlib/text.py =================================================================== --- trunk/matplotlib/lib/matplotlib/text.py 2010-01-29 16:22:51 UTC (rev 8102) +++ trunk/matplotlib/lib/matplotlib/text.py 2010-01-29 17:33:21 UTC (rev 8103) @@ -1402,6 +1402,9 @@ self.textcoords = textcoords self.set_annotation_clip(annotation_clip) + self._draggable = None + + def _get_xy(self, x, y, s): if s=='data': trans = self.axes.transData @@ -1534,8 +1537,38 @@ return True + def draggable(self, state=None): + """ + Set the draggable state -- if state is + * None : toggle the current state + * True : turn draggable on + + * False : turn draggable off + + If draggable is on, you can drag the annotation on the canvas with + the mouse. The DraggableAnnotation helper instance is returned if + draggable is on. + """ + from matplotlib.offsetbox import DraggableAnnotation + is_draggable = self._draggable is not None + + # if state is None we'll toggle + if state is None: + state = not is_draggable + + if state: + if self._draggable is None: + self._draggable = DraggableAnnotation(self) + else: + if self._draggable is not None: + self._draggable.disconnect() + self._draggable = None + + return self._draggable + + class Annotation(Text, _AnnotationBase): """ A :class:`~matplotlib.text.Text` class to make annotating things @@ -1661,6 +1694,7 @@ else: self.arrow_patch = None + def contains(self,event): t,tinfo = Text.contains(self,event) if self.arrow is not None: @@ -1803,6 +1837,9 @@ Text.draw(self, renderer) + + + docstring.interpd.update(Annotation=Annotation.__init__.__doc__) This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. ------------------------------------------------------------------------------ The Planet: dedicated and managed hosting, cloud storage, colocation Stay online with enterprise data centers and the best network in the business Choose flexible plans and management services without long-term contracts Personal 24x7 support from experience hosting pros just a phone call away. http://p.sf.net/sfu/theplanet-com _______________________________________________ Matplotlib-checkins mailing list Matplotlib-checkins@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/matplotlib-checkins