#! /usr/bin/env python # # Draw a scatterplot. # # This module is intended to be imported by another module. # If run as __main__, it just tests itself. # # Written and placed in the Public Domain by Peter Pearson. import Tkinter class Scatterplot: def __init__( self, xy, color = "black", size = 2, \ width = 600, height = 600, \ parent_window = None, \ id = None ): """xy is a list of coordinate pairs. color is a color name like "black". size is the dot width in pixels. id should be text: it labels the window and is passed in callbacks.""" assert len(xy) > 0 xmin, ymin = xy[0] xmax, ymax = xmin, ymin for x, y in xy: if x < xmin: xmin = x elif x > xmax: xmax = x if y < ymin: ymin = y elif y > ymax: ymax = y if xmin == xmax: xmin, xmax = xmin - 1, xmin + 1 if ymin == ymax: ymin, ymax = ymin - 1, ymin + 1 if parent_window == None: t = Tkinter.Tk() else: t = Tkinter.Toplevel( parent_window ) t.title( id ) t.protocol( "WM_DELETE_WINDOW", self.window_delete ) c = Tkinter.Canvas( t, width = width, height = height ) c.pack() # Define scaling and offset to translate from xy coordinates # to canvas coordinates: canvas = offset + scale * xy # # Leave a small margin. This is partly a kluge to avoid # thinking about the +2 I add to give the line a finite # length; also to avoid thinking about the fact that the # canvas coordinate system seems to begin at 1, not 0. # # margin = xoffset + xscale * xmin # width - margin = xoffset + xscale * xmax # => width - margin = margin - xscale * xmin + xscale * xmax # => width - 2*margin = xscale * ( xmax - xmin ) # => xscale = ( width - 2*margin ) / ( xmax - xmin ) # xoffset = margin - xscale * xmin # # margin = yoffset + yscale * ymax # height - margin = yoffset + yscale * ymin # => height - 2 * margin = yscale * ( ymin - ymax ) # => yscale = ( height - 2 * margin ) / ( ymin - ymax ) # yoffset = margin - yscale * ymax margin = 4 # pixels xscale = ( width - 2 * margin ) / float( xmax - xmin ) xoffset = margin - xscale * xmin yscale = ( height - 2 * margin ) / float( ymin - ymax ) yoffset = margin - yscale * ymax # Plot the points: plot_points( c, xy, size, color, xoffset, xscale, yoffset, yscale ) c.bind( "", self.mouseDown ) c.bind( "", self.mouseUp ) self.window = t self.xscale = xscale self.yscale = yscale self.xoffset = xoffset self.yoffset = yoffset self.parent_window = parent_window self.callback_mouse_click = None self.callback_window_delete = None self.mouseDownX = None self.mouseDownY = None self.id = id self.canvas = c def clear( self ): self.canvas.delete( Tkinter.ALL ) def plot( self, xy, color = "black", size = 2 ): plot_points( self.canvas, xy, size, color, \ self.xoffset, self.xscale, self.yoffset, self.yscale ) def close( self ): self.window.destroy() def mouseDown( self, event ): """Callback for left-button-down mouse events.""" self.mouseDownX = event.x self.mouseDownY = event.y def mouseUp( self, event ): """Callback for left-button-up mouse events.""" # If we're managing our own window, then control was # surrendered to mainloop in get_mouse_click, and we # just need to make control return there: if self.callback_mouse_click == None: self.window.quit() else: # Otherwise, the program that created us is waiting # to get control back. We call his callback. self.callback_mouse_click( \ ( self.mouseDownX - self.xoffset ) / self.xscale, \ ( self.mouseDownY - self.yoffset ) / self.yscale, \ self.id ) def set_callback( self, callback, event_type ): # # Unlike the callbacks that we define in order to get # control, these are callbacks by which our client (the # program that imports this package) demands control under # certain circumstances. # # Callbacks: # "mouse-click" takes three arguments: x, y, and id. # X and y are the coordinates (caller's coordinates) # of the position where the mouse was clicked. # Id is the identity (if any) assigned to this # object when it was created. # "window-delete" takes one argument: id, the identity (if any) # assigned to this object when it was created. # if event_type == "window-delete": self.callback_window_delete = callback else: assert event_type == "mouse-click" self.callback_mouse_click = callback def get_mouse_click( self ): """Wait for a mouse click, and return the coordinates.""" # If a parent window was specified during our creation, then # get_mouse_click can't be called. Instead, set_callback # must be used. assert self.parent_window == None self.window.mainloop() return ( self.mouseDownX - self.xoffset ) / self.xscale, \ ( self.mouseDownY - self.yoffset ) / self.yscale def window_delete( self ): """This callback gets called if the user closes our window.""" # # If our client requested notification, notify him: # if self.callback_window_delete: self.callback_window_delete( self.id ) # # Get our window off the screen: # self.close() def plot_points( c, xy, size, color, xoffset, xscale, yoffset, yscale ): for x, y in xy: c.create_line( xoffset + xscale * x - size/2, yoffset + yscale * y, \ xoffset + xscale * x + (size+1)/2, \ yoffset + yscale * y, \ width = size, fill = color ) # Running as __main__ only executes some tests: if __name__ == "__main__": import math z = [] for i in range( 1000 ): a = .1 * i z.append( ( math.sin( a ), math.sin( .7 * a ) ) ) s = Scatterplot( z ) print "Click someplace in the scatterplot." print "x, y = %f, %f" % s.get_mouse_click() s.close() z = [] for i in range( 1000 ): a = .1 * i z.append( ( math.sin( a ), math.sin( a/1.618 ) ) ) s = Scatterplot( z, color = "red" ) print "Click someplace in the scatterplot." print "x, y = %f, %f" % s.get_mouse_click() s.close() z = [] for i in range( 1000 ): a = .2 * i z.append( ( math.sin( a ), math.sin( a/1.618 ) ) ) s = Scatterplot( z, size = 4 ) print "Click someplace in the scatterplot." print "x, y = %f, %f" % s.get_mouse_click() s.close()