Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/graph.py
diff options
context:
space:
mode:
Diffstat (limited to 'graph.py')
-rwxr-xr-xgraph.py1447
1 files changed, 1447 insertions, 0 deletions
diff --git a/graph.py b/graph.py
new file mode 100755
index 0000000..d0d60bb
--- /dev/null
+++ b/graph.py
@@ -0,0 +1,1447 @@
+#!/usr/bin/env python
+"""
+ graph.py
+ Activity that plots 1st and 2nd degree polynomials
+ Part of the olpc.gr project
+ Copyright (C) 2009 Xenofon Papadopoulos <xpapad@gmail.com>
+"""
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import logging
+from gettext import gettext as _
+import os
+import copy
+import time
+import math
+
+import cairo
+import pango
+import gtk
+import gtk.glade
+import gtk.gdk
+import gobject
+from gobject import GObject
+from math import sqrt, pow
+from gtk.gdk import Color
+from gtk import ListStore
+from gtk import TreeViewColumn
+
+# Check for equality between floats
+SIGMA = 1e-6
+def equals( x, val ):
+ """Check for equality between floats, using a Sigma of 1e-6"""
+ return ( x == val or ( abs( x - val ) <= SIGMA ) )
+
+def markup_func( column, cell, model, iter ):
+ cell.props.markup = model.get_value( iter, 0 ).markup
+ cell.props.foreground = model.get_value( iter, 0 ).color
+
+def toggle_visible_func( column, cell, model, iter ):
+ cell.props.active = model.get_value( iter, 0 ).visible
+
+def toggle_info_func( column, cell, model, iter ):
+ cell.props.active = model.get_value( iter, 0 ).info
+
+def rounded_number( x, dec ):
+ """
+ Return a text representation of a number with a specified number of
+ decimal points.
+
+ x -- the number to convert to text
+ dex -- the number of decimal points
+ """
+ x = float( str( x ) )
+ fmt = '%%.%df' % ( dec )
+ return fmt % ( x )
+
+def translate_x( x, x_min, x_max, width ):
+ return int( ( width * ( x - x_min ) ) / ( x_max - x_min ) )
+
+def translate_y( y, y_min, y_max, height ):
+ return int( height - ( height * ( y - y_min ) ) / ( y_max - y_min ) )
+
+class Polynomial( GObject ):
+ """
+ A class to handle polynomials of 1st and 2nd degree. It can be expanded
+ to support polynomials of higher degree.
+ It subclasses gobject.GObject so that it can be used in TreeView models,
+ such as a ListStore.
+ """
+ def __init__( self, degree ):
+ """Class constructor."""
+ self.degree = degree
+ self.coefficient = { 0: 0, 1: 0, 2: 0 }
+ self.solved = False
+ self.D = None
+ self.x1 = None
+ self.x2 = None
+ self.limit = ( None, None )
+ self.has_min = None
+ self.cmap = gtk.gdk.colormap_get_system()
+ self.fg_color = None
+ self.is_visible = True
+ self.show_marks = True
+
+ def copy( self, obj ):
+ """
+ Make a deep copy of the object. The copy.deepcopy() function will
+ not work for GObjects.
+ """
+ self.degree = obj.degree
+ self.coefficient = copy.deepcopy( obj.coefficient )
+ self.solved = obj.solved
+ self.D = obj.D
+ self.x1 = obj.x1
+ self.x2 = obj.x2
+ self.limit = obj.limit
+ self.has_min = obj.has_min
+ self.fg_color = obj.fg_color
+ self.is_visible = obj.is_visible
+
+ def equals( self, obj ):
+ """
+ Check for equality between polynomials. Two polynomials are equal if
+ their degree and all their coefficients are equal.
+ """
+ if self.degree != obj.degree:
+ return False
+ degree = self.degree
+ while degree >= 0:
+ if not equals(self.coefficient[degree], obj.coefficient[degree]):
+ return False
+ degree = degree - 1
+ return True
+
+ def get_coefficient_name( self, degree ):
+ """
+ Get a text representation of a coefficient's name. We assume that the
+ polynomial's form is a1x^n + a2x^(n-1) + ... + a(n+1). We want to
+ display a1 as 'a', a2 as 'b' etc. The exact representation is specified
+ in the localization files.
+ """
+ index = self.degree - degree + 1
+ name = 'a' + str( index )
+ return _( name )
+
+ def is_empty( self ):
+ """Check if all coefficients are zero."""
+ degree = self.degree
+ while degree >= 0:
+ if not equals( self.coefficient[ degree ], 0 ):
+ return False
+ degree = degree - 1
+ return True
+
+ def get_template_markup( self, use_f_notation = False ):
+ """
+ Return a notation of the polynomial in Pango markup form. Do not
+ use coefficient values; use coefficient names instead.
+
+ use_f_notation -- if True, then use f(x) = ... in the notation,
+ otherwise use y = ...
+ """
+ msg = ''
+ if use_f_notation:
+ msg = 'f( x ) = '
+ else:
+ msg = 'y = '
+ degree = self.degree
+ while degree >= 0:
+ if degree != self.degree:
+ msg = msg + ' + '
+ msg = msg + self.get_coefficient_name( degree )
+ if degree >= 2:
+ msg = msg + 'x<sup>%d</sup>' % ( degree )
+ elif degree == 1:
+ msg = msg + 'x'
+ degree = degree - 1
+ return msg
+
+ def get_expression_markup( self, use_f_notation = False ):
+ """
+ Return a notation of the polynomial in Pango markup form, by using
+ coefficient values.
+
+ use_f_notation -- if True, then use f(x) = ... in the notation,
+ otherwise use y = ...
+ """
+ msg = ''
+ added = False
+ degree = self.degree
+ if use_f_notation:
+ msg = 'f( x ) = '
+ else:
+ msg = 'y = '
+ while degree >= 0:
+ val = self.coefficient[ degree ]
+ if equals( val, 0 ):
+ degree = degree - 1
+ continue
+
+ # We do not display a coefficient of 1, unless it's last one
+ if equals( abs( val ), 1.0 ) and degree > 0:
+ strval = ''
+ else:
+ strval = '%.3g' % ( abs( val ) )
+
+ # Display the sign seperated from the coefficient value
+ # Only the first factor will be signed normally
+ if not added:
+ if val < 0:
+ msg = msg + '-' + strval
+ else:
+ msg = msg + strval
+ else:
+ if val < 0:
+ msg = msg + ' - ' + strval
+ else:
+ msg = msg + ' + ' + strval
+ added = True
+
+ # Add the factor notation
+ if degree >= 2:
+ msg = msg + 'x<sup>%d</sup>' % ( degree )
+ elif degree == 1:
+ msg = msg + 'x'
+ degree = degree - 1
+
+ # Handle the special case where all coefficients are zero
+ if not added:
+ msg = msg + '0'
+ return msg
+
+ def set_expression_markup( self, txt ):
+ None
+
+ def set_coefficient( self, degree, value ):
+ """Set the value of the specified coefficient."""
+ val = float( str( value ) )
+ if equals( val, 0 ):
+ val = 0 # For the special case of -0.0
+ self.coefficient[ degree ] = float( str( value ) )
+ self.solved = False
+
+ def get_coefficients( self ):
+ """Get the values of all coefficients, in order of descending degree."""
+ degree = self.degree
+ ret = []
+ while degree >= 0:
+ ret.append( self.coefficient[ degree ] )
+ degree = degree - 1
+ return ret
+
+ def calculate( self, x ):
+ """Calculate the value of the polynomial for a given x."""
+ degree = 0
+ y = 0
+ # Add in order of increasing degree to keep precision for high x values
+ while degree <= self.degree:
+ coef = self.coefficient[ degree ]
+ val = pow( x, degree )
+ y = coef * val + y
+ degree = degree + 1
+ return y
+
+ def solve( self ):
+ """
+ Solve the polynomial. When the polynomial is solved, x1 and x2 are set
+ to the solutions, and D specified the number of solutions:
+
+ D = 0 : one solution (double, in case of a 2nd degree polynomial)
+ D > 0 : two solutions (only in the case of a 2nd degree polynomial)
+ D < 0 : no solutions
+ """
+ a = self.coefficient[ 2 ]
+ b = self.coefficient[ 1 ]
+ c = self.coefficient[ 0 ]
+
+ if equals( a, 0 ):
+ if equals( b, 0 ):
+ self.D = -1
+ self.x1 = None
+ self.x2 = None
+ self.solved = True
+ return
+ self.D = 0
+ self.x1 = -c / b
+ self.x2 = self.x1
+ self.solved = True
+ return
+
+ # Solution
+ D = b * b - 4 * a * c
+ self.D = D
+
+ if equals( D, 0 ):
+ x = -b / ( 2 * a )
+ self.x1 = x
+ self.x2 = x
+ elif D > 0:
+ self.x1 = ( -b - sqrt( D ) ) / ( 2 * a )
+ self.x2 = ( -b + sqrt( D ) ) / ( 2 * a )
+
+ else:
+ self.x1 = None
+ self.x2 = None
+
+ # Akrotata
+ x = -b / ( 2 * a )
+ y = -D / ( 4 * a )
+ if equals( x, 0 ):
+ x = 0
+ if equals( y, 0 ):
+ y = 0
+ self.limit = ( x, y )
+ if a > 0:
+ self.has_min = True
+ else:
+ self.has_min = False
+
+ self.solved = True
+
+ def get_points_d1( self ):
+ """
+ Return a list of points of interest for a 1st degree polynomial.
+ Each list element is a tuple of point coordinations.
+ Points of interest are the points of intersection of the graph with
+ the axis (x=0, y=0)
+ """
+ a, b = self.coefficient[ 1 ], self.coefficient[ 0 ]
+ ret = []
+
+ # x = 0
+ x = 0
+ y = self.calculate( x )
+ ret.append( ( x, y ) )
+
+ # y = 0
+ y = 0
+ if not equals( a, 0 ):
+ x = -b/a
+ ret.append( ( x, y ) )
+ return ret
+
+ def get_points_d2( self ):
+ """
+ Return a list of points of interest for a 2nd degree polynomial.
+ Each list element is a tuple of point coordinations.
+ Points of interest are the points of intersection of the graph with
+ the axis (x=0, y=0)
+ """
+ ret = []
+
+ # x = 0
+ x = 0
+ y = self.calculate( x )
+ ret.append( ( x, y ) )
+
+ # y = 0
+ self.solve()
+ if equals( self.D, 0 ):
+ x = self.x1
+ ret.append( ( x, 0 ) )
+ elif self.D > 0:
+ for x in [ self.x1, self.x2 ]:
+ ret.append( ( x, 0 ) )
+
+ return ret
+
+ def get_points( self ):
+ """
+ Return a list of points of interest for a polynomial.
+ Each list element is a tuple of point coordinations.
+ Points of interest are the points of intersection of the graph with
+ the axis (x=0, y=0)
+ """
+ if self.degree == 1:
+ return self.get_points_d1()
+ elif self.degree == 2:
+ return self.get_points_d2()
+ else:
+ return []
+
+ def get_second_degree( self ):
+ """
+ Return a 2nd degree polynomial with the same coefficients as a 1st
+ degree one. This is used to solve a system of polynomials of 1st and
+ 2nd degree by converting them all to 2nd degree.
+ """
+ poly = Polynomial( 2 )
+ if self.degree == 2:
+ poly.copy( self )
+ else:
+ poly.coefficient[ 1 ] = self.coefficient[ 1 ]
+ poly.coefficient[ 0 ] = self.coefficient[ 0 ]
+ poly.solved = False
+ return poly
+
+ def get_common_points( self, poly ):
+ """
+ Return a list of intersection points between self and poly.
+ Each list item is a tuple of point coordinations.
+ """
+ ret = []
+
+ # Get some 2nd degree polynomials
+ if self.degree == 1:
+ p1 = self.get_second_degree()
+ else:
+ p1 = self
+ if poly.degree == 1:
+ p2 = poly.get_second_degree()
+ else:
+ p2 = poly
+
+ # We need the common solutions of p1, p2
+ p = Polynomial( 2 )
+ for i in [ 2, 1, 0 ]:
+ p.coefficient[ i ] = p1.coefficient[ i ] - p2.coefficient[ i ]
+ p.solve()
+ if equals( p.D, 0 ):
+ x = p.x1
+ y = p1.calculate( x )
+ ret.append( ( x, y ) )
+ elif p.D > 0:
+ for x in [ p.x1, p.x2 ]:
+ y = p1.calculate( x )
+ ret.append( ( x, y ) )
+ return ret
+
+ markup = property( get_expression_markup )
+ """The markup property."""
+
+ def get_color( self ):
+ """color property getter."""
+ return self.fg_color
+
+ def set_color( self, val ):
+ """color property setter."""
+ cmap = gtk.gdk.colormap_get_system()
+ self.fg_color = cmap.alloc_color( val )
+
+ color = property( get_color )
+ """The color property."""
+
+ def get_visible( self ):
+ """visible property getter."""
+ return self.is_visible
+
+ def set_visible( self, state ):
+ """visible property setter."""
+ self.is_visible = state
+
+ def toggle_visible( self ):
+ """Toggles the visible property."""
+ self.is_visible = not self.is_visible
+
+ visible = property( get_visible, set_visible )
+ """The visible property."""
+
+ def set_show_marks( self, state ):
+ """info property setter."""
+ self.show_marks = state
+
+ def get_show_marks( self ):
+ """info property getter."""
+ return self.show_marks
+
+ def toggle_info( self ):
+ """Toggles the info property."""
+ self.show_marks = not self.show_marks
+
+ info = property( get_show_marks, set_show_marks )
+ """The info property."""
+
+ def get_x_bounds( self, axis, width, height, real_coords = False ):
+ """
+ Return a tuble of x coordinates that restict the visible area of the
+ polynomial on the specified screen width.
+
+ real_coords -- If True, then coordinates are real.
+ """
+ axis_x_min, axis_x_max, axis_y_min, axis_y_max = axis
+ step = ( axis_x_max - axis_x_min ) / width
+ x_min = x_max = None
+ xi_min = xi_max = None
+
+ x = axis_x_min
+ while x <= axis_x_max:
+ y = self.calculate( x )
+ xi = translate_x( x, axis_x_min, axis_x_max, width )
+ yi = translate_y( y, axis_y_min, axis_y_max, height )
+ if ( xi >= 0 and xi <= width and yi >= 0 and yi <= height ):
+ if xi_min == None:
+ xi_min = xi_max = xi
+ x_min = x_max = x
+ x = x + step
+ continue
+ if xi_max < xi:
+ xi_max = xi
+ x_max = x
+ x = x + step
+
+ if real_coords:
+ return ( xi_min, xi_max )
+ else:
+ return ( x_min, x_max )
+
+ def get_animator( self, wnd, gc, pixmap, axis ):
+ """Get the polygon's animator"""
+ if self.degree == 1:
+ ani = Animator_Degree_1( self, wnd, gc, pixmap, axis )
+ else:
+ ani = None
+ return ani
+
+class Point:
+ """
+ A helper class to handle points of interest of polynomials on the x-y
+ axis. It contains the coordinates, and some text to display when the user
+ clicks on (or near) them.
+ Each point has an associated rectangle of specified size that constitutes
+ the 'active' area around the point. The user may click on the rectangle
+ to get info about the point.
+ Point.x and Point.y are REAL, screen coordinates.
+ """
+ def __init__(self, x, y, size = 8):
+ """Class constructor."""
+ self.x = x
+ self.y = y
+ self.size = size
+ self.txt = ''
+ self.color = None
+
+ def set_text( self, txt ):
+ """Set the text displayed when the user clicks near the points."""
+ self.txt = txt
+
+ def set_poly_coords( self, x, y ):
+ """Set the coordinates of the rectangle displayed at this point."""
+ self.poly_x = x
+ self.poly_y = y
+
+ def draw( self, wnd, gc, size = 8 ):
+ """Draw a rectangle around this point, with te specified size."""
+ old_fg = None
+ if size != self.size:
+ self.size = size
+ if self.color != None:
+ old_fg = gc.foreground
+ gc.set_foreground( self.color )
+ s = self.size
+ x,y = self.x,self.y
+ wnd.draw_rectangle( gc, True, x - s / 2, y - s / 2, s + 1, s + 1 )
+ if old_fg != None:
+ gc.set_foreground( old_fg )
+
+ def set_color( self, color ):
+ """Set the color of this point."""
+ self.color = color
+
+ def in_window( self, xi, yi ):
+ """Return True if xi, yi reside inside the rectangle of this point."""
+ s = self.size
+ x,y = self.x, self.y
+ if xi < x - s/2 or xi > x + s/2:
+ return False
+ if yi < y - s/2 or yi > y + s/2:
+ return False
+ return True
+
+ def get_markup( self ):
+ """Return a formatted string of the point's text."""
+ x,y = self.poly_x, self.poly_y
+ msg = "x=%s\ny=%s" % ( rounded_number( x, 2 ), rounded_number( y, 2 ) )
+ return msg
+
+class Animator:
+ """A helper class to handle animation effects."""
+ def __init__(self, poly, wnd, gc, pixmap, axis ):
+ self.round = 0
+ self.poly = poly
+ self.wnd = wnd
+ self.gc = gc
+ self.pixmap = pixmap
+ self.axis = axis
+ self.width, self.height = pixmap.get_size()
+ self.x_min, self.x_max = poly.get_x_bounds( axis, self.width, self.height )
+ self.xi_min = self.translate_x( self.x_min )
+ self.xi_max = self.translate_x( self.x_max )
+
+ def translate_x( self, x ):
+ axis_x_min, axis_x_max, axis_y_min, axis_y_max = self.axis
+ return translate_x( x, axis_x_min, axis_x_max, self.width )
+
+ def translate_y( self, y ):
+ axis_x_min, axis_x_max, axis_y_min, axis_y_max = self.axis
+ return translate_y( y, axis_y_min, axis_y_max, self.height )
+
+ def animate(self):
+ """
+ This is called whenever pixmap needs an update. The method should just
+ draw on the pixmap, updates are handled elsewhere.
+
+ Return False to end the animation.
+ """
+ self.round = self.round + 1
+ return self.round != 5
+
+ def next_round(self):
+ """Next animator round."""
+ self.round = self.round + 1
+
+class Animator_Degree_1( Animator ):
+ def __init__(self, poly, wnd, gc, pixmap, axis ):
+ Animator.__init__(self, poly, wnd, gc, pixmap, axis)
+ self.a = poly.coefficient[ 1 ]
+
+ self.pixbufs = [
+ self.pixbuf_from_img( "images/bike_rider_1.png", self.a ),
+ self.pixbuf_from_img( "images/bike_rider_2.png", self.a ),
+ self.pixbuf_from_img( "images/bike_rider_3.png", self.a ) ]
+
+ def pixbuf_from_img( self, img, theta ):
+ # Read the bike image
+ image = cairo.ImageSurface.create_from_png( img )
+ # Calculate the size of the rotated image
+ w, h = image.get_width(), image.get_height()
+ r = abs( math.atan( theta ) )
+ sinf = math.sin( r )
+ cosf = math.cos( r )
+ new_w = int( w * cosf + h * sinf )
+ new_h = int( w * sinf + h * cosf )
+
+ # Create a rotated image of the appropriate size
+ surface = cairo.ImageSurface( cairo.FORMAT_ARGB32, new_w, new_h )
+ context = cairo.Context( surface )
+
+ # Test rectangle
+ """
+ context.set_line_width( 1 )
+ context.set_source_rgba( 1, 0, 0 )
+ context.rectangle( 0, 0, new_w, new_h )
+ context.stroke()
+ """
+
+ if theta >= 0:
+ context.translate( 0, w * sinf )
+ context.rotate( -r )
+ else:
+ context.translate( h * sinf, 0 )
+ context.rotate( r )
+ context.set_source_surface( image )
+ context.paint()
+
+ if theta >= 0:
+ hypo = h + w * sinf * cosf
+ dx = - int( sinf * hypo )
+ dy = - int( cosf * hypo )
+ else:
+ hypo = h * cosf * cosf
+ dx = int( sinf * hypo )
+ dy = - int( cosf * hypo )
+
+ data = surface.get_data()
+ width, height = surface.get_width(), surface.get_height()
+ stride = surface.get_stride()
+
+ pixbuf = gtk.gdk.pixbuf_new_from_data( data, gtk.gdk.COLORSPACE_RGB, True, 8, width, height, stride )
+ return ( pixbuf, dx, dy )
+
+ def animate(self):
+ # Get image info
+ width, height = self.pixmap.get_size()
+ pb, dx, dy = self.pixbufs[ self.round % len( self.pixbufs ) ]
+ iw,ih = pb.get_width(), pb.get_height()
+
+ axis_x_min, axis_x_max, axis_y_min, axis_y_max = self.axis
+ x_start = axis_x_min
+ x_end = axis_x_max
+
+ if self.x_min == None:
+ return False
+
+ step = ( self.x_max - self.x_min ) / 45
+ x = self.x_min + step * self.round
+ self.round = self.round + 1
+ if x >= self.x_max:
+ return False
+
+ y = self.poly.calculate( x )
+ xi, yi = self.translate_x( x ), self.translate_y( y )
+
+ posx = xi + dx
+ posy = yi + dy
+ self.pixmap.draw_pixbuf( self.gc, pb, 0, 0, posx, posy, iw, ih )
+ return True
+
+class Animator_Degree_2( Animator ):
+ def __init__(self, poly, wnd, gc, pixmap, axis ):
+ Animator.__init__(self, poly, wnd, gc, pixmap, axis)
+ self.a = poly.coefficient[ 1 ]
+
+ self.pixbufs = [
+ self.pixbuf_from_img( "images/bike_rider_1.png", self.a ),
+ self.pixbuf_from_img( "images/bike_rider_2.png", self.a ),
+ self.pixbuf_from_img( "images/bike_rider_3.png", self.a ) ]
+
+ def pixbuf_from_img( self, img, theta ):
+ # Read the bike image
+ image = cairo.ImageSurface.create_from_png( img )
+ # Calculate the size of the rotated image
+ w, h = image.get_width(), image.get_height()
+ r = abs( math.atan( theta ) )
+ sinf = math.sin( r )
+ cosf = math.cos( r )
+ new_w = int( w * cosf + h * sinf )
+ new_h = int( w * sinf + h * cosf )
+
+ # Create a rotated image of the appropriate size
+ surface = cairo.ImageSurface( cairo.FORMAT_ARGB32, new_w, new_h )
+ context = cairo.Context( surface )
+
+ # Test rectangle
+ """
+ context.set_line_width( 1 )
+ context.set_source_rgba( 1, 0, 0 )
+ context.rectangle( 0, 0, new_w, new_h )
+ context.stroke()
+ """
+
+ if theta >= 0:
+ context.translate( 0, w * sinf )
+ context.rotate( -r )
+ else:
+ context.translate( h * sinf, 0 )
+ context.rotate( r )
+ context.set_source_surface( image )
+ context.paint()
+
+ if theta >= 0:
+ hypo = h + w * sinf * cosf
+ dx = - int( sinf * hypo )
+ dy = - int( cosf * hypo )
+ else:
+ hypo = h * cosf * cosf
+ dx = int( sinf * hypo )
+ dy = - int( cosf * hypo )
+
+ data = surface.get_data()
+ width, height = surface.get_width(), surface.get_height()
+ stride = surface.get_stride()
+
+ pixbuf = gtk.gdk.pixbuf_new_from_data( data, gtk.gdk.COLORSPACE_RGB, True, 8, width, height, stride )
+ return ( pixbuf, dx, dy )
+
+ def animate(self):
+ # Get image info
+ width, height = self.pixmap.get_size()
+ pb, dx, dy = self.pixbufs[ self.round % len( self.pixbufs ) ]
+ iw,ih = pb.get_width(), pb.get_height()
+
+ axis_x_min, axis_x_max, axis_y_min, axis_y_max = self.axis
+ x_start = axis_x_min
+ x_end = axis_x_max
+
+ if self.x_min == None:
+ return False
+
+ step = ( self.x_max - self.x_min ) / 45
+ x = self.x_min + step * self.round
+ self.round = self.round + 1
+ if x >= self.x_max:
+ return False
+
+ y = self.poly.calculate( x )
+ xi, yi = self.translate_x( x ), self.translate_y( y )
+
+ posx = xi + dx
+ posy = yi + dy
+ self.pixmap.draw_pixbuf( self.gc, pb, 0, 0, posx, posy, iw, ih )
+ return True
+
+# The main module class
+class Graph:
+ """
+ The main application class.
+ """
+ def __init__(self, runaslib=True):
+ """Class constructor."""
+ # Load Glade XML
+ self.xml = gtk.glade.XML( "graph.glade" )
+
+ # Make sure application shuts down if main window closes
+ self.w = self.xml.get_widget( 'window_graph' )
+ self.w.connect( 'delete_event', gtk.main_quit )
+
+ # Get Windows child
+ self.w_child = self.w.get_child()
+
+ # Get a 2nd degree polynomial
+ self.poly = None
+
+ # Get widgets
+ self.label_info = self.xml.get_widget( 'label_info' )
+ self.txt_func_a_1 = self.xml.get_widget( 'txt_func_a_1' )
+ self.txt_func_b_1 = self.xml.get_widget( 'txt_func_b_1' )
+ self.txt_func_a_2 = self.xml.get_widget( 'txt_func_a_2' )
+ self.txt_func_b_2 = self.xml.get_widget( 'txt_func_b_2' )
+ self.txt_func_c_2 = self.xml.get_widget( 'txt_func_c_2' )
+
+ # Axis widgets
+ self.txt_axis_x_min = self.xml.get_widget( 'txt_axis_x_min' )
+ self.txt_axis_x_max = self.xml.get_widget( 'txt_axis_x_max' )
+ self.txt_axis_x_step = self.xml.get_widget( 'txt_axis_x_step' )
+ self.txt_axis_y_min = self.xml.get_widget( 'txt_axis_y_min' )
+ self.txt_axis_y_max = self.xml.get_widget( 'txt_axis_y_max' )
+ self.txt_axis_y_step = self.xml.get_widget( 'txt_axis_y_step' )
+
+ # Main tab buttons
+ btn_draw = self.xml.get_widget( 'btn_draw' )
+ btn_draw.connect( 'clicked', self.on_draw_clicked )
+ self.set_button_icon( btn_draw, 'draw.svg' )
+
+ btn_store = self.xml.get_widget( 'btn_store' )
+ btn_store.connect( 'clicked', self.on_store_clicked )
+ self.set_button_icon( btn_store, 'archive.svg' )
+
+ self.btn_delete = self.xml.get_widget( 'btn_delete' )
+ self.btn_delete.connect( 'clicked', self.on_delete_clicked )
+ self.set_button_icon( self.btn_delete, 'delete.svg' )
+
+ self.btn_color = self.xml.get_widget( 'btn_color' )
+ self.btn_color.connect( 'clicked', self.on_color_clicked )
+ self.set_button_icon( self.btn_color, 'color.svg' )
+ self.color_dlg = None
+
+ self.btn_animate = self.xml.get_widget( 'btn_animate' )
+ self.btn_animate.connect( 'clicked', self.on_animate_clicked )
+ self.set_button_icon( self.btn_animate, 'animate.svg' )
+
+ # Config tab buttons
+ btn_reset = self.xml.get_widget( 'btn_reset' )
+ btn_reset.connect( 'clicked', self.on_reset_clicked )
+ self.set_button_icon( btn_reset, 'undo.svg' )
+
+ self.toggle_show_axis_numbers = self.xml.get_widget( "toggle_show_axis_numbers" )
+ self.set_button_icon( self.toggle_show_axis_numbers, 'ruller.svg' )
+
+ self.toggle_show_grid = self.xml.get_widget( "toggle_show_grid" )
+ self.set_button_icon( self.toggle_show_grid, 'grid.svg' )
+
+ # Setup draw area
+ self.draw = self.xml.get_widget( 'drawing_area' )
+ self.draw.connect( 'motion_notify_event', self.update_coords )
+ self.draw.connect( 'button_press_event', self.on_draw_button_pressed )
+ self.draw.connect( 'configure_event', self.on_draw_configure )
+ self.draw.connect( "expose_event", self.on_draw_expose )
+ self.draw.get_pango_context().set_font_description( pango.FontDescription( "fixed 6" ) )
+ self.pixmap = None
+ self.old_pixmap = None
+ self.gc = None
+ self.width = self.height = 0
+
+ # More event handlers
+ self.notebook = self.xml.get_widget( 'notebook_app' )
+ self.notebook.connect( 'switch_page', self.on_tab_switched )
+
+ # Template labels
+ self.notebook_degree = self.xml.get_widget( 'notebook_degree' )
+ self.list_functions = self.xml.get_widget( 'list_functions' )
+ self.list_functions.connect( "cursor-changed", self.on_functions_updated )
+ # setup the view ...
+ cellr = gtk.CellRendererText()
+ col = TreeViewColumn( _( 'Functions' ), cellr )
+ col.set_cell_data_func( cellr, markup_func )
+ col.set_expand( True )
+ self.list_functions.append_column( col )
+
+ cellr = gtk.CellRendererToggle()
+ cellr.connect( "toggled", self.on_visible_toggled )
+ pb = gtk.gdk.pixbuf_new_from_file( "icons/eye.png" )
+ img = gtk.Image()
+ img.set_from_pixbuf( pb )
+ img.show()
+ col = TreeViewColumn( None, cellr )
+ col.set_widget( img )
+ col.set_cell_data_func( cellr, toggle_visible_func )
+ col.set_expand( False )
+ self.list_functions.append_column( col )
+
+ cellr = gtk.CellRendererToggle()
+ cellr.connect( "toggled", self.on_info_toggled )
+ pb = gtk.gdk.pixbuf_new_from_file( "icons/information.png" )
+ img = gtk.Image()
+ img.set_from_pixbuf( pb )
+ img.show()
+ col = TreeViewColumn( None, cellr )
+ col.set_widget( img )
+ col.set_cell_data_func( cellr, toggle_info_func )
+ col.set_expand( False )
+ self.list_functions.append_column( col )
+
+ # ... and the model
+ self.list_functions.set_model( ListStore( gobject.TYPE_PYOBJECT ) )
+
+ # Labels
+ self.label_function_1_template = self.xml.get_widget( 'label_function_1_template' )
+ self.label_function_1_template.set_markup( Polynomial( 1 ).get_template_markup() )
+ self.label_function_2_template = self.xml.get_widget( 'label_function_2_template' )
+ self.label_function_2_template.set_markup( Polynomial( 2 ).get_template_markup() )
+
+ # Info label
+ self.label_info = self.xml.get_widget( 'label_info' )
+ self.label_info.set_text( '' )
+ #self.label_info.modify_font( pango.FontDescription( "sans 14" ) )
+
+ self.label_coords = self.xml.get_widget( 'label_coords' )
+ self.label_coords.set_text( '' )
+
+ # a[n] labels
+ self.xml.get_widget( "label_a_1" ).set_text( _( 'a1' ) + ' = ' )
+ self.xml.get_widget( "label_b_1" ).set_text( _( 'a2' ) + ' = ' )
+ self.xml.get_widget( "label_a_2" ).set_text( _( 'a1' ) + ' = ' )
+ self.xml.get_widget( "label_b_2" ).set_text( _( 'a2' ) + ' = ' )
+ self.xml.get_widget( "label_c_2" ).set_text( _( 'a3' ) + ' = ' )
+
+ # More labels
+ self.xml.get_widget( "label_from_1" ).set_text( _( 'From' ) )
+ self.xml.get_widget( "label_from_2" ).set_text( _( 'From' ) )
+ self.xml.get_widget( "label_until_1" ).set_text( _( 'To' ) )
+ self.xml.get_widget( "label_until_2" ).set_text( _( 'To' ) )
+ self.xml.get_widget( "label_step_1" ).set_text( _( 'Step' ) )
+ self.xml.get_widget( "label_step_2" ).set_text( _( 'Step' ) )
+ self.xml.get_widget( "label_axis" ).set_text( _( 'Axis' ) )
+ self.xml.get_widget( "label_dec_1" ).set_text( _( 'Decimals' ) )
+ self.xml.get_widget( "label_dec_2" ).set_text( _( 'Decimals' ) )
+
+ # Spin
+ self.spin_axis_x_dec = self.xml.get_widget( "spin_axis_x_dec" )
+ self.spin_axis_y_dec = self.xml.get_widget( "spin_axis_y_dec" )
+ self.spin_axis_x_dec.set_editable( False )
+ self.spin_axis_y_dec.set_editable( False )
+
+ # Tab labels
+ self.xml.get_widget( "label_tab_graph" ).set_text( _( 'Graph Tab' ) )
+ self.xml.get_widget( "label_tab_config" ).set_text( _( 'Config Tab' ) )
+
+ # Get parameters
+ self.txt_func_a_1 = self.xml.get_widget( 'txt_func_a_1' )
+ self.txt_func_b_1 = self.xml.get_widget( 'txt_func_b_1' )
+ self.txt_func_a_2 = self.xml.get_widget( 'txt_func_a_2' )
+ self.txt_func_b_2 = self.xml.get_widget( 'txt_func_b_2' )
+ self.txt_func_c_2 = self.xml.get_widget( 'txt_func_c_2' )
+
+ # Setup colors
+ cmap = gtk.gdk.colormap_get_system()
+ self.color_bg = cmap.alloc_color( '#FFFFCC' )
+ self.marked_point_color = cmap.alloc_color( '#00FF00' )
+
+ # Setup initial axis values etc
+ self.validate_parameters( None )
+ self.update_buttons_status()
+
+ # self.widget will be attached to the Activity
+ # This can be any GTK widget except a window
+ self.widget = self.w_child
+ if not runaslib:
+ self.w.show_all()
+ gtk.main()
+
+ def set_button_icon( self, btn, file, width = 40, height = 40 ):
+ fname = "icons/" + file
+ pb = gtk.gdk.pixbuf_new_from_file( fname )
+ pb = pb.scale_simple( width, height, gtk.gdk.INTERP_BILINEAR )
+ img = gtk.Image()
+ img.set_from_pixbuf( pb )
+ btn.set_label( '' )
+ btn.set_image( img )
+
+ def validate_parameters( self, poly ):
+ # Set default function factors
+ for txt in ( self.txt_func_a_1, self.txt_func_b_1, self.txt_func_a_2, self.txt_func_b_2, self.txt_func_c_2 ):
+ if ( txt.get_text() == '' ):
+ txt.set_text( '0' )
+
+ # Override function factors
+ try:
+ a1 = float( self.txt_func_a_1.get_text() )
+ b1 = float( self.txt_func_b_1.get_text() )
+ a2 = float( self.txt_func_a_2.get_text() )
+ b2 = float( self.txt_func_b_2.get_text() )
+ c2 = float( self.txt_func_c_2.get_text() )
+ except:
+ return False
+
+ # Set default axis values
+ if self.txt_axis_x_min.get_text() == '':
+ self.txt_axis_x_min.set_text( '-10' )
+ if self.txt_axis_x_max.get_text() == '':
+ self.txt_axis_x_max.set_text( '10' )
+ if self.txt_axis_x_step.get_text() == '':
+ self.txt_axis_x_step.set_text( '1' )
+
+ if self.txt_axis_y_min.get_text() == '':
+ self.txt_axis_y_min.set_text( '-10' )
+ if self.txt_axis_y_max.get_text() == '':
+ self.txt_axis_y_max.set_text( '10' )
+ if self.txt_axis_y_step.get_text() == '':
+ self.txt_axis_y_step.set_text( '1' )
+
+ # Override axis values
+ try:
+ self.axis_x_min = float( self.txt_axis_x_min.get_text() );
+ self.axis_x_max = float( self.txt_axis_x_max.get_text() );
+ self.axis_x_step = float( self.txt_axis_x_step.get_text() );
+
+ self.axis_y_min = float( self.txt_axis_y_min.get_text() );
+ self.axis_y_max = float( self.txt_axis_y_max.get_text() );
+ self.axis_y_step = float( self.txt_axis_y_step.get_text() );
+ except:
+ return False
+
+ # Update the poly coefficients
+ if poly != None:
+ if poly.degree == 2:
+ poly.set_coefficient( 2, a2 )
+ poly.set_coefficient( 1, b2 )
+ poly.set_coefficient( 0, c2 )
+ elif poly.degree == 1:
+ poly.set_coefficient( 1, a1 )
+ poly.set_coefficient( 0, b1 )
+ else:
+ return False
+
+ # Eveything is fine
+ return True
+
+ def get_selected_function_iter( self ):
+ tree = self.list_functions
+ select = tree.get_selection()
+ if select == None:
+ return
+ tree, iter = select.get_selected()
+ return iter
+
+ def on_visible_toggled( self, renderer, path, *args ):
+ model = self.list_functions.get_model()
+ iter = model.get_iter( path )
+ if iter == None:
+ return
+ poly = model.get_value( iter, 0 )
+ poly.toggle_visible()
+ self.refresh()
+
+ def on_info_toggled( self, renderer, path, *args ):
+ model = self.list_functions.get_model()
+ iter = model.get_iter( path )
+ if iter == None:
+ return
+ poly = model.get_value( iter, 0 )
+ poly.toggle_info()
+ self.refresh()
+
+ def update_buttons_status( self ):
+ iter = self.get_selected_function_iter()
+ if iter == None:
+ self.btn_delete.set_sensitive( False )
+ self.btn_color.set_sensitive( False )
+ self.btn_animate.set_sensitive( False )
+ else:
+ self.btn_delete.set_sensitive( True )
+ self.btn_color.set_sensitive( True )
+ self.btn_animate.set_sensitive( True )
+
+ def update_all_buttons( self, state ):
+ self.notebook.set_sensitive( state )
+
+ def on_functions_updated( self, tree, *args ):
+ self.update_buttons_status( )
+
+ def on_animate_clicked( self, *args ):
+ self.animate_selected_function()
+ return
+
+ def on_color_clicked( self, *args ):
+ iter = self.get_selected_function_iter()
+ if iter == None:
+ return
+ poly = self.list_functions.get_model().get_value( iter, 0 )
+ old_color = poly.get_color()
+ dlg = gtk.ColorSelectionDialog( _( 'Select a color' ) )
+ ret = dlg.run()
+ if ret == gtk.RESPONSE_OK:
+ color = dlg.colorsel.get_current_color()
+ if color != old_color:
+ poly.set_color( color )
+ self.refresh()
+ dlg.destroy()
+
+ def on_tab_switched( self, notebook, page, page_num ):
+ if page_num == 0:
+ self.refresh()
+
+ def on_delete_clicked( self, *args ):
+ iter = self.get_selected_function_iter()
+ if iter == None:
+ return
+ model = self.list_functions.get_model()
+ model.remove( iter )
+ self.update_buttons_status()
+ self.refresh()
+
+ def on_draw_button_pressed( self, widget, event ):
+ if ( event.button != 1 ):
+ return
+ wnd = self.pixmap
+ gc = self.gc
+ for pt in self.points:
+ if pt.in_window( event.x, event.y ):
+ self.label_info.set_markup( pt.get_markup() )
+ for pto in self.points:
+ pto.set_color( None )
+ pto.draw( wnd, self.gc )
+ pt.set_color( self.marked_point_color )
+ pt.draw( wnd, gc )
+ self.draw.queue_draw()
+ return
+
+ def on_reset_clicked( self, *args ):
+ self.txt_axis_x_min.set_text( '-10' )
+ self.txt_axis_x_max.set_text( '10' )
+ self.txt_axis_x_step.set_text( '1' )
+ self.txt_axis_y_min.set_text( '-10' )
+ self.txt_axis_y_max.set_text( '10' )
+ self.txt_axis_y_step.set_text( '1' )
+
+ def refresh( self ):
+ wnd = self.pixmap
+ gc = self.gc
+ visible_polys = []
+ points = []
+
+ # Get current window dimensions
+ self.width, self.height = self.pixmap.get_size()
+ self.draw.set_size_request( self.width, self.height )
+
+ wnd.draw_rectangle( self.draw.get_style().white_gc, True, 0, 0, self.width, self.height )
+ gc.set_background( self.color_bg )
+ self.points = [ ]
+ self.draw_axis( wnd, gc )
+
+ # Get a list of all visible polynomials
+ model = self.list_functions.get_model()
+ iter = model.get_iter_first()
+ while iter != None:
+ poly = model.get_value( iter, 0 )
+ if poly.get_visible():
+ visible_polys.append( poly )
+ iter = model.iter_next( iter )
+ if self.poly != None:
+ visible_polys.append( self.poly )
+
+ # Get a list of points of interest
+ m = len( visible_polys )
+ i = 0
+ for poly in visible_polys:
+ self.draw_function( wnd, gc, poly )
+ if not poly.info:
+ continue
+
+ points = points + poly.get_points()
+ t = i + 1
+ while t < m:
+ p2 = visible_polys[ t ]
+ if not p2.info:
+ t = t + 1
+ continue
+ points = points + poly.get_common_points( p2 )
+ t = t + 1
+ i = i + 1
+
+ # Now we have the points. Create the data
+ for p in points:
+ x,y = p
+ xi, yi = self.translate_x( x ), self.translate_y( y )
+ pt = Point( xi, yi )
+ pt.set_poly_coords( x, y )
+ pt.draw( wnd, gc )
+ self.points.append( pt )
+
+ self.draw.queue_draw()
+
+ def copy_pixmap( self, pixmap ):
+ width, height = pixmap.get_size()
+ cp = gtk.gdk.Pixmap( pixmap, width, height, -1 )
+ cp.draw_drawable( self.gc, pixmap, 0, 0, 0, 0, width, height )
+ return cp
+
+ def animate( self, animator ):
+ w, h = self.pixmap.get_size()
+ self.pixmap.draw_drawable( self.gc, self.old_pixmap, 0, 0, 0, 0, w, h )
+ self.draw.queue_draw()
+ ret = animator.animate()
+ self.draw.queue_draw()
+ if ret == False:
+ self.pixmap.draw_drawable( self.gc, self.old_pixmap, 0, 0, 0, 0, w, h )
+ self.old_pixmap = None
+ return False
+ return True
+
+ # Some animations
+ def animate_selected_function( self ):
+ # Get the selected polynomial
+ iter = self.get_selected_function_iter()
+ if iter == None:
+ return
+ poly = self.list_functions.get_model().get_value( iter, 0 )
+
+ # Store the pixmap
+ self.old_pixmap = self.copy_pixmap( self.pixmap )
+
+ # Queue the animation
+ axis = ( self.axis_x_min, self.axis_x_max, self.axis_y_min, self.axis_y_max )
+ ani = poly.get_animator( self.draw.window, self.gc, self.pixmap, axis )
+ if ani != None:
+ gobject.timeout_add( 100, self.animate, ani )
+
+ # This is called when the drawing area is created, and every time its
+ # size changes
+ def on_draw_configure( self, widget, event ):
+ x,y,width,height = widget.get_allocation()
+ self.pixmap = gtk.gdk.Pixmap( widget.get_window(), width, height, -1 )
+ self.pixmap.draw_rectangle( widget.get_style().white_gc, True, 0, 0, width, height )
+ if self.gc == None:
+ self.gc = self.pixmap.new_gc( )
+ self.refresh()
+
+ def on_draw_expose( self, widget, event ):
+ x, y, width, height = event.area
+ wnd = widget.window
+ wnd.draw_drawable( self.gc, self.pixmap, x, y, x, y, width, height )
+ return False
+
+ def poly_is_stored( self, poly ):
+ model = self.list_functions.get_model()
+ iter = model.get_iter_first()
+ while iter != None:
+ p = model.get_value( iter, 0 )
+ if p.equals( poly ):
+ return True
+ iter = model.iter_next( iter )
+ return False
+
+ def on_store_clicked( self, *args ):
+ if self.notebook_degree.get_current_page() == 0:
+ poly = Polynomial( 1 )
+ else:
+ poly = Polynomial( 2 )
+ if not self.validate_parameters( poly ):
+ # TODO: Warn about error
+ return
+
+ # Ignore stored polynomials
+ if poly.is_empty() or self.poly_is_stored( poly ):
+ return
+ self.poly = None
+
+ # add to model
+ model = self.list_functions.get_model()
+ model.append( [ poly ] )
+ self.refresh()
+
+ def on_draw_clicked( self, *args ):
+ if self.notebook_degree.get_current_page() == 0:
+ poly = Polynomial( 1 )
+ else:
+ poly = Polynomial( 2 )
+
+ # Get and validate function arguments
+ if not self.validate_parameters( poly ):
+ # TODO: Warn about error
+ return
+
+ if poly.is_empty():
+ return
+ self.poly = poly
+ self.refresh()
+
+ # Draw the axes of the graph
+ def draw_axis( self, wnd, gc ):
+ width = self.width
+ height = self.height
+ x0 = self.translate_x( 0 )
+ y0 = self.translate_y( 0 )
+ w = gc.line_width
+ gc.line_width = 2
+ # y-axis (x=0)
+ wnd.draw_line( gc, x0, 0, x0, height )
+ # x-axis (y=0)
+ wnd.draw_line( gc, 0, y0, width, y0 )
+ gc.line_width = w
+ self.draw_marks( wnd, gc )
+
+ def draw_marks( self, wnd, gc ):
+ x0 = self.translate_x( 0 )
+ y0 = self.translate_y( 0 )
+ width = self.width
+ height = self.height
+ show_numbers = self.toggle_show_axis_numbers.get_active()
+ show_grid = self.toggle_show_grid.get_active()
+ x = 0
+ dec_x = self.spin_axis_x_dec.get_value_as_int()
+ dec_y = self.spin_axis_y_dec.get_value_as_int()
+
+ while x < self.axis_x_max:
+ x = x + self.axis_x_step
+ if x >= self.axis_x_max:
+ break
+ tx = self.translate_x( x )
+ wnd.draw_line( gc, tx, y0 - 2, tx, y0 + 2 )
+ if show_numbers:
+ self.add_axis_number( wnd, gc, x, dec_x, tx - 6, y0 + 4 )
+ if show_grid:
+ wnd.draw_line( gc, tx, 0, tx, height )
+ x = 0
+ while x > self.axis_x_min:
+ x = x - self.axis_x_step
+ if x <= self.axis_x_min:
+ break
+ tx = self.translate_x( x )
+ wnd.draw_line( gc, tx, y0 - 2, tx, y0 + 2 )
+ if show_numbers:
+ self.add_axis_number( wnd, gc, x, dec_x, tx - 6, y0 + 4 )
+ if show_grid:
+ wnd.draw_line( gc, tx, 0, tx, height )
+ y = 0
+ while y < self.axis_y_max:
+ y = y + self.axis_y_step
+ if y >= self.axis_y_max:
+ break
+ ty = self.translate_y( y )
+ wnd.draw_line( gc, x0 - 2, ty, x0 + 2, ty )
+ if show_numbers:
+ self.add_axis_number( wnd, gc, y, dec_y, x0 - 25, ty - 6 )
+ if show_grid:
+ wnd.draw_line( gc, 0, ty, width, ty )
+ y = 0
+ while y > self.axis_y_min:
+ y = y - self.axis_y_step
+ if y <= self.axis_y_min:
+ break
+ ty = self.translate_y( y )
+ wnd.draw_line( gc, x0 - 2, ty, x0 + 2, ty )
+ if show_numbers:
+ self.add_axis_number( wnd, gc, y, dec_y, x0 - 25, ty - 6 )
+ if show_grid:
+ wnd.draw_line( gc, 0, ty, width, ty )
+
+ def add_axis_number( self, wnd, gc, val, dec, x, y ):
+ pl = pango.Layout( self.draw.get_pango_context() )
+ msg = rounded_number( val, dec )
+ pl.set_text( msg )
+ wnd.draw_layout( gc, x, y, pl )
+
+ # Draw the graph of the function
+ def draw_function( self, wnd, gc, poly ):
+ # Setup some graph parameters
+ width = self.width
+ height = self.height
+ x_start = self.axis_x_min
+ x_end = self.axis_x_max
+ step = ( x_end - x_start ) / width
+
+ # Set the line color
+ old_fg = gc.foreground
+ fg = poly.get_color()
+ if fg != None:
+ gc.set_foreground( fg )
+
+ # Erase old point
+ old_x, old_y = ( None, None )
+
+ x = x_start
+ while x < x_end:
+ y = poly.calculate( x )
+ x = x + step
+ self.draw_line( wnd, gc, old_x, old_y, x, y )
+ old_x = x
+ old_y = y
+
+ gc.set_foreground( old_fg )
+
+ def translate_x( self, x ):
+ width = self.width
+ x_min = self.axis_x_min
+ x_max = self.axis_x_max
+ return int( ( width * ( x - x_min ) ) / ( x_max - x_min ) )
+
+ def rev_translate_x( self, x ):
+ width = self.width
+ x_min = self.axis_x_min
+ x_max = self.axis_x_max
+ return x_min + ( x * ( x_max - x_min ) ) / width
+
+ def translate_y( self, y ):
+ height = self.height
+ y_min = self.axis_y_min
+ y_max = self.axis_y_max
+ return int( height - ( height * ( y - y_min ) ) / ( y_max - y_min ) )
+
+ def rev_translate_y( self, y ):
+ height = self.height
+ y_min = self.axis_y_min
+ y_max = self.axis_y_max
+ return y_min + ( ( height - y ) * ( y_max - y_min ) ) / height
+
+ def draw_line( self, wnd, gc, old_x, old_y, x, y ):
+ if old_x == None:
+ wnd.draw_point( gc, self.translate_x( x), self.translate_y( y ) )
+ return
+
+ x0 = self.translate_x( old_x )
+ y0 = self.translate_y( old_y )
+ x1 = self.translate_x( x )
+ y1 = self.translate_y( y )
+ if ( x0 < 0 or x0 > self.width ):
+ return
+ if ( x1 < 0 or x1 > self.width ):
+ return
+ if ( y0 < 0 or y0 > self.height ):
+ return
+ if ( y1 < 0 or y1 > self.height ):
+ return
+
+ wnd.draw_line( gc, x0, y0, x1, y1 )
+
+ def update_coords( self, widget, event ):
+ if self.width == 0 or self.height == 0:
+ return
+ if event.is_hint:
+ x, y, = event.window.pointer
+ state = event.window.pointer_state
+ else:
+ x = event.x
+ y = event.y
+ state = event.state
+ self.draw.get_pointer()
+ x = self.rev_translate_x( x )
+ y = self.rev_translate_y( y )
+ msg = '(%4.2f,%4.2f)' % ( x, y )
+ self.label_coords.set_text( msg )
+
+# Uncomment the following line for stand-alone application
+#Graph( False )