# # $Id: biggles.py,v 1.232 2003/05/12 22:14:41 mrnolta Exp $ # # Copyright (C) 2000-2001 Mike Nolta # # 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 2 # 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, write to the # Free Software Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. # import copy, math, os, string import Numeric import config, _biggles from geometry import * import libplot.renderer renderer = libplot.renderer # miscellaneous --------------------------------------------------------------- _true, _false = 1, 0 def _floor(x): return long(math.floor(x)) def _ceil(x): return long(math.ceil(x)) def _tsil(x): l = list(x) l.reverse() return l def _message( s ): print "biggles:", s def _series( m, n, a=1, b=0 ): return map( lambda x,y=a,z=b: x*y+z, range(m, n+1) ) def _first_not_none( *args ): for arg in args: if arg is not None: return arg return None class _Alias: def __init__( self, *args ): self.__dict__["objs"] = args[:] def __call__( self, *args, **kw ): for obj in self.objs: apply( obj, args, kw ) def __getattr__( self, name ): objs = [] for obj in self.objs: objs.append( getattr(obj, name) ) return apply( _Alias, objs ) def __setattr__( self, name, value ): for obj in self.objs: setattr( obj, name, value ) def __setitem__( self, key, value ): for obj in self.objs: obj[key] = value # BigglesError ---------------------------------------------------------------- class BigglesError( Exception ): pass # relative size --------------------------------------------------------------- def _size_relative( relsize, bbox ): w = bbox.width() h = bbox.height() yardstick = math.sqrt(8) * w * h / (w + h) return (relsize/100.) * yardstick def _fontsize_relative( relsize, bbox, device ): devsize = _size_relative( relsize, bbox ) devsize_min = _size_relative( config.value('default','fontsize_min'), device.bbox ) return max( devsize, devsize_min ) # _PlotContext ----------------------------------------------------------------- class _PlotGeometry: _logfunc = math.log10 _logfunc_vec = Numeric.log10 def __init__( self, src, dest, xlog=0, ylog=0 ): self.src_bbox = src self.dest_bbox = dest self.xlog = xlog self.ylog = ylog a, b = src.lowerleft() c, d = src.upperright() if xlog: a = self._logfunc(a) c = self._logfunc(c) if ylog: b = self._logfunc(b) d = self._logfunc(d) fsrc = BoundingBox( (a,b), (c,d) ) self.aff = RectilinearMap( fsrc, dest ) def __call__( self, x, y ): u, v = x, y if self.xlog: u = self._logfunc(x) if self.ylog: v = self._logfunc(y) return self.aff( u, v ) def call_vec( self, x, y ): u = Numeric.asarray( x ) v = Numeric.asarray( y ) if self.xlog: u = self._logfunc_vec(u) if self.ylog: v = self._logfunc_vec(v) return self.aff.call_vec( u, v ) def geodesic( self, x, y, div=1 ): return [(x, y)] class _PlotContext: def __init__( self, device, dev, data, xlog=0, ylog=0 ): self.draw = device self.dev_bbox = dev self.data_bbox = data self.xlog = xlog self.ylog = ylog self.geom = _PlotGeometry( data, dev, xlog=xlog, ylog=ylog ) self.plot_geom = _PlotGeometry( BoundingBox((0,0),(1,1)), dev ) def do_clip( self ): xr = self.dev_bbox.xrange() yr = self.dev_bbox.yrange() self.draw.set( "cliprect", (xr[0], xr[1], yr[0], yr[1]) ) # _StyleKeywords -------------------------------------------------------------- def _kw_func_relative_fontsize( context, key, value ): device_size = _fontsize_relative( value, context.dev_bbox, context.draw ) context.draw.set( key, device_size ) def _kw_func_relative_size( context, key, value ): device_size = _size_relative( value, context.dev_bbox ) context.draw.set( key, device_size ) def _kw_func_relative_width( context, key, value ): device_width = _size_relative( value/10., context.dev_bbox ) context.draw.set( key, device_width ) class _StyleKeywords: kw_style = None kw_defaults = {} kw_rename = {} kw_func = { 'fontsize' : _kw_func_relative_fontsize, 'linewidth' : _kw_func_relative_width, 'symbolsize' : _kw_func_relative_size, } def kw_init( self, kw=None ): self.kw_style = {} self.kw_style.update( self.kw_defaults ) if kw is not None: for key, value in kw.items(): self.kw_set( key, value ) def kw_set( self, key, value ): if self.kw_style is None: self.kw_init() key = self.kw_rename.get( key, key ) self.kw_style[key] = value def style( self, **kw ): for key,val in kw.items(): self.kw_set( key, val ) def kw_get( self, key, notfound=None ): if self.kw_style is not None: return self.kw_style.get( key, notfound ) else: return None def kw_predraw( self, context ): context.draw.save_state() if self.kw_style is not None: for key, value in self.kw_style.items(): if self.kw_func.has_key(key): method = self.kw_func[key] apply( method, (context,key,value) ) else: context.draw.set( key, value ) def kw_postdraw( self, context ): context.draw.restore_state() # _ConfAttributes ----------------------------------------------------------- class _ConfAttributes: def conf_setattr( self, section, **kw ): import copy, string sec = config.options( section ) if sec is not None: for key,val in sec.items(): x = string.split( key, "." ) obj = self for y in x[:-1]: obj = getattr( obj, y ) setattr( obj, x[-1], copy.copy(val) ) for key,val in kw.items(): setattr( self, key, copy.copy(val) ) # _DeviceObject --------------------------------------------------------------- class _DeviceObject( _StyleKeywords ): def bbox( self, context ): return BoundingBox() def draw( self, context ): raise BigglesError def render( self, context ): self.kw_predraw( context ) self.draw( context ) self.kw_postdraw( context ) class _SymbolObject( _DeviceObject ): kw_rename = { 'type' : 'symboltype', 'size' : 'symbolsize', } def __init__( self, pos, **kw ): self.kw_init( kw ) self.pos = pos def bbox( self, context ): self.kw_predraw( context ) symbolsize = context.draw.get( 'symbolsize' ) self.kw_postdraw( context ) dp = symbolsize/2, symbolsize/2 p = pt_sub( self.pos, dp ) q = pt_add( self.pos, dp ) return BoundingBox( p, q ) def draw( self, context ): context.draw.symbol( self.pos ) class _TextObject( _DeviceObject ): kw_defaults = { 'textangle' : 0, 'texthalign' : 'center', 'textvalign' : 'center', } kw_rename = { 'face' : 'fontface', 'size' : 'fontsize', 'angle' : 'textangle', 'halign' : 'texthalign', 'valign' : 'textvalign', } def __init__( self, pos, str, **kw ): self.kw_init( kw ) self.pos = pos self.str = str __halign_offset = { 'right':(-1,0), 'center':(-.5,.5), 'left':(0,1) } __valign_offset = { 'top':(-1,0), 'center':(-.5,.5), 'bottom':(0,1) } def bbox( self, context ): self.kw_predraw( context ) angle = context.draw.get( 'textangle' ) * math.pi/180. halign = context.draw.get( 'texthalign' ) valign = context.draw.get( 'textvalign' ) width = context.draw.textwidth( self.str ) height = context.draw.textheight( self.str ) self.kw_postdraw( context ) hvec = pt_mul( width, _TextObject.__halign_offset[halign] ) vvec = pt_mul( height, _TextObject.__valign_offset[valign] ) p = self.pos[0] + hvec[0], self.pos[1] + vvec[0] q = self.pos[0] + hvec[1], self.pos[1] + vvec[1] bb = BoundingBox( p, q ) bb.rotate( angle, self.pos ) return bb def draw( self, context ): #bb = self.bbox( context ) #context.draw.rect( bb.lowerleft(), bb.upperright() ) context.draw.text( self.pos, self.str ) class _LabelsObject( _DeviceObject ): kw_defaults = { 'textangle' : 0, 'texthalign' : 'center', 'textvalign' : 'center', } kw_rename = { 'face' : 'fontface', 'size' : 'fontsize', 'angle' : 'textangle', 'halign' : 'texthalign', 'valign' : 'textvalign', } def __init__( self, points, labels, **kw ): self.kw_init( kw ) self.points = points self.labels = labels __halign_offset = { 'right':(-1,0), 'center':(-.5,.5), 'left':(0,1) } __valign_offset = { 'top':(-1,0), 'center':(-.5,.5), 'bottom':(0,1) } def bbox( self, context ): bb = BoundingBox() self.kw_predraw( context ) angle = context.draw.get( 'textangle' ) * math.pi/180. halign = context.draw.get( 'texthalign' ) valign = context.draw.get( 'textvalign' ) height = context.draw.textheight( self.labels[0] ) ho = _LabelsObject.__halign_offset[halign] vo = _LabelsObject.__valign_offset[valign] for i in range(len(self.labels)): pos = self.points[i] width = context.draw.textwidth( self.labels[i] ) p = pos[0] + width * ho[0], pos[1] + height * vo[0] q = pos[0] + width * ho[1], pos[1] + height * vo[1] bb_label = BoundingBox( p, q ) if angle != 0: bb_label.rotate( angle, pos ) bb.union( bb_label ) self.kw_postdraw( context ) return bb def draw( self, context ): for i in range(len(self.labels)): context.draw.text( self.points[i], self.labels[i] ) class _LineTextObject( _TextObject ): kw_rename = { 'face' : 'fontface', 'size' : 'fontsize', } def __init__( self, p, q, str, offset, **kw ): self.kw_init( kw ) self.str = str midpoint = pt_mul( 0.5, pt_add(p, q) ) direction = pt_unit( pt_sub(q, p) ) angle = pt_angle( direction ) direction = pt_rot( direction, math.pi/2 ) self.pos = pt_add( midpoint, pt_mul(offset, direction) ) self.kw_set( 'textangle', angle * 180./math.pi ) self.kw_set( 'texthalign', 'center' ) if offset > 0: self.kw_set( 'textvalign', 'bottom' ) else: self.kw_set( 'textvalign', 'top' ) class _LineObject( _DeviceObject ): kw_rename = { 'width' : 'linewidth', 'type' : 'linetype', } def __init__( self, p, q, **kw ): self.kw_init( kw ) self.p = p self.q = q def bbox( self, context ): return BoundingBox( self.p, self.q ) def draw( self, context ): context.draw.line( self.p, self.q ) class _PolygonObject( _DeviceObject ): kw_rename = { 'width' : 'linewidth', 'type' : 'linetype', } def __init__( self, points, **kw ): self.kw_init( kw ) self.points = points def bbox( self, context ): return apply( BoundingBox, self.points ) def draw( self, context ): context.draw.polygon( self.points ) class _PathObject( _DeviceObject ): kw_rename = { 'width' : 'linewidth', 'type' : 'linetype', } def __init__( self, x, y, **kw ): self.kw_init( kw ) self.x = x self.y = y def bbox( self, context ): xmin, xmax = _biggles.range( self.x ) ymin, ymax = _biggles.range( self.y ) return BoundingBox( (xmin,ymin), (xmax,ymax) ) def draw( self, context ): context.draw.curve( self.x, self.y ) class _SymbolsObject( _DeviceObject ): kw_rename = { 'type' : 'symboltype', 'size' : 'symbolsize', } def __init__( self, x, y, **kw ): self.kw_init( kw ) self.x = x self.y = y def bbox( self, context ): xmin, xmax = _biggles.range( self.x ) ymin, ymax = _biggles.range( self.y ) return BoundingBox( (xmin,ymin), (xmax,ymax) ) def draw( self, context ): context.draw.symbols( self.x, self.y ) class _ColoredSymbolsObject( _DeviceObject ): kw_rename = { 'type' : 'symboltype', 'size' : 'symbolsize', } def __init__( self, x, y, c, **kw ): self.kw_init( kw ) self.x = x self.y = y self.c = c def bbox( self, context ): xmin, xmax = _biggles.range( self.x ) ymin, ymax = _biggles.range( self.y ) return BoundingBox( (xmin,ymin), (xmax,ymax) ) def draw( self, context ): context.draw.colored_symbols( self.x, self.y, self.c ) class _DensityObject( _DeviceObject ): kw_rename = { } def __init__( self, densgrid, ((xmin,ymin), (xmax,ymax)), **kw ): self.kw_init( kw ) self.densgrid = densgrid self.extent = ( (xmin,ymin), (xmax,ymax) ) def bbox( self, context ): return apply( BoundingBox, self.extent ) def draw( self, context ): #from Numeric import rank #if rank(self.densgrid) == 3: if len(self.densgrid.shape) == 3: context.draw.color_density_plot( self.densgrid, self.extent ) else: context.draw.density_plot( self.densgrid, self.extent ) class _EllipseObject( _DeviceObject ): def __init__( self, p, rx, ry, angle=0., **kw ): self.kw_init( kw ) self.p = p self.rx = rx self.ry = ry self.angle = angle def bbox( self, context ): r = self.rx, self.ry p = pt_add( self.p, r ) q = pt_sub( self.p, r ) bb = BoundingBox( p, q ) bb.rotate( self.p, self.angle ) return bb def draw( self, context ): context.draw.ellipse( self.p, self.rx, self.ry, self.angle ) class _CombObject( _DeviceObject ): def __init__( self, points, dp, **kw ): self.kw_init( kw ) self.points = points self.dp = dp def bbox( self, context ): return apply( BoundingBox, self.points ) def draw( self, context ): for p in self.points: context.draw.move( p ) context.draw.linetorel( self.dp ) class _BoxObject( _DeviceObject ): def __init__( self, p, q, **kw ): self.kw_init( kw ) self.p = p self.q = q def bbox( self, context ): return BoundingBox( self.p, self.q ) def draw( self, context ): context.draw.rect( self.p, self.q ) class _ArcObject( _DeviceObject ): def __init__( self, pc, p0, p1, **kw ): self.kw_init( kw ) self.pc = pc self.p0 = p0 self.p1 = p1 def bbox( self, context ): return BoundingBox( self.pc, self.p0, self.p1 ) def draw( self, context ): context.draw.arc( self.pc, self.p0, self.p1 ) # _PlotComponent -------------------------------------------------------------- class _PlotComponent( _StyleKeywords, _ConfAttributes ): def __init__( self ): self.clear() def add( self, *args ): for obj in args: self.device_objects.append( obj ) def limits( self ): return BoundingBox() def clear( self ): self.device_objects = [] def make( self, context ): raise BigglesError def make_key( self, bbox ): pass def bbox( self, context ): self.clear() self.make( context ) bb = BoundingBox() for obj in self.device_objects: bb.union( obj.bbox(context) ) return bb def render( self, context ): self.clear() self.make( context ) self.kw_predraw( context ) for obj in self.device_objects: obj.render( context ) self.kw_postdraw( context ) # _LabelComponent ------------------------------------------------------------- class _LabelComponent( _PlotComponent ): kw_rename = { 'face' : 'fontface', 'size' : 'fontsize', 'angle' : 'textangle', 'halign' : 'texthalign', 'valign' : 'textvalign', } def __init__( self, x, y, str, **kw ): _PlotComponent.__init__( self ) self.conf_setattr( "_LabelComponent" ) self.kw_init( kw ) self.pos = x, y self.str = str def limits( self ): return BoundingBox() class DataLabel( _LabelComponent ): def make( self, context ): pos = apply( context.geom, self.pos ) t = apply( _TextObject, (pos, self.str), self.kw_style ) self.add( t ) class PlotLabel( _LabelComponent ): def make( self, context ): pos = apply( context.plot_geom, self.pos ) t = apply( _TextObject, (pos, self.str), self.kw_style ) self.add( t ) # _LineComponent -------------------------------------------------------------- class _LineComponent( _PlotComponent ): def __init__( self ): _PlotComponent.__init__( self ) self.conf_setattr( "_LineComponent" ) kw_rename = { 'color' : 'linecolor', 'width' : 'linewidth', 'type' : 'linetype', } def make_key( self, bbox ): xr = bbox.xrange() y = bbox.center()[1] p = xr[0], y q = xr[1], y return apply( _LineObject, (p,q), self.kw_style ) class Curve( _LineComponent ): def __init__( self, x, y, **kw ): _LineComponent.__init__( self ) self.conf_setattr( "Curve" ) self.kw_init( kw ) self.x = x self.y = y def limits( self ): p0 = min(self.x), min(self.y) p1 = max(self.x), max(self.y) return BoundingBox( p0, p1 ) def make( self, context ): segs = context.geom.geodesic( self.x, self.y ) for seg in segs: x, y = context.geom.call_vec( seg[0], seg[1] ) self.add( _PathObject(x, y) ) class DataLine( _LineComponent ): def __init__( self, p, q, **kw ): _LineComponent.__init__( self ) self.conf_setattr( "DataLine" ) self.kw_init( kw ) self.p = p self.q = q def limits( self ): return BoundingBox( self.p, self.q ) def make( self, context ): a = apply( context.geom, self.p ) b = apply( context.geom, self.q ) self.add( _LineObject(a, b) ) class Geodesic( _LineComponent ): def __init__( self, p, q, **kw ): _LineComponent.__init__( self ) self.conf_setattr( "Geodesic" ) self.kw_init( kw ) self.p = p self.q = q def limits( self ): return BoundingBox( self.p, self.q ) def make( self, context ): l = self.p[0], self.q[0] b = self.p[1], self.q[1] segs = context.geom.geodesic( l, b, self.divisions ) for seg in segs: x, y = context.geom.call_vec( seg[0], seg[1] ) self.add( _PathObject(x, y) ) class Histogram( _LineComponent ): def __init__( self, values, x0=0, binsize=1, **kw ): _LineComponent.__init__( self ) self.conf_setattr( "Histogram" ) self.kw_init( kw ) self.values = values self.x0 = x0 self.binsize = binsize def limits( self ): nval = len( self.values ) if self.drop_to_zero: p = self.x0, min( 0, min(self.values) ) else: p = self.x0, min(self.values) q = self.x0 + nval*self.binsize, max(self.values) return BoundingBox( p, q ) def make( self, context ): nval = len( self.values ) x = [] y = [] if self.drop_to_zero: x.append( self.x0 ) y.append( 0 ) for i in range(0,nval): xi = self.x0 + i * self.binsize yi = self.values[i] x.extend( [xi, xi + self.binsize] ) y.extend( [yi, yi] ) if self.drop_to_zero: x.append( self.x0 + nval*self.binsize ) y.append( 0 ) u, v = context.geom.call_vec( x, y ) self.add( _PathObject(u, v) ) class LineX( _LineComponent ): def __init__( self, x, **kw ): _LineComponent.__init__( self ) self.conf_setattr( "LineX" ) self.kw_init( kw ) self.x = x def limits( self ): return BoundingBox( (self.x,None), (self.x,None) ) def make( self, context ): yrange = context.data_bbox.yrange() p = self.x, yrange[0] q = self.x, yrange[1] a = apply( context.geom, p ) b = apply( context.geom, q ) self.add( _LineObject(a, b) ) class LineY( _LineComponent ): def __init__( self, y, **kw ): _LineComponent.__init__( self ) self.conf_setattr( "LineY" ) self.kw_init( kw ) self.y = y def limits( self ): return BoundingBox( (None,self.y), (None,self.y) ) def make( self, context ): xrange = context.data_bbox.xrange() p = xrange[0], self.y q = xrange[1], self.y a = apply( context.geom, p ) b = apply( context.geom, q ) self.add( _LineObject(a, b) ) class PlotLine( _LineComponent ): def __init__( self, p, q, **kw ): _LineComponent.__init__( self ) self.conf_setattr( "PlotLine" ) self.kw_init( kw ) self.p = p self.q = q def make( self, context ): a = apply( context.plot_geom, self.p ) b = apply( context.plot_geom, self.q ) self.add( _LineObject(a, b) ) class Slope( _LineComponent ): def __init__( self, slope, intercept=None, **kw ): _LineComponent.__init__( self ) self.conf_setattr( "Slope" ) self.kw_init( kw ) self.slope = slope self.intercept = intercept if intercept is None: self.intercept = (0.,0.) def _x( self, y ): x0, y0 = self.intercept return x0 + float(y - y0) / self.slope def _y( self, x ): x0, y0 = self.intercept return y0 + (x - x0) * self.slope def make( self, context ): xrange = context.data_bbox.xrange() yrange = context.data_bbox.yrange() if self.slope == 0: l = [ ( xrange[0], self.intercept[1] ),\ ( xrange[1], self.intercept[1] ) ] else: l = [ ( xrange[0], self._y(xrange[0]) ),\ ( xrange[1], self._y(xrange[1]) ),\ ( self._x(yrange[0]), yrange[0] ),\ ( self._x(yrange[1]), yrange[1] ) ] m = filter( context.data_bbox.contains, l ) m.sort() if len(m) > 1: a = apply( context.geom, m[0] ) b = apply( context.geom, m[-1] ) self.add( _LineObject(a, b) ) class DataBox( _LineComponent ): def __init__( self, p, q, **kw ): _LineComponent.__init__( self ) self.conf_setattr( "DataBox" ) self.kw_init( kw ) self.p = p self.q = q def limits( self ): return BoundingBox( self.p, self.q ) def make( self, context ): a = apply( context.geom, self.p ) b = apply( context.geom, self.q ) self.add( _BoxObject(a, b) ) class PlotBox( _LineComponent ): def __init__( self, p, q, **kw ): _LineComponent.__init__( self ) self.conf_setattr( "PlotBox" ) self.kw_init( kw ) self.p = p self.q = q def make( self, context ): a = apply( context.plot_geom, self.p ) b = apply( context.plot_geom, self.q ) self.add( _BoxObject(a, b) ) class PlotArc( _LineComponent ): def __init__( self, pc, r, a0, a1, **kw ): _LineComponent.__init__( self ) self.conf_setattr( "PlotArc" ) self.kw_init( kw ) self.pc = pc self.p0 = p[0] + r*math.cos(a0), p[1] + r*math.sin(a0) self.p1 = p[0] + r*math.cos(a1), p[1] + r*math.sin(a1) def make( self, context ): pc = apply( context.plot_geom, self.pc ) p0 = apply( context.plot_geom, self.p0 ) p1 = apply( context.plot_geom, self.p1 ) self.add( _ArcObject(pc, p0, p1) ) class DataArc( _LineComponent ): def __init__( self, pc, r, a0, a1, **kw ): _LineComponent.__init__( self ) self.conf_setattr( "DataArc" ) self.kw_init( kw ) self.pc = pc self.p0 = p[0] + r*math.cos(a0), p[1] + r*math.sin(a0) self.p1 = p[0] + r*math.cos(a1), p[1] + r*math.sin(a1) def limits( self ): return BoundingBox( self.pc, self.p0, self.p1 ) def make( self, context ): pc = apply( context.geom, self.pc ) p0 = apply( context.geom, self.p0 ) p1 = apply( context.geom, self.p1 ) self.add( _ArcObject(pc, p0, p1) ) # _SymbolDataComponent -------------------------------------------------------- class _SymbolDataComponent( _PlotComponent ): kw_rename = { 'type' : 'symboltype', 'size' : 'symbolsize', } def make_key( self, bbox ): pos = bbox.center() return apply(_SymbolObject, (pos,), self.kw_style) class Points( _SymbolDataComponent ): kw_defaults = { 'symboltype' : config.value('Points','symboltype'), 'symbolsize' : config.value('Points','symbolsize'), } def __init__( self, x, y, **kw ): _SymbolDataComponent.__init__( self ) self.conf_setattr( "Points" ) self.kw_init( kw ) self.x = x self.y = y def limits( self ): p = min(self.x), min(self.y) q = max(self.x), max(self.y) return BoundingBox( p, q ) def make( self, context ): x, y = context.geom.call_vec( self.x, self.y ) self.add( _SymbolsObject(x, y) ) def Point( x, y, **kw ): return apply( Points, ([x],[y]), kw ) class ColoredPoints( _SymbolDataComponent ): kw_defaults = { 'symboltype' : config.value('Points','symboltype'), 'symbolsize' : config.value('Points','symbolsize'), } def __init__( self, x, y, c=None, **kw ): _SymbolDataComponent.__init__( self ) self.conf_setattr( "Points" ) self.kw_init( kw ) self.x = x self.y = y self.c = c def limits( self ): p = min(self.x), min(self.y) q = max(self.x), max(self.y) return BoundingBox( p, q ) def make( self, context ): x, y = context.geom.call_vec( self.x, self.y ) self.add( _ColoredSymbolsObject(x, y, self.c) ) def ColoredPoint( x, y, **kw ): return apply( ColoredPoints, ([x],[y]), kw ) # _DensityComponent ----------------------------------------------------------- class Density( _PlotComponent ): kw_defaults = { 'foo' : config.value('Points','symbolsize'), } def __init__( self, densgrid, ((xmin,ymin), (xmax,ymax)), **kw ): _PlotComponent.__init__( self ) self.conf_setattr( "Density" ) self.kw_init( kw ) self.densgrid = densgrid self.extent = ((xmin,ymin), (xmax,ymax)) def limits( self ): return apply( BoundingBox, self.extent ) def make( self, context ): (x0,y0),(x1,y1) = self.extent (x0,x1),(y0,y1) = context.geom.call_vec((x0,x1),(y0,y1)) self.add( _DensityObject(self.densgrid, ((x0,y0),(x1,y1))) ) # _FillComponent -------------------------------------------------------------- class _FillComponent( _PlotComponent ): kw_defaults = { 'color' : config.value('_FillComponent','fillcolor'), 'filltype' : config.value('_FillComponent','filltype'), } def make_key( self, bbox ): p = bbox.lowerleft() q = bbox.upperright() return apply( _BoxObject, (p,q), self.kw_style ) class FillAbove( _FillComponent ): def __init__( self, x, y, **kw ): _FillComponent.__init__( self ) self.conf_setattr( "FillAbove" ) self.kw_init( kw ) self.x = x self.y = y def limits( self ): p = min(self.x), min(self.y) q = max(self.x), max(self.y) return BoundingBox( p, q ) def make( self, context ): coords = map( context.geom, self.x, self.y ) max_y = context.data_bbox.yrange()[1] coords.append( context.geom(self.x[-1], max_y) ) coords.append( context.geom(self.x[0], max_y) ) self.add( _PolygonObject(coords) ) class FillBelow( _FillComponent ): def __init__( self, x, y, **kw ): _FillComponent.__init__( self ) self.conf_setattr( "FillBelow" ) self.kw_init( kw ) self.x = x self.y = y def limits( self ): p = min(self.x), min(self.y) q = max(self.x), max(self.y) return BoundingBox( p, q ) def make( self, context ): coords = map( context.geom, self.x, self.y ) min_y = context.data_bbox.yrange()[0] coords.append( context.geom(self.x[-1], min_y) ) coords.append( context.geom(self.x[0], min_y) ) self.add( _PolygonObject(coords) ) class FillBetween( _FillComponent ): def __init__( self, x1, y1, x2, y2, **kw ): _FillComponent.__init__( self ) self.conf_setattr( "FillBetween" ) self.kw_init( kw ) self.x1, self.y1 = x1, y1 self.x2, self.y2 = x2, y2 def limits( self ): min_x = min( min(self.x1), min(self.x2) ) max_x = max( max(self.x1), max(self.x2) ) min_y = min( min(self.y1), min(self.y2) ) max_y = max( max(self.y1), max(self.y2) ) return BoundingBox( (min_x,min_y), (max_x,max_y) ) def make( self, context ): x = list(self.x1) + _tsil(self.x2) y = list(self.y1) + _tsil(self.y2) coords = map( context.geom, x, y ) self.add( _PolygonObject(coords) ) # ErrorBars ------------------------------------------------------------------- class _ErrorBar( _PlotComponent ): kw_rename = { 'color' : 'linecolor', 'width' : 'linewidth', 'type' : 'linetype', } def __init__( self ): _PlotComponent.__init__( self ) self.conf_setattr( "_ErrorBar" ) class ErrorBarsX( _ErrorBar ): def __init__( self, y, lo, hi, **kw ): _ErrorBar.__init__( self ) self.conf_setattr( "ErrorBarsX" ) self.kw_init( kw ) self.y = y self.lo = lo self.hi = hi def limits( self ): p = min( min(self.lo), min(self.hi) ), min(self.y) q = max( max(self.lo), max(self.hi) ), max(self.y) return BoundingBox( p, q ) def make( self, context ): l = _size_relative( self.barsize, context.dev_bbox ) for i in range(len(self.y)): p = context.geom( self.lo[i], self.y[i] ) q = context.geom( self.hi[i], self.y[i] ) l0 = _LineObject( p, q ) l1 = _LineObject( (p[0],p[1]-l), (p[0],p[1]+l) ) l2 = _LineObject( (q[0],q[1]-l), (q[0],q[1]+l) ) self.add( l0, l1, l2 ) class ErrorBarsY( _ErrorBar ): def __init__( self, x, lo, hi, **kw ): _ErrorBar.__init__( self ) self.conf_setattr( "ErrorBarsY" ) self.kw_init( kw ) self.x = x self.lo = lo self.hi = hi def limits( self ): p = min(self.x), min( min(self.lo), min(self.hi) ) q = max(self.x), max( max(self.lo), max(self.hi) ) return BoundingBox( p, q ) def make( self, context ): l = _size_relative( self.barsize, context.dev_bbox ) for i in range(len(self.x)): p = context.geom( self.x[i], self.lo[i] ) q = context.geom( self.x[i], self.hi[i] ) l0 = _LineObject( p, q ) l1 = _LineObject( (p[0]-l,p[1]), (p[0]+l,p[1]) ) l2 = _LineObject( (q[0]-l,q[1]), (q[0]+l,q[1]) ) self.add( l0, l1, l2 ) def SymmetricErrorBarsX( x, y, err, **kw ): import operator xlo = map( operator.sub, x, err ) xhi = map( operator.add, x, err ) return apply( ErrorBarsX, (y, xlo, xhi), kw ) def SymmetricErrorBarsY( x, y, err, **kw ): import operator ylo = map( operator.sub, y, err ) yhi = map( operator.add, y, err ) return apply( ErrorBarsY, (x, ylo, yhi), kw ) # Limits ---------------------------------------------------------------------- class _ErrorLimit( _PlotComponent ): kw_rename = { 'color' : 'linecolor', 'width' : 'linewidth', 'type' : 'linetype', } def __init__( self ): _PlotComponent.__init__( self ) self.conf_setattr( "_ErrorLimit" ) class UpperLimits( _ErrorLimit ): def __init__( self, x, ulimit, **kw ): _ErrorLimit.__init__( self ) self.conf_setattr( "UpperLimits" ) self.kw_init( kw ) self.x = x self.ulimit = ulimit def limits( self ): p = min(self.x), min(self.ulimit) q = max(self.x), max(self.ulimit) return BoundingBox( p, q ) def make( self, context ): l = _size_relative( self.size, context.dev_bbox ) for i in range(len(self.x)): p = context.geom( self.x[i], self.ulimit[i] ) l1 = _LineObject( (p[0]-l,p[1]), (p[0]+l,p[1]) ) l2 = _LineObject( (p[0],p[1]-2*l), (p[0],p[1]) ) l3 = _LineObject( (p[0],p[1]-2*l), (p[0]+l,p[1]-l) ) l4 = _LineObject( (p[0],p[1]-2*l), (p[0]-l,p[1]-l) ) self.add( l1, l2, l3, l4 ) class LowerLimits( _ErrorLimit ): def __init__( self, x, llimit, **kw ): _ErrorLimit.__init__( self ) self.conf_setattr( "UpperLimits" ) self.kw_init( kw ) self.x = x self.llimit = llimit def limits( self ): p = min(self.x), min(self.llimit) q = max(self.x), max(self.llimit) return BoundingBox( p, q ) def make( self, context ): l = _size_relative( self.size, context.dev_bbox ) for i in range(len(self.x)): p = context.geom( self.x[i], self.llimit[i] ) l1 = _LineObject( (p[0]-l,p[1]), (p[0]+l,p[1]) ) l2 = _LineObject( (p[0],p[1]+2*l), (p[0],p[1]) ) l3 = _LineObject( (p[0],p[1]+2*l), (p[0]+l,p[1]+l) ) l4 = _LineObject( (p[0],p[1]+2*l), (p[0]-l,p[1]+l) ) self.add( l1, l2, l3, l4 ) # Ellipses -------------------------------------------------------------------- class Ellipses( _PlotComponent ): kw_rename = { 'color' : 'linecolor', 'width' : 'linewidth', 'type' : 'linetype', } def __init__( self, x, y, rx, ry, angle=None, **kw ): _PlotComponent.__init__( self ) self.kw_init( kw ) self.x = x self.y = y self.rx = rx self.ry = ry self.angle = angle def limits( self ): # XXX:kludge minx, maxx = self.x[0], self.x[0] miny, maxy = self.y[0], self.y[0] for i in range(len(self.x)): r = max( self.rx[i], self.ry[i] ) minx = min( minx, self.x[i]-r ) miny = min( miny, self.y[i]-r ) maxx = max( maxx, self.x[i]+r ) maxy = max( maxy, self.y[i]+r ) return BoundingBox( (minx,miny), (maxx,maxy) ) def make( self, context ): for i in range(len(self.x)): p = context.geom( self.x[i], self.y[i] ) r = context.geom( \ self.x[i] + self.rx[i], \ self.y[i] + self.ry[i] ) rx, ry = pt_sub( r, p ) if self.angle is not None: e = _EllipseObject( p, rx, ry, self.angle[i] ) else: e = _EllipseObject( p, rx, ry ) self.add( e ) def Ellipse( x, y, rx, ry, angle=None, **kw ): if angle is None: args = ([x],[y],[rx],[ry]) else: args = ([x],[y],[rx],[ry],[angle]) return apply( Ellipses, args, kw ) def Circles( x, y, r, **kw ): return apply( Ellipses, (x,y,r,r), kw ) def Circle( x, y, r, **kw ): return apply( Circles, ([x],[y],[r]), kw ) # _PlotKey -------------------------------------------------------------------- class PlotKey( _PlotComponent ): kw_rename = { 'face' : 'fontface', 'size' : 'fontsize', 'angle' : 'textangle', 'halign' : 'texthalign', 'valign' : 'textvalign', } def __init__( self, x, y, components, **kw ): _PlotComponent.__init__( self ) self.conf_setattr( "PlotKey" ) self.kw_init( kw ) self.x = x self.y = y self.components = components def make( self, context ): key_pos = context.plot_geom( self.x, self.y ) key_width = _size_relative( self.key_width, context.dev_bbox ) key_height = _size_relative( self.key_height, context.dev_bbox ) key_hsep = _size_relative( self.key_hsep, context.dev_bbox ) key_vsep = _size_relative( self.key_vsep, context.dev_bbox ) halign = self.kw_get( 'texthalign' ) if halign == 'left': text_pos = pt_add( (key_width/2+key_hsep,0), key_pos ) else: text_pos = pt_add( (-key_width/2-key_hsep,0), key_pos ) bbox = BoundingBox( (-key_width/2,-key_height/2), (key_width/2,key_height/2) ) bbox.shift( key_pos ) dp = 0, -(key_vsep + key_height) for comp in self.components: try: obj,str = comp except: obj = comp str = getattr( comp, "label", "" ) t = apply( _TextObject, (text_pos,str), self.kw_style ) self.add( t, obj.make_key(bbox) ) text_pos = pt_add( text_pos, dp ) bbox.shift( dp ) # XXX:deprecated def OldKey( x, y, labels, align='left', **kw ): kw['texthalign'] = align return apply( PlotKey, (x,y,labels), kw ) # _HalfAxis ------------------------------------------------------------------- def _magform( x ): "Given x, returns (a,b), where x = a*10^b [a >= 1., b integral]." if x == 0: return 0., 0 a, b = math.modf(math.log10(abs(x))) a, b = math.pow(10,a), int(b) if a < 1.: a, b = a * 10, b - 1 if x < 0.: a = -a return a, b def _format_ticklabel( x, range=0. ): if x == 0: return "0" a, b = _magform( x ) if abs(b) > 4: if a == 1.: return r"$10^{%d}$" % b elif a == -1.: return r"-$10^{%d}$" % b else: return r"$%g\times 10^{%d}$" % (a,b) if range < 1e-6: a, b = _magform( range ) return "%.*f" % (abs(b),x) return "%g" % x def _ticklist_linear( lo, hi, sep, origin=0. ): r = [] a = _ceil(float(lo - origin)/float(sep)) b = _floor(float(hi - origin)/float(sep)) #for i in range( a, b+1 ): # r.append( origin + i * sep ) r0 = origin + a*sep for i in range( b-a+1 ): r.append( r0 + i*sep ) return r def _pow10(x): return math.pow(10,x) def _log10(x): return math.log10(x) def _ticks_default_linear( lim ): a, b = _magform( (lim[1] - lim[0])/5. ) if a < (1 + 2)/2.: x = 1 elif a < (2 + 5)/2.: x = 2 elif a < (5 + 10)/2.: x = 5 else: x = 10 major_div = x * math.pow(10, b) return _ticklist_linear( lim[0], lim[1], major_div ) def _ticks_default_log( lim ): log_lim = _log10(lim[0]), _log10(lim[1]) nlo = _ceil( math.log10(lim[0]) ) nhi = _floor( math.log10(lim[1]) ) nn = nhi - nlo +1 if nn >= 10: return map( _pow10, _ticks_default_linear(log_lim) ) elif nn >= 2: return map( _pow10, range(nlo, nhi+1) ) else: return _ticks_default_linear( lim ) def _ticks_num_linear( lim, num ): ticks = [] a = lim[0] b = (lim[1] - lim[0])/float(num-1) for i in range(num): ticks.append( a + i*b ) return ticks def _ticks_num_log( lim, num ): ticks = [] a = math.log10(lim[0]) b = (math.log10(lim[1]) - a)/float(num - 1) for i in range(num): ticks.append( a + i*b ) return map( _pow10, ticks ) def _subticks_linear( lim, ticks, num=None ): major_div = (ticks[-1] - ticks[0])/float(len(ticks) - 1) if num is None: _num = 4 a, b = _magform( major_div ) if 1. < a < (2 + 5)/2.: _num = 3 else: _num = num minor_div = major_div/float(_num+1) return _ticklist_linear( lim[0], lim[1], minor_div, ticks[0] ) def _subticks_log( lim, ticks, num=None ): log_lim = _log10(lim[0]), _log10(lim[1]) nlo = _ceil( math.log10(lim[0]) ) nhi = _floor( math.log10(lim[1]) ) nn = nhi - nlo +1 if nn >= 10: return map( _pow10, _subticks_linear(log_lim, map(_log10,ticks), num) ) elif nn >= 2: minor_ticks = [] for i in range(nlo-1,nhi+1): for j in range(1,10): z = j * _pow10(i) if lim[0] <= z and z <= lim[1]: minor_ticks.append(z) return minor_ticks else: return _subticks_linear( lim, ticks, num ) class _Group: def __init__( self, objs ): self.objs = objs[:] def bbox( self, context ): bb = BoundingBox() for obj in self.objs: bb.union( obj.bbox(context) ) return bb class _HalfAxis( _PlotComponent ): func_ticks_default = _ticks_default_linear, _ticks_default_log func_ticks_num = _ticks_num_linear, _ticks_num_log func_subticks_default = _subticks_linear, _subticks_log func_subticks_num = _subticks_linear, _subticks_log _attr_map = { 'labeloffset' : 'label_offset', 'major_ticklabels' : 'ticklabels', 'major_ticks' : 'ticks', 'minor_ticks' : 'subticks', } def __init__( self, **kw ): _PlotComponent.__init__( self ) self.kw_init( kw ) self.conf_setattr( "_HalfAxis" ) def __getattr__( self, name ): return self.__dict__[ self._attr_map.get(name,name) ] def __setattr__( self, name, value ): self.__dict__[ self._attr_map.get(name,name) ] = value def _ticks( self, context ): log = self._log( context ) _range = self._range( context ) if self.ticks is None: return self.func_ticks_default[log]( _range ) elif type(self.ticks) == type(0): return self.func_ticks_num[log]( _range, self.ticks ) else: return self.ticks def _subticks( self, context, ticks ): log = self._log( context ) _range = self._range( context ) if self.subticks is None: return self.func_subticks_default[log]( _range, ticks ) elif type(self.subticks) == type(0): return self.func_subticks_num[log]( \ _range, ticks, self.subticks ) else: return self.subticks def _ticklabels( self, context, ticks ): if self.ticklabels is None: range = [ max(ticks) - min(ticks) ] * len(ticks) return map( _format_ticklabel, ticks, range ) else: return self.ticklabels def _make_ticklabels( self, context, pos, labels ): if labels is None or not len(labels) > 0: return dir = self.ticklabels_dir offset = _size_relative( self.ticklabels_offset, \ context.dev_bbox ) if self.draw_ticks and self.tickdir > 0: offset = offset + _size_relative( \ self.ticks_size, context.dev_bbox ) labelpos = [] for i in range(len(labels)): labelpos.append( \ self._pos(context, pos[i], dir*offset) ) halign, valign = self._align() style = { "halign" : halign, "valign" : valign } style.update( self.ticklabels_style ) l = apply( _LabelsObject, (labelpos, labels), style ) self.add( l ) def _make_spine( self, context ): a, b = self._range( context ) p = self._pos( context, a ) q = self._pos( context, b ) self.add( apply(_LineObject, (p, q), self.spine_style) ) def _make_ticks( self, context, ticks, size, style ): if ticks is None or not len(ticks) > 0: return dir = self.tickdir * self.ticklabels_dir ticklen = self._dpos( dir *\ _size_relative(size, context.dev_bbox) ) tickpos = [] for tick in ticks: tickpos.append( self._pos(context, tick) ) self.add( apply(_CombObject, (tickpos, ticklen), style) ) def make( self, context ): if self.draw_nothing: return ticks = self._ticks( context ) subticks = self._subticks( context, ticks ) ticklabels = self._ticklabels( context, ticks ) implicit_draw_subticks = self.draw_subticks is None and \ self.draw_ticks implicit_draw_ticklabels = self.draw_ticklabels is None and \ (self.range is not None or self.ticklabels is not None) if self.draw_grid: self._make_grid( context, ticks ) if self.draw_axis: if self.draw_subticks or implicit_draw_subticks: self._make_ticks( context, subticks, \ self.subticks_size, \ self.subticks_style ) if self.draw_ticks: self._make_ticks( context, ticks, \ self.ticks_size, self.ticks_style ) if self.draw_spine: self._make_spine( context ) if self.draw_ticklabels or implicit_draw_ticklabels: self._make_ticklabels( context, ticks, ticklabels ) ## has to be made last if self.label is not None: self.add( apply(_BoxLabel, (_Group(self.device_objects), self.label, self._side(), self.label_offset),\ self.label_style) ) class _HalfAxisX( _HalfAxis ): def _pos( self, context, a, db=0. ): p = context.geom( a, self._intercept(context) ) return p[0], p[1] + db def _dpos( self, d ): return 0., d def _align( self ): if self.ticklabels_dir < 0: return 'center', 'top' else: return 'center', 'bottom' def _intercept( self, context ): if self.intercept is not None: return self.intercept limits = context.data_bbox if self.ticklabels_dir < 0: return limits.yrange()[0] else: return limits.yrange()[1] def _log( self, context ): if self.log is None: return context.xlog return self.log def _side( self ): if self.ticklabels_dir < 0: return 'bottom' else: return 'top' def _range( self, context ): if self.range is not None: a,b = self.range if a is None or b is None: c,d = context.data_bbox.xrange() if a is None: a = c if b is None: b = d return a,b else: return self.range return context.data_bbox.xrange() def _make_grid( self, context, ticks ): if ticks is None: return for tick in ticks: self.add( apply(LineX, (tick,), self.grid_style) ) class _HalfAxisY( _HalfAxis ): def _pos( self, context, a, db=0. ): p = context.geom( self._intercept(context), a ) return p[0] + db, p[1] def _dpos( self, d ): return d, 0. def _align( self ): if self.ticklabels_dir > 0: return 'left', 'center' else: return 'right', 'center' def _intercept( self, context ): if self.intercept is not None: return self.intercept limits = context.data_bbox if self.ticklabels_dir > 0: return limits.xrange()[1] else: return limits.xrange()[0] def _log( self, context ): if self.log is None: return context.ylog return self.log def _side( self ): if self.ticklabels_dir > 0: return 'right' else: return 'left' def _range( self, context ): if self.range is not None: a,b = self.range if a is None or b is None: c,d = context.data_bbox.yrange() if a is None: a = c if b is None: b = d return a,b else: return self.range return context.data_bbox.yrange() def _make_grid( self, context, ticks ): if ticks is None: return for tick in ticks: self.add( apply(LineY, (tick,), self.grid_style) ) # _BoxLabel ------------------------------------------------------------------- class _BoxLabel( _PlotComponent ): kw_rename = { 'face' : 'fontface', 'size' : 'fontsize', } def __init__( self, obj, str, side, offset, **kw ): _PlotComponent.__init__( self ) self.kw_init( kw ) self.obj = obj self.str = str self.side = side self.offset = offset def make( self, context ): bb = self.obj.bbox( context ) offset = _size_relative( self.offset, context.dev_bbox ) if self.side == 'top': p = bb.upperleft() q = bb.upperright() elif self.side == 'bottom': p = bb.lowerleft() q = bb.lowerright() offset = -offset elif self.side == 'left': p = bb.lowerleft() q = bb.upperleft() elif self.side == 'right': p = bb.upperright() q = bb.lowerright() lt = apply( _LineTextObject, (p, q, self.str, offset), \ self.kw_style ) self.add( lt ) # _PlotComposite -------------------------------------------------------------- class _PlotComposite( _StyleKeywords ): def __init__( self, **kw ): self.kw_init( kw ) self.components = [] self.dont_clip = 0 def add( self, *args ): for obj in args: self.components.append( obj ) def clear( self ): self.components = [] def empty( self ): return len(self.components) == 0 def limits( self ): bb = BoundingBox() for obj in self.components: bb.union( obj.limits() ) return bb def make( self, context ): pass def bbox( self, context ): self.make( context ) bb = BoundingBox() for obj in self.components: bb.union( obj.bbox(context) ) return bb def render( self, context ): self.make( context ) self.kw_predraw( context ) if not self.dont_clip: context.do_clip() for obj in self.components: obj.render( context ) self.kw_postdraw( context ) # Frame ----------------------------------------------------------------------- class Frame( _PlotComposite ): def __init__( self, labelticks=(0,1,1,0), **kw ): apply( _PlotComposite.__init__, (self,), kw ) self.dont_clip = 1 self.x2 = _HalfAxisX() self.x2.draw_ticklabels = labelticks[0] self.x2.ticklabels_dir = 1 self.x1 = _HalfAxisX() self.x1.draw_ticklabels = labelticks[1] self.x1.ticklabels_dir = -1 self.y1 = _HalfAxisY() self.y1.draw_ticklabels = labelticks[2] self.y1.ticklabels_dir = -1 self.y2 = _HalfAxisY() self.y2.draw_ticklabels = labelticks[3] self.y2.ticklabels_dir = 1 def make( self, context ): self.clear() self.add( self.x1, self.x2, self.y1, self.y2 ) # _PlotContainer -------------------------------------------------------------- def _open_output( filename ): if filename == '-': import sys return sys.stdout else: # b is for windows return open( filename, 'wb' ) def _close_output( file ): if 2 < file.fileno(): file.close() else: file.flush() def _draw_text( device, p, str, **kw ): device.save_state() for key,val in kw.items(): device.set( key, val ) device.text( p, str ) device.restore_state() def win_temp_path(): """ Intended for Windows, returns a valid temp directory, or at least the current working directory. """ import os if os.environ.has_key('TEMP'): if os.path.exists(os.environ['TEMP']): return(os.environ['TEMP']) else: possible_temp_paths = [ '\\temp', '\\winnt\\temp', \ '\\windows\\temp', '\\tmp' ] for i in possible_temp_paths: if os.path.exists(i): return i return os.getcwd() class _PlotContainer( _ConfAttributes ): def __init__( self, **kw ): apply( self.conf_setattr, ("_PlotContainer",), kw ) def empty( self ): pass def interior( self, device, exterior ): TOL = 0.005 interior = exterior.copy() region_diagonal = exterior.diagonal() for i in range(10): bb = self.exterior( device, interior ) dll = pt_sub( exterior.lowerleft(), bb.lowerleft() ) dur = pt_sub( exterior.upperright(), bb.upperright() ) sll = pt_len(dll) / region_diagonal sur = pt_len(dur) / region_diagonal if sll < TOL and sur < TOL: # XXX:fixme if self.aspect_ratio is not None: interior.make_aspect_ratio(\ self.aspect_ratio ) return interior scale = interior.diagonal() / bb.diagonal() dll = pt_mul( scale, dll ) dur = pt_mul( scale, dur ) interior = BoundingBox( pt_add(interior.lowerleft(), dll), pt_add(interior.upperright(), dur) ) raise BigglesError def exterior( self, device, interior ): return interior.copy() def compose_interior( self, device, interior ): if self.title is not None: offset = _size_relative( self.title_offset, interior ) exterior = self.exterior( device, interior ) x = interior.center()[0] y = exterior.yrange()[1] + offset style = self.title_style.copy() style["fontsize"] = _fontsize_relative( \ self.title_style["fontsize"], interior, device ) style["texthalign"] = "center" style["textvalign"] = "bottom" apply( _draw_text, (device, (x,y), self.title), style ) def compose( self, device, region ): if self.empty(): raise BigglesError( "empty container" ) exterior = region.copy() if self.title is not None: offset = _size_relative( self.title_offset, exterior ) fontsize = _fontsize_relative( \ self.title_style["fontsize"], exterior, device ) exterior.deform( -offset-fontsize, 0, 0, 0 ) interior = self.interior( device, exterior ) self.compose_interior( device, interior ) def page_compose( self, device ): device.open() bb = BoundingBox( device.lowerleft, device.upperright ) device.bbox = bb.copy() for key,val in config.options('default').items(): device.set( key, val ) bb.expand( -self.page_margin ) self.compose( device, bb ) device.close() def show( self, width=None, height=None ): import os if width is None: width = config.value('screen','width') if height is None: height = config.value('screen','height') if os.name == 'posix': self.show_x11( width, height ) elif os.name == 'dos' or os.name == 'nt': self.show_win( width, height ) else: _message( "show: system type '%s' not supported" \ % os.name ) def show_x11( self, width, height ): persistent = config.interactive() and \ config.bool('screen','persistent') device = renderer.ScreenRenderer( persistent, width, height ) self.page_compose( device ) device.delete() def show_win( self, width, height ): """ Substitute for show() that will work on Windows. Generates temporary files somewhere that end with '_biggles.png'. These temporary files are not deleted, they must be manually cleaned up during normal temp directory maintenance. """ import os, tempfile #tf = os.path.join( win_temp_path(), 'biggles_graph.png' ) tf = tempfile.mktemp('_biggles.png') self.write_img( width, height, tf ) os.startfile( tf ) def psprint( self, printcmd=None, **kw ): import os, copy if os.name != 'posix': _message( "psprint: system type '%s' not supported" \ % os.name ) if printcmd is None: printcmd = config.value("printer","command") opt = copy.copy( config.options("postscript") ) opt.update( kw ) _message( 'printing plot with "%s"' % printcmd ) printer = os.popen( printcmd, 'w' ) device = apply( renderer.PSRenderer, (printer,), opt ) self.page_compose( device ) device.delete() printer.close() def write_eps( self, filename, **kw ): opt = copy.copy( config.options("postscript") ) opt.update( kw ) file = _open_output( filename ) device = apply( renderer.PSRenderer, (file,), opt ) self.page_compose( device ) device.delete() _close_output( file ) def write_img( self, *args ): if len(args) == 4: type,width,height,filename = args elif len(args) == 3: import string width,height,filename = args type = string.lower( filename[-3:] ) file = _open_output( filename ) device = renderer.ImageRenderer( type, width, height, file ) self.page_compose( device ) device.delete() _close_output( file ) save_as_eps = write_eps save_as_img = write_img def draw_piddle( self, canvastype=None, size=(500,500) ): from device.piddle import PiddleRenderer device = PiddleRenderer( canvastype, size ) self.page_compose( device ) canvas = device.canvas device.delete() return canvas def write_back_png( self, *args ): """ Saves PNG file in temporary file. Returns file contents. """ import tempfile, os if len(args) == 2: width,height = args type = 'png' file = tempfile.mktemp('_biggles.png') self.write_img(type, width, height, file) f = open(file, 'rb') output = f.read() f.close() os.remove(file) return output def multipage( plots, filename, **kw ): file = _open_output( filename ) opt = copy.copy( config.options("postscript") ) opt.update( kw ) device = apply( renderer.PSRenderer, (file,), opt ) for plot in plots: plot.page_compose( device ) device.delete() _close_output( file ) # ----------------------------------------------------------------------------- def _limits_axis( content_range, gutter, user_range, log ): r0, r1 = 0, 1 if content_range is not None: a, b = content_range if a is not None: r0 = a if b is not None: r1 = b if gutter is not None: dx = 0.5 * gutter * (r1 - r0) a = r0 - dx if not log or a > 0: r0 = a r1 = r1 + dx if user_range is not None: a, b = user_range if a is not None: r0 = a if b is not None: r1 = b if r0 == r1: r0 = r0 - 1 r1 = r1 + 1 return r0, r1 def _limits( content_bbox, gutter, xlog, ylog, xrange, yrange ): xr = _limits_axis( content_bbox.xrange(), gutter, xrange, xlog ) yr = _limits_axis( content_bbox.yrange(), gutter, yrange, ylog ) return BoundingBox( (xr[0],yr[0]), (xr[1],yr[1]) ) # Plot ------------------------------------------------------------------------ class Plot( _PlotContainer ): def __init__( self, **kw ): apply( _PlotContainer.__init__, (self,) ) apply( self.conf_setattr, ("Plot",), kw ) self.content = _PlotComposite() def __iadd__( self, other ): self.add( other ) def empty( self ): return self.content.empty() def add( self, *args ): apply( self.content.add, args ) def limits( self ): return _limits( self.content.limits(), self.gutter, \ self.xlog, self.ylog, self.xrange, self.yrange ) def compose_interior( self, device, region, limits=None ): if limits is None: limits = self.limits() context = _PlotContext( device, region, limits, xlog=self.xlog, ylog=self.ylog ) self.content.render( context ) def compose( self, device, region, limits=None ): interior = self.interior( device, region ) self.compose_interior( device, interior, limits ) # FramedPlot ------------------------------------------------------------------ class FramedPlot( _PlotContainer ): def __init__( self, **kw ): apply( _PlotContainer.__init__, (self,) ) self.content1 = _PlotComposite() self.content2 = _PlotComposite() self.x1 = _HalfAxisX() self.x1.ticklabels_dir = -1 self.y1 = _HalfAxisY() self.y1.ticklabels_dir = -1 self.x2 = _HalfAxisX() self.x2.draw_ticklabels = None self.y2 = _HalfAxisY() self.y2.draw_ticklabels = None self.frame = _Alias( self.x1, self.x2, self.y1, self.y2 ) self.frame1 = _Alias( self.x1, self.y1 ) self.frame2 = _Alias( self.x2, self.y2 ) self.x = _Alias( self.x1, self.x2 ) self.y = _Alias( self.y1, self.y2 ) apply( self.conf_setattr, ("FramedPlot",), kw ) _attr_map = { "xlabel" : ("x1", "label"), "ylabel" : ("y1", "label"), "xlog" : ("x1", "log"), "ylog" : ("y1", "log"), "xrange" : ("x1", "range"), "yrange" : ("y1", "range"), "xtitle" : ("x1", "label"), "ytitle" : ("y1", "label"), } def __getattr__( self, name ): if self._attr_map.has_key( name ): xs = self._attr_map[ name ] obj = self for x in xs[:-1]: obj = getattr( obj, x ) return getattr( obj, xs[-1] ) else: return self.__dict__[name] def __setattr__( self, name, value ): if self._attr_map.has_key( name ): xs = self._attr_map[ name ] obj = self for x in xs[:-1]: obj = getattr( obj, x ) setattr( obj, xs[-1], value ) else: self.__dict__[name] = value def empty( self ): return self.content1.empty() and self.content2.empty() def add( self, *args ): apply( self.content1.add, args ) def add2( self, *args ): apply( self.content2.add, args ) def _xy2log( self ): return _first_not_none(self.x2.log, self.x1.log), \ _first_not_none(self.y2.log, self.y1.log) def _limits1( self ): return _limits( self.content1.limits(), self.gutter, self.x1.log, self.y1.log, \ self.x1.range, self.y1.range ) def _context1( self, device, region ): return _PlotContext( device, region, self._limits1(), xlog=self.x1.log, ylog=self.y1.log ) def _limits2( self ): limits = self.content2.limits() if self.content2.empty(): limits = self.content1.limits() xlog, ylog = self._xy2log() xrange = _first_not_none( self.x2.range, self.x1.range ) yrange = _first_not_none( self.y2.range, self.y1.range ) return _limits( limits, self.gutter, xlog, ylog, \ xrange, yrange ) def _context2( self, device, region ): xlog, ylog = self._xy2log() return _PlotContext( device, region, self._limits2(), xlog, ylog ) def exterior( self, device, region ): bbox = BoundingBox() context1 = self._context1( device, region ) bbox.union( self.x1.bbox(context1) ) bbox.union( self.y1.bbox(context1) ) context2 = self._context2( device, region ) bbox.union( self.x2.bbox(context2) ) bbox.union( self.y2.bbox(context2) ) return bbox def compose_interior( self, device, region ): _PlotContainer.compose_interior( self, device, region ) context1 = self._context1( device, region ) context2 = self._context2( device, region ) self.content1.render( context1 ) self.content2.render( context2 ) self.y2.render( context2 ) self.x2.render( context2 ) self.y1.render( context1 ) self.x1.render( context1 ) class OldCustomFramedPlot( FramedPlot ): def __init__( self, **kw ): apply( FramedPlot.__init__, (self,), kw ) self.x = self.x1 self.y = self.y1 # Table ----------------------------------------------------------------------- class _Grid: def __init__( self, nrows, ncols, bbox, cellpadding=0, cellspacing=0 ): self.nrows = nrows self.ncols = ncols w, h = bbox.width(), bbox.height() cp = _size_relative( cellpadding, bbox ) cs = _size_relative( cellspacing, bbox ) self.origin = pt_add( bbox.lowerleft(), (cp,cp) ) self.step_x = (w + cs)/ncols self.step_y = (h + cs)/nrows self.cell_dimen = self.step_x - cs - 2*cp, \ self.step_y - cs - 2*cp def cell( self, i, j ): ii = self.nrows-1 - i p = pt_add( self.origin, (j*self.step_x,ii*self.step_y) ) q = pt_add( p, self.cell_dimen ) return BoundingBox( p, q ) class Table( _PlotContainer ): def __init__( self, rows, cols, **kw ): apply( _PlotContainer.__init__, (self,) ) apply( self.conf_setattr, ("Table",), kw ) self.rows = rows self.cols = cols self.content = {} def __getitem__( self, key ): return self.content[key] def __setitem__( self, key, value ): self.content[key] = value def set( self, i, j, obj ): self.content[i,j] = obj def get( self, i, j ): return self.content.get( (i,j), None ) def exterior( self, device, interior ): ext = interior.copy() if self.align_interiors: g = _Grid( self.rows, self.cols, interior, \ self.cellpadding, self.cellspacing ) for key,obj in self.content.items(): subregion = apply( g.cell, key ) ext.union( obj.exterior(device, subregion) ) return ext def compose_interior( self, device, interior ): _PlotContainer.compose_interior( self, device, interior ) g = _Grid( self.rows, self.cols, interior, \ self.cellpadding, self.cellspacing ) for key,obj in self.content.items(): subregion = apply( g.cell, key ) if self.align_interiors: obj.compose_interior( device, subregion ) else: obj.compose( device, subregion ) # FramedArray ----------------------------------------------------------------- # # Hideous, but it works... # def _frame_draw( obj, device, region, limits, labelticks=(0,1,1,0) ): frame = Frame( labelticks=labelticks ) context = _PlotContext( device, region, limits, xlog=obj.xlog, ylog=obj.ylog ) frame.render( context ) def _frame_bbox( obj, device, region, limits, labelticks=(0,1,1,0) ): frame = Frame( labelticks=labelticks ) context = _PlotContext( device, region, limits, xlog=obj.xlog, ylog=obj.ylog ) return frame.bbox( context ) def _range_union( a, b ): if a is None: return b if b is None: return a return min(a[0],b[0]), max(a[1],b[1]) class FramedArray( _PlotContainer ): def __init__( self, nrows, ncols, **kw ): apply( _PlotContainer.__init__, (self,) ) self.nrows = nrows self.ncols = ncols self.content = {} for i in range(nrows): for j in range(ncols): self.content[i,j] = Plot() apply( self.conf_setattr, ("FramedArray",), kw ) _attr_distribute = [ 'gutter', 'xlog', 'ylog', 'xrange', 'yrange', ] _attr_deprecated = { 'labeloffset' : 'label_offset', 'labelsize' : 'label_size', } def __setattr__( self, name, value ): if name in self._attr_distribute: for obj in self.content.values(): setattr( obj, name, value ) else: _name = self._attr_deprecated.get( name, name ) self.__dict__[_name] = value def __getitem__( self, key ): return self.content[key] def _limits( self, i, j ): if self.uniform_limits: return self._limits_uniform() else: return self._limits_nonuniform( i, j ) def _limits_uniform( self ): limits = BoundingBox() for obj in self.content.values(): limits.union( obj.limits() ) return limits def _limits_nonuniform( self, i, j ): lx = None for k in range(self.nrows): l = self.content[k,j].limits() lx = _range_union( l.xrange(), lx ) ly = None for k in range(self.ncols): l = self.content[i,k].limits() ly = _range_union( l.yrange(), ly ) return BoundingBox( (lx[0],ly[0]), (lx[1],ly[1]) ) def _grid( self, interior ): return _Grid( self.nrows, self.ncols, interior, cellspacing=self.cellspacing ) def _frames_bbox( self, device, interior ): bb = BoundingBox() g = self._grid( interior ) corners = [(0,0),(self.nrows-1,self.ncols-1)] for key in corners: obj = self.content[key] subregion = apply( g.cell, key ) limits = apply( self._limits, key ) axislabels = [0,0,0,0] if key[0] == self.nrows-1: axislabels[1] = 1 if key[1] == 0: axislabels[2] = 1 bb.union( _frame_bbox(obj, device, subregion, \ limits, axislabels) ) return bb def exterior( self, device, interior ): bb = self._frames_bbox( device, interior ) labeloffset = _size_relative( self.label_offset, interior ) labelsize = _fontsize_relative( \ self.label_size, interior, device ) margin = labeloffset + labelsize if self.xlabel is not None: bb.deform( 0, margin, 0, 0 ) if self.ylabel is not None: bb.deform( 0, 0, margin, 0 ) return bb def _frames_draw( self, device, interior ): g = self._grid( interior ) for key,obj in self.content.items(): subregion = apply( g.cell, key ) limits = apply( self._limits, key ) axislabels = [0,0,0,0] if key[0] == self.nrows-1: axislabels[1] = 1 if key[1] == 0: axislabels[2] = 1 _frame_draw( obj, device, subregion, \ limits, axislabels ) def _data_draw( self, device, interior ): g = self._grid( interior ) for key,obj in self.content.items(): subregion = apply( g.cell, key ) limits = apply( self._limits, key ) obj.compose_interior( device, subregion, limits ) def _labels_draw( self, device, interior ): bb = self._frames_bbox( device, interior ) labeloffset = _size_relative( self.label_offset, interior ) labelsize = _fontsize_relative( \ self.label_size, interior, device ) device.save_state() device.set( 'fontsize', labelsize ) device.set( 'texthalign', 'center' ) if self.xlabel is not None: x = interior.center()[0] y = bb.yrange()[0] - labeloffset device.set( 'textvalign', 'top' ) device.text( (x,y), self.xlabel ) if self.ylabel is not None: x = bb.xrange()[0] - labeloffset y = interior.center()[1] device.set( 'textangle', 90. ) device.set( 'textvalign', 'bottom' ) device.text( (x,y), self.ylabel ) device.restore_state() def add( self, *args ): for obj in self.content.values(): apply( obj.add, args ) def compose_interior( self, device, interior ): _PlotContainer.compose_interior( self, device, interior ) self._data_draw( device, interior ) self._frames_draw( device, interior ) self._labels_draw( device, interior ) # Text ------------------------------------------------------------------------ class Text( _PlotContainer ): def __init__( self, text, **kw ): apply( _PlotContainer.__init__, (self,) ) apply( self.conf_setattr, ("Text",), kw ) import string self.lines = string.split( text, "\n" ) if self.lines[0] == '': del self.lines[0] if self.lines[-1] == '': del self.lines[-1] def compose( self, device, region ): device.save_state() context = _PlotContext( device, region, region ) fontsize = _fontsize_relative( self.fontsize, context.dev_bbox, device ) device.set( 'fontsize', fontsize ) block_w = 0 for line in self.lines: block_w = max( block_w, device.textwidth(line) ) dy = fontsize * self.lineheight block_h = fontsize + dy * (len(self.lines)-1) x0, y0 = region.center() y0 = y0 + block_h/2. if self.halign == 'left': x0 = x0 - (region.width() - block_w)/2. elif self.halign == 'right': x0 = x0 + (region.width() - block_w)/2. if self.valign == 'top': y0 = y0 + (region.height() - block_h)/2. elif self.valign == 'bottom': y0 = y0 - (region.height() - block_h)/2. if self.justify == 'left': x0 = x0 - block_w/2 elif self.justify == 'right': x0 = x0 + block_w/2 ## render block = _PlotComposite() y = y0 for line in self.lines: block.add( DataLabel(x0, y, line, \ halign=self.justify, valign='top', \ fontface=self.fontface, \ fontsize=self.fontsize) ) y = y - dy block.render( context ) device.restore_state() # Inset ----------------------------------------------------------------------- class _Inset: def __init__( self, p, q, plot ): self.plot_limits = BoundingBox( p, q ) self.plot = plot def render( self, context ): region = self.bbox( context ) self.plot.compose_interior( context.draw, region ) class DataInset( _Inset ): def bbox( self, context ): p = apply( context.geom, self.plot_limits.lowerleft() ) q = apply( context.geom, self.plot_limits.upperright() ) return BoundingBox( p, q ) def limits( self ): return self.plot_limits.copy() class PlotInset( _Inset ): def bbox( self, context ): p = apply( context.plot_geom, self.plot_limits.lowerleft() ) q = apply( context.plot_geom, self.plot_limits.upperright() ) return BoundingBox( p, q ) def limits( self ): return BoundingBox()