# $Id: Graph2D.py,v 1.9.2.5 2007/01/26 22:51:33 marcusva Exp $
#
# Copyright (c) 2006-2007, Marcus von Appen
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""A 2D graph widget."""
from pygame import K_PLUS, K_KP_PLUS, K_MINUS, K_KP_MINUS, K_DOWN, K_LEFT
from pygame import K_RIGHT, K_UP
from Diagram import Diagram
from ocempgui.draw import Draw
from Constants import *
import base
class Graph2D (Diagram):
"""Graph2D (width, height) -> Graph2D
A widget displaying graphs on a cartesian coordinate plane.
The Graph2D widget displays function graphs on a cartesian
coordinate plane using two axes.
The axes, scale units and units attributes must have at least two
values on assignment to fit both, the horizontal and vertical axes.
self.axes = 't', 'v' # Set velocity related to time.
self.set_axes ('x', 'y') # The default.
# seconds for the time axis, mph for the velocity axis.
self.scale_units = 's', 'mph'
self.set_scale_units ('-', '-') # The default.
# Each second has a distance of 10px to its neighbours and each mph
# has a distance of 25px to its neighbours.
self.units = 10, 25
self.set_units (10, 10) # The default.
Setting the 'orientation' attribute of the Graph2D widget to
ORIENTATION_VERTICAL will cause it to be rotated clockwise by 90
degrees.
The Graph2D can display the names of the axes and their scale units
next to the related axes. This can be adjusted using the
'show_names' attribute and set_show_names() method:
self.show_names = True # Show the names and scale units.
self.set_show_names (False) # Do not show them.
The axes and graph colors can be set to individual values (e.g. to
outline certain function behaviours) through the 'axis_color' and
'graph_color' attributes and their respective methods
set_axis_color() and set_graph_color().
self.axis_color = (0, 255, 0)
self.set_axiscolor ((255, 255, 0))
self.graph_color = (0, 255, 255)
self.set_graph_color (255, 0, 0)
The 'zoom_factor' attribute and set_zoom_factor() method set the zoom
factor for the graph. Values between 0 and 1 will zoom it out while
values greater than 1 will zoom it out.
Note: The zoom factor directly modifies the 'units' attribute values,
which can lead to rounding errors when using floating point zoom factors.
This usually results in a minor discrepancy, when you try to restore the
original values.
# Zoom the x axis out and the y axis in.
self.zoom_factor = 0.5, 2.0
# Zoom both axis in.
self.set_zoom_factor (3.0, 3.0)
Default action (invoked by activate()):
See the Diagram class.
Mnemonic action (invoked by activate_mnemonic()):
See the Diagram class.
Signals:
SIG_KEYDOWN - Invoked, when a key is pressed while the Graph2D has
the input.
SIG_MOUSEDOWN - Invoked, when a mouse button is pressed on the
Graph2D.
SIG_DOUBLECLICKED - Invoked, when a double-click was emitted on the
Graph2D.
Attributes:
show_names - Indicates, whether the axis names and scale units should
be shown or not.
axis_color - The color for both axes.
graph_color - The color for the displayed graph.
zoom_factor - The zoom factor values for both axes.
"""
def __init__ (self, width, height):
Diagram.__init__ (self)
# Some defaults.
self._zoom_factor = (1, 1)
self._units = (20, 20)
self._scaleunits = ("-", "-")
self._axes = ("x", "y")
self._shownames = True
# Colors.
self._axiscolor = (0, 0, 0)
self._graphcolor = (255, 0, 0)
# Signals.
self._signals[SIG_KEYDOWN] = []
self._signals[SIG_MOUSEDOWN] = []
self._signals[SIG_DOUBLECLICKED] = []
self.minsize = width, height
def get_scale_units (self):
"""G.get_scale_units (...) -> None
Gets the scale units of the axes.
"""
return self._scaleunits
def set_scale_units (self, units):
"""G.set_scale_units (...) -> None
Sets the scale units of the axes.
Raises a TypeError, if the passed argument is not a list or tuple.
Raises a ValueError, if the passed argument values are not strings.
"""
if type (units) not in (list, tuple):
raise TypeError ("units must be a list or tuple")
if len (units) < 2:
raise ValueError ("units must at least contains 2 values")
ov = filter (lambda x: type (x) not in (str, unicode), units)
if len (ov) != 0:
raise ValueError ("values in units must be strings or unicode")
self._scaleunits = units
self.dirty = True
def get_units (self):
"""G.set_units (...) -> None
Gets the pixels per unit for dimensioning.
"""
return self._units
def set_units (self, units):
"""G.set_units (...) -> None
Sets the pixels per unit for dimensioning.
Raises a TypeError, if the passed argument is not a list or tuple.
Raises a ValueError, if the passed argument values are not
integers greater than 0.
"""
if type (units) not in (list, tuple):
raise TypeError ("units must be a list or tuple")
if len (units) < 2:
raise ValueError ("units must at least contains 2 values")
ov = filter (lambda x: (type (x) != int) or (x <= 0), units)
if len (ov) != 0:
raise ValueError ("values in units must be positive integers")
self._units = units
self.dirty = True
def get_axes (self):
"""G.get_axes (...) -> None
Gets the amount and names of the axes.
"""
return self._axes
def set_axes (self, axes):
"""G.set_axes (...) -> None
Sets the amount and names of the axes.
Raises a TypeError, if the passed argument is not a list or tuple.
Raises a ValueError, if the passed argument values are not strings.
"""
if type (axes) not in (list, tuple):
raise TypeError ("axes must be a list or tuple")
if len (axes) < 2:
raise ValueError ("axes must at least contains 2 values")
ov = filter (lambda x: type (x) not in (str, unicode), axes)
if len (ov) != 0:
raise ValueError ("values in axes must be strings or unicode")
self._axes = axes
self.dirty = True
def set_show_names (self, var):
"""G.set_show_names (...) -> None
Sets, whether the axis names and scale units should be shown.
If set to True, the names and scale units of both axes will be
displayed besides and beneath the axes in the form 'name / unit'.
"""
self._shownames = var
self.dirty = True
def set_axis_color (self, var):
"""G.set_axis_color (...) -> None
Sets the color of the axes of the coordinate plane.
"""
self._graphcolor = var
self.dirty = True
def set_graph_color (self, var):
"""G.set_graph_color (...) -> None
Sets the color of the graph to draw.
"""
self._graphcolor = var
self.dirty = True
def set_data (self, data):
"""G.set_data (...) -> None
Sets the data to evaluate.
Raises a TypeError, if the passed argument is not a list or tuple.
Raises a ValueError, if the passed argument values are not
integers or floats.
"""
if data != None:
if type (data) not in (list, tuple):
raise TypeError ("data must be a list, tuple or array")
ov = filter (lambda x: type (x) not in (int, float), data)
if len (ov) != 0:
raise ValueError ("vales in data must be integers or float")
Diagram.set_data (self, data)
def set_zoom_factor (self, x, y):
"""G.set_zoom_factor (...) -> None
Zooms the graph in or out by modifying the pixel per units values.
Zooms the graph in or out by modifying the pixel per units
values. Passing 0 as zoom factor will reset the
Raises a TypeError, if the passed arguments are not floats or
integers.
Raises a ValueError, if the passed arguments are smaller than or
equal to 0.
"""
if (type (x) not in (float, int)) or (type (y) not in (float, int)):
raise TypeError ("x and y must floats or integers")
if (x <= 0) or (y <= 0):
raise ValueError ("x and y must be grater than 0")
ux, uy = 0, 0
oldx, oldy = self._zoom_factor
ux = max (1, int ((self._units[0] / oldx) * x))
uy = max (1, int ((self._units[1] / oldy) * y))
self._zoomfactor = (x, y)
if self.units != (ux, uy):
self.units = ux, uy
def zoom_in (self):
""" G.zoom_in () -> None
Zooms into the graph by factor 2.
"""
x, y = self.zoom_factor
self.set_zoom_factor (x * 2.0, y * 2.0)
def zoom_out (self):
"""G.zoom_out () -> None
Zoom out of the graph by factor 2.
"""
x, y = self.zoom_factor
x /= 2.0
y /= 2.0
if x == 0:
x = self.zoom_factor[0]
if y == 0:
y = self.zoom_factor[1]
self.set_zoom_factor (x, y)
def draw_bg (self):
"""G.draw_bg () -> Surface
Draws the Graph2D background surface and returns it.
Creates the visible background surface of the Graph2D and
returns it to the caller.
"""
return base.GlobalStyle.engine.draw_graph2d (self)
def notify (self, event):
"""G.notify (...) -> None
Notifies the Graph2D about an event.
"""
if not self.sensitive:
return
if event.signal in SIGNALS_MOUSE:
eventarea = self.rect_to_client ()
if (event.signal == SIG_MOUSEDOWN) and \
eventarea.collidepoint (event.data.pos):
if event.data.button == 1:
self.focus = True
# Mouse wheel.
elif event.data.button == 4:
self.zoom_out ()
elif event.data.button == 5:
self.zoom_in ()
self.run_signal_handlers (SIG_MOUSEDOWN, event.data)
event.handled = True
elif (event.signal == SIG_DOUBLECLICKED):
eventarea = self.rect_to_client ()
if eventarea.collidepoint (event.data.pos):
# The y origin starts at the bottom left, thus we have
# to invert the y value by using the height.
self.origin = event.data.pos[0] - eventarea.x, \
self.height - event.data.pos[1] + eventarea.y
self.run_signal_handlers (SIG_DOUBLECLICKED, event.data)
event.handled = True
elif self.focus and (event.signal == SIG_KEYDOWN):
# Zoom in and out.
if event.data.key in (K_PLUS, K_KP_PLUS):
self.zoom_in ()
event.handled = True
elif event.data.key in (K_MINUS, K_KP_MINUS):
self.zoom_out ()
event.handled = True
# Axis movement.
elif event.data.key == K_UP:
self.origin = (self.origin[0], self.origin[1] - 5)
event.handled = True
elif event.data.key == K_DOWN:
self.origin = (self.origin[0], self.origin[1] + 5)
event.handled = True
elif event.data.key == K_LEFT:
self.origin = (self.origin[0] + 5, self.origin[1])
event.handled = True
elif event.data.key == K_RIGHT:
self.origin = (self.origin[0] - 5, self.origin[1])
event.handled = True
Diagram.notify (self, event)
def _draw_axes (self, surface, rect, origin, unitx, unity):
"""G._draw_axes (...) -> None
Draws the coordinate axes on the surface.
"""
style = base.GlobalStyle
cls = self.__class__
if self.orientation == ORIENTATION_VERTICAL:
right = (origin[0], rect.bottom)
left = (origin[0], rect.top)
top = (rect.right, origin[1])
bottom = (rect.left, origin[1])
else:
left = (rect.left, origin[1])
right = (rect.right, origin[1])
top = (origin[0], rect.top)
bottom = (origin[0], rect.bottom)
start = None
end = None
scx = 1
scy = 1
# Draw both, positive and negative axes
if self.negative:
Draw.draw_line (surface, self._axiscolor, left, right, 1)
Draw.draw_line (surface, self._axiscolor, bottom, top, 1)
else:
Draw.draw_line (surface, self._axiscolor, origin, right, 1)
Draw.draw_line (surface, self._axiscolor, origin, top, 1)
# Axis names and units.
if self.show_names:
st = "%s / %s " % (self.axes[0], self.scale_units[0])
surface_x = style.engine.draw_string (st, self.state, cls,
self.style)
st = "%s / %s " % (self.axes[1], self.scale_units[1])
surface_y = style.engine.draw_string (st, self.state, cls,
self.style)
rect_sx = surface_x.get_rect()
rext_sy = surface_y.get_rect()
if self.orientation == ORIENTATION_VERTICAL:
surface.blit (surface_x,
(right[0] - 1 - 2 * scx - rect_sx.width,
right[1] - rect_sx.height))
surface.blit (surface_y, (top[0] - rect_sy.width,
top[1] + 1 + 2 * scy))
else:
surface.blit (surface_x, (right[0] - rect_sx.width,
right[1] + 1 + 2 * scx))
surface.blit (surface_y, (top[0] + 1 + 2 * scy, top[1]))
# Draw the scale unit marks.
# From the origin right and up
y = origin[1]
x = origin[0]
while y > rect.top:
start = (origin[0] - scy, y)
end = (origin[0] + scy, y)
Draw.draw_line (surface, self._axiscolor, start, end, 1)
y -= unity
while x < rect.right:
start = (x, origin[1] - scx)
end = (x, origin[1] + scx)
Draw.draw_line (surface, self._axiscolor, start, end, 1)
x += unitx
# From the origin down and left.
if self.negative:
y = origin[1]
while y < rect.bottom:
start = (origin[0] - scy, y)
end = (origin[0] + scy, y)
Draw.draw_line (surface, self._axiscolor, start, end, 1)
y += unity
x = origin[0]
while x > rect.left:
start = (x, origin[1] - scx)
end = (x, origin[1] + scx)
Draw.draw_line (surface, self._axiscolor, start, end, 1)
x -= unitx
def draw (self):
"""W.draw () -> None
Draws the Graph2D surface.
Creates the visible surface of the Graph2D.
"""
Diagram.draw (self)
surface = self.image
rect = surface.get_rect ()
# Orientation swapped?
swap = self.orientation == ORIENTATION_VERTICAL
# Coordinates.
origin = (rect.left + self.origin[0],
rect.bottom - self.origin[1] - 1)
unitx = 1.0
unity = 1.0
if self.units and (len (self.units) > 1):
if self.orientation == ORIENTATION_VERTICAL:
unitx = self.units[1]
unity = self.units[0]
else:
unitx = self.units[0]
unity = self.units[1]
self._draw_axes (surface, rect, origin, unitx, unity)
data = self.data
values = self.values
if data and values:
# Filter negative values.
if not self.negative:
data = filter (lambda x: x >= 0, data)
values = filter (lambda y: y >= 0, values)
# Create the coordinate tuples and take the unit resolution into
# account.
coords = []
org0 = origin[0]
org1 = origin[1]
if self.orientation == ORIENTATION_VERTICAL:
coords = map (lambda x, y: (int (org1 + (x * unitx)),
int (org0 + (y * unity))),
data, values)
else:
coords = map (lambda x, y: (int (org0 + (x * unitx)),
int (org1 - (y * unity))),
data, values)
# Filter non-visible values.
width = self.width
height = self.height
coords = filter (lambda (x, y): (0 < x < width) and \
(0 < y < width), coords)
# Draw them.
color = self.graph_color
setat = surface.set_at
for xy in coords:
setat (xy, color)
show_names = property (lambda self: self._shownames,
lambda self, var: self.set_show_names (var),
doc = "Indicates, whether the axis names should " \
"be shown.")
axis_color = property (lambda self: self._axiscolor,
lambda self, var: self.set_axis_color (var),
doc = "The color of the axes.")
graph_color = property (lambda self: self._graphcolor,
lambda self, var: self.set_graph_color (var),
doc = "The color of the graph.")
zoom_factor = property (lambda self: self._zoom_factor,
lambda self, (x, y): self.set_zoom_factory (x, y),
doc = "Zoom factor for the axes.")
syntax highlighted by Code2HTML, v. 0.9.1