mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-04-19 08:31:48 +00:00
Added printing requirements
This commit is contained in:
5
reportlab/graphics/charts/__init__.py
Normal file
5
reportlab/graphics/charts/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2012
|
||||
#see license.txt for license details
|
||||
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/__init__.py
|
||||
__version__=''' $Id$ '''
|
||||
__doc__='''Business charts'''
|
||||
94
reportlab/graphics/charts/areas.py
Normal file
94
reportlab/graphics/charts/areas.py
Normal file
@@ -0,0 +1,94 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2012
|
||||
#see license.txt for license details
|
||||
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/areas.py
|
||||
|
||||
__version__=''' $Id$ '''
|
||||
__doc__='''This module defines a Area mixin classes'''
|
||||
|
||||
from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isNoneOrShape
|
||||
from reportlab.graphics.widgetbase import Widget
|
||||
from reportlab.graphics.shapes import Rect, Group, Line, Polygon
|
||||
from reportlab.lib.attrmap import AttrMap, AttrMapValue
|
||||
from reportlab.lib.colors import grey
|
||||
|
||||
class PlotArea(Widget):
|
||||
"Abstract base class representing a chart's plot area, pretty unusable by itself."
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumber, desc='X position of the lower-left corner of the chart.'),
|
||||
y = AttrMapValue(isNumber, desc='Y position of the lower-left corner of the chart.'),
|
||||
width = AttrMapValue(isNumber, desc='Width of the chart.'),
|
||||
height = AttrMapValue(isNumber, desc='Height of the chart.'),
|
||||
strokeColor = AttrMapValue(isColorOrNone, desc='Color of the plot area border.'),
|
||||
strokeWidth = AttrMapValue(isNumber, desc='Width plot area border.'),
|
||||
fillColor = AttrMapValue(isColorOrNone, desc='Color of the plot area interior.'),
|
||||
background = AttrMapValue(isNoneOrShape, desc='Handle to background object e.g. Rect(0,0,width,height).'),
|
||||
debug = AttrMapValue(isNumber, desc='Used only for debugging.'),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.x = 20
|
||||
self.y = 10
|
||||
self.height = 85
|
||||
self.width = 180
|
||||
self.strokeColor = None
|
||||
self.strokeWidth = 1
|
||||
self.fillColor = None
|
||||
self.background = None
|
||||
self.debug = 0
|
||||
|
||||
def makeBackground(self):
|
||||
if self.background is not None:
|
||||
BG = self.background
|
||||
if isinstance(BG,Group):
|
||||
g = BG
|
||||
for bg in g.contents:
|
||||
bg.x = self.x
|
||||
bg.y = self.y
|
||||
bg.width = self.width
|
||||
bg.height = self.height
|
||||
else:
|
||||
g = Group()
|
||||
if type(BG) not in (type(()),type([])): BG=(BG,)
|
||||
for bg in BG:
|
||||
bg.x = self.x
|
||||
bg.y = self.y
|
||||
bg.width = self.width
|
||||
bg.height = self.height
|
||||
g.add(bg)
|
||||
return g
|
||||
else:
|
||||
strokeColor,strokeWidth,fillColor=self.strokeColor, self.strokeWidth, self.fillColor
|
||||
if (strokeWidth and strokeColor) or fillColor:
|
||||
g = Group()
|
||||
_3d_dy = getattr(self,'_3d_dy',None)
|
||||
x = self.x
|
||||
y = self.y
|
||||
h = self.height
|
||||
w = self.width
|
||||
if _3d_dy is not None:
|
||||
_3d_dx = self._3d_dx
|
||||
if fillColor and not strokeColor:
|
||||
from reportlab.lib.colors import Blacker
|
||||
c = Blacker(fillColor, getattr(self,'_3d_blacken',0.7))
|
||||
else:
|
||||
c = strokeColor
|
||||
if not strokeWidth: strokeWidth = 0.5
|
||||
if fillColor or strokeColor or c:
|
||||
bg = Polygon([x,y,x,y+h,x+_3d_dx,y+h+_3d_dy,x+w+_3d_dx,y+h+_3d_dy,x+w+_3d_dx,y+_3d_dy,x+w,y],
|
||||
strokeColor=strokeColor or c or grey, strokeWidth=strokeWidth, fillColor=fillColor)
|
||||
g.add(bg)
|
||||
g.add(Line(x,y,x+_3d_dx,y+_3d_dy, strokeWidth=0.5, strokeColor=c))
|
||||
g.add(Line(x+_3d_dx,y+_3d_dy, x+_3d_dx,y+h+_3d_dy,strokeWidth=0.5, strokeColor=c))
|
||||
fc = Blacker(c, getattr(self,'_3d_blacken',0.8))
|
||||
g.add(Polygon([x,y,x+_3d_dx,y+_3d_dy,x+w+_3d_dx,y+_3d_dy,x+w,y],
|
||||
strokeColor=strokeColor or c or grey, strokeWidth=strokeWidth, fillColor=fc))
|
||||
bg = Line(x+_3d_dx,y+_3d_dy, x+w+_3d_dx,y+_3d_dy,strokeWidth=0.5, strokeColor=c)
|
||||
else:
|
||||
bg = None
|
||||
else:
|
||||
bg = Rect(x, y, w, h,
|
||||
strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor)
|
||||
if bg: g.add(bg)
|
||||
return g
|
||||
else:
|
||||
return None
|
||||
2332
reportlab/graphics/charts/axes.py
Normal file
2332
reportlab/graphics/charts/axes.py
Normal file
File diff suppressed because it is too large
Load Diff
2298
reportlab/graphics/charts/barcharts.py
Normal file
2298
reportlab/graphics/charts/barcharts.py
Normal file
File diff suppressed because it is too large
Load Diff
165
reportlab/graphics/charts/dotbox.py
Normal file
165
reportlab/graphics/charts/dotbox.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from reportlab.lib.colors import blue, _PCMYK_black
|
||||
from reportlab.graphics.charts.textlabels import Label
|
||||
from reportlab.graphics.shapes import Circle, Drawing, Group, Line, Rect, String
|
||||
from reportlab.graphics.widgetbase import Widget
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.lib.validators import *
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.pdfbase.pdfmetrics import getFont
|
||||
from reportlab.graphics.charts.lineplots import _maxWidth
|
||||
|
||||
class DotBox(Widget):
|
||||
"""Returns a dotbox widget."""
|
||||
|
||||
#Doesn't use TypedPropertyCollection for labels - this can be a later improvement
|
||||
_attrMap = AttrMap(
|
||||
xlabels = AttrMapValue(isNoneOrListOfNoneOrStrings,
|
||||
desc="List of text labels for boxes on left hand side"),
|
||||
ylabels = AttrMapValue(isNoneOrListOfNoneOrStrings,
|
||||
desc="Text label for second box on left hand side"),
|
||||
labelFontName = AttrMapValue(isString,
|
||||
desc="Name of font used for the labels"),
|
||||
labelFontSize = AttrMapValue(isNumber,
|
||||
desc="Size of font used for the labels"),
|
||||
labelOffset = AttrMapValue(isNumber,
|
||||
desc="Space between label text and grid edge"),
|
||||
strokeWidth = AttrMapValue(isNumber,
|
||||
desc='Width of the grid and dot outline'),
|
||||
gridDivWidth = AttrMapValue(isNumber,
|
||||
desc="Width of each 'box'"),
|
||||
gridColor = AttrMapValue(isColor,
|
||||
desc='Colour for the box and gridding'),
|
||||
dotDiameter = AttrMapValue(isNumber,
|
||||
desc="Diameter of the circle used for the 'dot'"),
|
||||
dotColor = AttrMapValue(isColor,
|
||||
desc='Colour of the circle on the box'),
|
||||
dotXPosition = AttrMapValue(isNumber,
|
||||
desc='X Position of the circle'),
|
||||
dotYPosition = AttrMapValue(isNumber,
|
||||
desc='X Position of the circle'),
|
||||
x = AttrMapValue(isNumber,
|
||||
desc='X Position of dotbox'),
|
||||
y = AttrMapValue(isNumber,
|
||||
desc='Y Position of dotbox'),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.xlabels=["Value", "Blend", "Growth"]
|
||||
self.ylabels=["Small", "Medium", "Large"]
|
||||
self.labelFontName = "Helvetica"
|
||||
self.labelFontSize = 6
|
||||
self.labelOffset = 5
|
||||
self.strokeWidth = 0.5
|
||||
self.gridDivWidth=0.5*cm
|
||||
self.gridColor=colors.Color(25/255.0,77/255.0,135/255.0)
|
||||
self.dotDiameter=0.4*cm
|
||||
self.dotColor=colors.Color(232/255.0,224/255.0,119/255.0)
|
||||
self.dotXPosition = 1
|
||||
self.dotYPosition = 1
|
||||
self.x = 30
|
||||
self.y = 5
|
||||
|
||||
|
||||
def _getDrawingDimensions(self):
|
||||
leftPadding=rightPadding=topPadding=bottomPadding=5
|
||||
#find width of grid
|
||||
tx=len(self.xlabels)*self.gridDivWidth
|
||||
#add padding (and offset)
|
||||
tx=tx+leftPadding+rightPadding+self.labelOffset
|
||||
#add in maximum width of text
|
||||
tx=tx+_maxWidth(self.xlabels, self.labelFontName, self.labelFontSize)
|
||||
#find height of grid
|
||||
ty=len(self.ylabels)*self.gridDivWidth
|
||||
#add padding (and offset)
|
||||
ty=ty+topPadding+bottomPadding+self.labelOffset
|
||||
#add in maximum width of text
|
||||
ty=ty+_maxWidth(self.ylabels, self.labelFontName, self.labelFontSize)
|
||||
#print (tx, ty)
|
||||
return (tx,ty)
|
||||
|
||||
def demo(self,drawing=None):
|
||||
if not drawing:
|
||||
tx,ty=self._getDrawingDimensions()
|
||||
drawing = Drawing(tx,ty)
|
||||
drawing.add(self.draw())
|
||||
return drawing
|
||||
|
||||
def draw(self):
|
||||
g = Group()
|
||||
|
||||
#box
|
||||
g.add(Rect(self.x,self.y,len(self.xlabels)*self.gridDivWidth,len(self.ylabels)*self.gridDivWidth,
|
||||
strokeColor=self.gridColor,
|
||||
strokeWidth=self.strokeWidth,
|
||||
fillColor=None))
|
||||
|
||||
#internal gridding
|
||||
for f in range (1,len(self.ylabels)):
|
||||
#horizontal
|
||||
g.add(Line(strokeColor=self.gridColor,
|
||||
strokeWidth=self.strokeWidth,
|
||||
x1 = self.x,
|
||||
y1 = self.y+f*self.gridDivWidth,
|
||||
x2 = self.x+len(self.xlabels)*self.gridDivWidth,
|
||||
y2 = self.y+f*self.gridDivWidth))
|
||||
for f in range (1,len(self.xlabels)):
|
||||
#vertical
|
||||
g.add(Line(strokeColor=self.gridColor,
|
||||
strokeWidth=self.strokeWidth,
|
||||
x1 = self.x+f*self.gridDivWidth,
|
||||
y1 = self.y,
|
||||
x2 = self.x+f*self.gridDivWidth,
|
||||
y2 = self.y+len(self.ylabels)*self.gridDivWidth))
|
||||
|
||||
# draw the 'dot'
|
||||
g.add(Circle(strokeColor=self.gridColor,
|
||||
strokeWidth=self.strokeWidth,
|
||||
fillColor=self.dotColor,
|
||||
cx = self.x+(self.dotXPosition*self.gridDivWidth),
|
||||
cy = self.y+(self.dotYPosition*self.gridDivWidth),
|
||||
r = self.dotDiameter/2.0))
|
||||
|
||||
#used for centering y-labels (below)
|
||||
ascent=getFont(self.labelFontName).face.ascent
|
||||
if ascent==0:
|
||||
ascent=0.718 # default (from helvetica)
|
||||
ascent=ascent*self.labelFontSize # normalize
|
||||
|
||||
#do y-labels
|
||||
if self.ylabels != None:
|
||||
for f in range (len(self.ylabels)-1,-1,-1):
|
||||
if self.ylabels[f]!= None:
|
||||
g.add(String(strokeColor=self.gridColor,
|
||||
text = self.ylabels[f],
|
||||
fontName = self.labelFontName,
|
||||
fontSize = self.labelFontSize,
|
||||
fillColor=_PCMYK_black,
|
||||
x = self.x-self.labelOffset,
|
||||
y = self.y+(f*self.gridDivWidth+(self.gridDivWidth-ascent)/2.0),
|
||||
textAnchor = 'end'))
|
||||
|
||||
#do x-labels
|
||||
if self.xlabels != None:
|
||||
for f in range (0,len(self.xlabels)):
|
||||
if self.xlabels[f]!= None:
|
||||
l=Label()
|
||||
l.x=self.x+(f*self.gridDivWidth)+(self.gridDivWidth+ascent)/2.0
|
||||
l.y=self.y+(len(self.ylabels)*self.gridDivWidth)+self.labelOffset
|
||||
l.angle=90
|
||||
l.textAnchor='start'
|
||||
l.fontName = self.labelFontName
|
||||
l.fontSize = self.labelFontSize
|
||||
l.fillColor = _PCMYK_black
|
||||
l.setText(self.xlabels[f])
|
||||
l.boxAnchor = 'sw'
|
||||
l.draw()
|
||||
g.add(l)
|
||||
|
||||
return g
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
d = DotBox()
|
||||
d.demo().save(fnRoot="dotbox")
|
||||
400
reportlab/graphics/charts/doughnut.py
Normal file
400
reportlab/graphics/charts/doughnut.py
Normal file
@@ -0,0 +1,400 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2012
|
||||
#see license.txt for license details
|
||||
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/doughnut.py
|
||||
# doughnut chart
|
||||
|
||||
__version__=''' $Id$ '''
|
||||
__doc__="""Doughnut chart
|
||||
|
||||
Produces a circular chart like the doughnut charts produced by Excel.
|
||||
Can handle multiple series (which produce concentric 'rings' in the chart).
|
||||
|
||||
"""
|
||||
|
||||
import copy
|
||||
from math import sin, cos, pi
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\
|
||||
isListOfNumbers, isColorOrNone, isString,\
|
||||
isListOfStringsOrNone, OneOf, SequenceOf,\
|
||||
isBoolean, isListOfColors,\
|
||||
isNoneOrListOfNoneOrStrings,\
|
||||
isNoneOrListOfNoneOrNumbers,\
|
||||
isNumberOrNone
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, Ellipse, \
|
||||
Wedge, String, SolidShape, UserNode, STATE_DEFAULTS
|
||||
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
|
||||
from reportlab.graphics.charts.piecharts import AbstractPieChart, WedgeProperties, _addWedgeLabel, fixLabelOverlaps
|
||||
from reportlab.graphics.charts.textlabels import Label
|
||||
from reportlab.graphics.widgets.markers import Marker
|
||||
from functools import reduce
|
||||
|
||||
class SectorProperties(WedgeProperties):
|
||||
"""This holds descriptive information about the sectors in a doughnut chart.
|
||||
|
||||
It is not to be confused with the 'sector itself'; this just holds
|
||||
a recipe for how to format one, and does not allow you to hack the
|
||||
angles. It can format a genuine Sector object for you with its
|
||||
format method.
|
||||
"""
|
||||
_attrMap = AttrMap(BASE=WedgeProperties,
|
||||
)
|
||||
|
||||
class Doughnut(AbstractPieChart):
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumber, desc='X position of the chart within its container.'),
|
||||
y = AttrMapValue(isNumber, desc='Y position of the chart within its container.'),
|
||||
width = AttrMapValue(isNumber, desc='width of doughnut bounding box. Need not be same as width.'),
|
||||
height = AttrMapValue(isNumber, desc='height of doughnut bounding box. Need not be same as height.'),
|
||||
data = AttrMapValue(None, desc='list of numbers defining sector sizes; need not sum to 1'),
|
||||
labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"),
|
||||
startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"),
|
||||
direction = AttrMapValue(OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"),
|
||||
slices = AttrMapValue(None, desc="collection of sector descriptor objects"),
|
||||
simpleLabels = AttrMapValue(isBoolean, desc="If true(default) use String not super duper WedgeLabel"),
|
||||
# advanced usage
|
||||
checkLabelOverlap = AttrMapValue(isBoolean, desc="If true check and attempt to fix\n standard label overlaps(default off)",advancedUsage=1),
|
||||
sideLabels = AttrMapValue(isBoolean, desc="If true attempt to make chart with labels along side and pointers", advancedUsage=1)
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.width = 100
|
||||
self.height = 100
|
||||
self.data = [1,1]
|
||||
self.labels = None # or list of strings
|
||||
self.startAngle = 90
|
||||
self.direction = "clockwise"
|
||||
self.simpleLabels = 1
|
||||
self.checkLabelOverlap = 0
|
||||
self.sideLabels = 0
|
||||
|
||||
self.slices = TypedPropertyCollection(SectorProperties)
|
||||
self.slices[0].fillColor = colors.darkcyan
|
||||
self.slices[1].fillColor = colors.blueviolet
|
||||
self.slices[2].fillColor = colors.blue
|
||||
self.slices[3].fillColor = colors.cyan
|
||||
self.slices[4].fillColor = colors.pink
|
||||
self.slices[5].fillColor = colors.magenta
|
||||
self.slices[6].fillColor = colors.yellow
|
||||
|
||||
|
||||
def demo(self):
|
||||
d = Drawing(200, 100)
|
||||
|
||||
dn = Doughnut()
|
||||
dn.x = 50
|
||||
dn.y = 10
|
||||
dn.width = 100
|
||||
dn.height = 80
|
||||
dn.data = [10,20,30,40,50,60]
|
||||
dn.labels = ['a','b','c','d','e','f']
|
||||
|
||||
dn.slices.strokeWidth=0.5
|
||||
dn.slices[3].popout = 10
|
||||
dn.slices[3].strokeWidth = 2
|
||||
dn.slices[3].strokeDashArray = [2,2]
|
||||
dn.slices[3].labelRadius = 1.75
|
||||
dn.slices[3].fontColor = colors.red
|
||||
dn.slices[0].fillColor = colors.darkcyan
|
||||
dn.slices[1].fillColor = colors.blueviolet
|
||||
dn.slices[2].fillColor = colors.blue
|
||||
dn.slices[3].fillColor = colors.cyan
|
||||
dn.slices[4].fillColor = colors.aquamarine
|
||||
dn.slices[5].fillColor = colors.cadetblue
|
||||
dn.slices[6].fillColor = colors.lightcoral
|
||||
|
||||
d.add(dn)
|
||||
return d
|
||||
|
||||
def normalizeData(self, data=None):
|
||||
from operator import add
|
||||
sum = float(reduce(add,data,0))
|
||||
return abs(sum)>=1e-8 and list(map(lambda x,f=360./sum: f*x, data)) or len(data)*[0]
|
||||
|
||||
def makeSectors(self):
|
||||
# normalize slice data
|
||||
if isinstance(self.data,(list,tuple)) and isinstance(self.data[0],(list,tuple)):
|
||||
#it's a nested list, more than one sequence
|
||||
normData = []
|
||||
n = []
|
||||
for l in self.data:
|
||||
t = self.normalizeData(l)
|
||||
normData.append(t)
|
||||
n.append(len(t))
|
||||
self._seriesCount = max(n)
|
||||
else:
|
||||
normData = self.normalizeData(self.data)
|
||||
n = len(normData)
|
||||
self._seriesCount = n
|
||||
|
||||
#labels
|
||||
checkLabelOverlap = self.checkLabelOverlap
|
||||
L = []
|
||||
L_add = L.append
|
||||
|
||||
if self.labels is None:
|
||||
labels = []
|
||||
if not isinstance(n,(list,tuple)):
|
||||
labels = [''] * n
|
||||
else:
|
||||
for m in n:
|
||||
labels = list(labels) + [''] * m
|
||||
else:
|
||||
labels = self.labels
|
||||
#there's no point in raising errors for less than enough labels if
|
||||
#we silently create all for the extreme case of no labels.
|
||||
if not isinstance(n,(list,tuple)):
|
||||
i = n-len(labels)
|
||||
if i>0:
|
||||
labels = list(labels) + [''] * i
|
||||
else:
|
||||
tlab = 0
|
||||
for m in n:
|
||||
tlab += m
|
||||
i = tlab-len(labels)
|
||||
if i>0:
|
||||
labels = list(labels) + [''] * i
|
||||
|
||||
xradius = self.width/2.0
|
||||
yradius = self.height/2.0
|
||||
centerx = self.x + xradius
|
||||
centery = self.y + yradius
|
||||
|
||||
if self.direction == "anticlockwise":
|
||||
whichWay = 1
|
||||
else:
|
||||
whichWay = -1
|
||||
|
||||
g = Group()
|
||||
|
||||
startAngle = self.startAngle #% 360
|
||||
styleCount = len(self.slices)
|
||||
if isinstance(self.data[0],(list,tuple)):
|
||||
#multi-series doughnut
|
||||
ndata = len(self.data)
|
||||
yir = (yradius/2.5)/ndata
|
||||
xir = (xradius/2.5)/ndata
|
||||
ydr = (yradius-yir)/ndata
|
||||
xdr = (xradius-xir)/ndata
|
||||
for sn,series in enumerate(normData):
|
||||
for i,angle in enumerate(series):
|
||||
endAngle = (startAngle + (angle * whichWay)) #% 360
|
||||
if abs(startAngle-endAngle)<1e-5:
|
||||
startAngle = endAngle
|
||||
continue
|
||||
if startAngle < endAngle:
|
||||
a1 = startAngle
|
||||
a2 = endAngle
|
||||
else:
|
||||
a1 = endAngle
|
||||
a2 = startAngle
|
||||
startAngle = endAngle
|
||||
|
||||
#if we didn't use %stylecount here we'd end up with the later sectors
|
||||
#all having the default style
|
||||
sectorStyle = self.slices[i%styleCount]
|
||||
|
||||
# is it a popout?
|
||||
cx, cy = centerx, centery
|
||||
if sectorStyle.popout != 0:
|
||||
# pop out the sector
|
||||
averageAngle = (a1+a2)/2.0
|
||||
aveAngleRadians = averageAngle * pi/180.0
|
||||
popdistance = sectorStyle.popout
|
||||
cx = centerx + popdistance * cos(aveAngleRadians)
|
||||
cy = centery + popdistance * sin(aveAngleRadians)
|
||||
|
||||
yr1 = yir+sn*ydr
|
||||
yr = yr1 + ydr
|
||||
xr1 = xir+sn*xdr
|
||||
xr = xr1 + xdr
|
||||
if isinstance(n,(list,tuple)):
|
||||
theSector = Wedge(cx, cy, xr, a1, a2, yradius=yr, radius1=xr1, yradius1=yr1)
|
||||
else:
|
||||
theSector = Wedge(cx, cy, xr, a1, a2, yradius=yr, radius1=xr1, yradius1=yr1, annular=True)
|
||||
|
||||
theSector.fillColor = sectorStyle.fillColor
|
||||
theSector.strokeColor = sectorStyle.strokeColor
|
||||
theSector.strokeWidth = sectorStyle.strokeWidth
|
||||
theSector.strokeDashArray = sectorStyle.strokeDashArray
|
||||
|
||||
g.add(theSector)
|
||||
|
||||
if sn == 0:
|
||||
text = self.getSeriesName(i,'')
|
||||
if text:
|
||||
averageAngle = (a1+a2)/2.0
|
||||
aveAngleRadians = averageAngle*pi/180.0
|
||||
labelRadius = sectorStyle.labelRadius
|
||||
rx = xradius*labelRadius
|
||||
ry = yradius*labelRadius
|
||||
labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius)
|
||||
labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius)
|
||||
l = _addWedgeLabel(self,text,averageAngle,labelX,labelY,sectorStyle)
|
||||
if checkLabelOverlap:
|
||||
l._origdata = { 'x': labelX, 'y':labelY, 'angle': averageAngle,
|
||||
'rx': rx, 'ry':ry, 'cx':cx, 'cy':cy,
|
||||
'bounds': l.getBounds(),
|
||||
}
|
||||
L_add(l)
|
||||
|
||||
else:
|
||||
#single series doughnut
|
||||
yir = yradius/2.5
|
||||
xir = xradius/2.5
|
||||
for i,angle in enumerate(normData):
|
||||
endAngle = (startAngle + (angle * whichWay)) #% 360
|
||||
if abs(startAngle-endAngle)<1e-5:
|
||||
startAngle = endAngle
|
||||
continue
|
||||
if startAngle < endAngle:
|
||||
a1 = startAngle
|
||||
a2 = endAngle
|
||||
else:
|
||||
a1 = endAngle
|
||||
a2 = startAngle
|
||||
startAngle = endAngle
|
||||
|
||||
#if we didn't use %stylecount here we'd end up with the later sectors
|
||||
#all having the default style
|
||||
sectorStyle = self.slices[i%styleCount]
|
||||
|
||||
# is it a popout?
|
||||
cx, cy = centerx, centery
|
||||
if sectorStyle.popout != 0:
|
||||
# pop out the sector
|
||||
averageAngle = (a1+a2)/2.0
|
||||
aveAngleRadians = averageAngle * pi/180.0
|
||||
popdistance = sectorStyle.popout
|
||||
cx = centerx + popdistance * cos(aveAngleRadians)
|
||||
cy = centery + popdistance * sin(aveAngleRadians)
|
||||
|
||||
if n > 1:
|
||||
theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=xir, yradius1=yir)
|
||||
elif n==1:
|
||||
theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=xir, yradius1=yir, annular=True)
|
||||
|
||||
theSector.fillColor = sectorStyle.fillColor
|
||||
theSector.strokeColor = sectorStyle.strokeColor
|
||||
theSector.strokeWidth = sectorStyle.strokeWidth
|
||||
theSector.strokeDashArray = sectorStyle.strokeDashArray
|
||||
|
||||
g.add(theSector)
|
||||
|
||||
# now draw a label
|
||||
if labels[i] != "":
|
||||
averageAngle = (a1+a2)/2.0
|
||||
aveAngleRadians = averageAngle*pi/180.0
|
||||
labelRadius = sectorStyle.labelRadius
|
||||
labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius)
|
||||
labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius)
|
||||
rx = xradius*labelRadius
|
||||
ry = yradius*labelRadius
|
||||
l = _addWedgeLabel(self,labels[i],averageAngle,labelX,labelY,sectorStyle)
|
||||
if checkLabelOverlap:
|
||||
l._origdata = { 'x': labelX, 'y':labelY, 'angle': averageAngle,
|
||||
'rx': rx, 'ry':ry, 'cx':cx, 'cy':cy,
|
||||
'bounds': l.getBounds(),
|
||||
}
|
||||
L_add(l)
|
||||
|
||||
if checkLabelOverlap and L:
|
||||
fixLabelOverlaps(L)
|
||||
|
||||
for l in L: g.add(l)
|
||||
|
||||
return g
|
||||
|
||||
def draw(self):
|
||||
g = Group()
|
||||
g.add(self.makeSectors())
|
||||
return g
|
||||
|
||||
|
||||
def sample1():
|
||||
"Make up something from the individual Sectors"
|
||||
|
||||
d = Drawing(400, 400)
|
||||
g = Group()
|
||||
|
||||
s1 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=0, endangledegrees=120, radius1=100)
|
||||
s1.fillColor=colors.red
|
||||
s1.strokeColor=None
|
||||
d.add(s1)
|
||||
s2 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=120, endangledegrees=240, radius1=100)
|
||||
s2.fillColor=colors.green
|
||||
s2.strokeColor=None
|
||||
d.add(s2)
|
||||
s3 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=240, endangledegrees=260, radius1=100)
|
||||
s3.fillColor=colors.blue
|
||||
s3.strokeColor=None
|
||||
d.add(s3)
|
||||
s4 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=260, endangledegrees=360, radius1=100)
|
||||
s4.fillColor=colors.gray
|
||||
s4.strokeColor=None
|
||||
d.add(s4)
|
||||
|
||||
return d
|
||||
|
||||
def sample2():
|
||||
"Make a simple demo"
|
||||
|
||||
d = Drawing(400, 400)
|
||||
|
||||
dn = Doughnut()
|
||||
dn.x = 50
|
||||
dn.y = 50
|
||||
dn.width = 300
|
||||
dn.height = 300
|
||||
dn.data = [10,20,30,40,50,60]
|
||||
|
||||
d.add(dn)
|
||||
|
||||
return d
|
||||
|
||||
def sample3():
|
||||
"Make a more complex demo"
|
||||
|
||||
d = Drawing(400, 400)
|
||||
dn = Doughnut()
|
||||
dn.x = 50
|
||||
dn.y = 50
|
||||
dn.width = 300
|
||||
dn.height = 300
|
||||
dn.data = [[10,20,30,40,50,60], [10,20,30,40]]
|
||||
dn.labels = ['a','b','c','d','e','f']
|
||||
|
||||
d.add(dn)
|
||||
|
||||
return d
|
||||
|
||||
def sample4():
|
||||
"Make a more complex demo with Label Overlap fixing"
|
||||
|
||||
d = Drawing(400, 400)
|
||||
dn = Doughnut()
|
||||
dn.x = 50
|
||||
dn.y = 50
|
||||
dn.width = 300
|
||||
dn.height = 300
|
||||
dn.data = [[10,20,30,40,50,60], [10,20,30,40]]
|
||||
dn.labels = ['a','b','c','d','e','f']
|
||||
dn.checkLabelOverlap = True
|
||||
|
||||
d.add(dn)
|
||||
|
||||
return d
|
||||
|
||||
if __name__=='__main__':
|
||||
|
||||
from reportlab.graphics.renderPDF import drawToFile
|
||||
d = sample1()
|
||||
drawToFile(d, 'doughnut1.pdf')
|
||||
d = sample2()
|
||||
drawToFile(d, 'doughnut2.pdf')
|
||||
d = sample3()
|
||||
drawToFile(d, 'doughnut3.pdf')
|
||||
716
reportlab/graphics/charts/legends.py
Normal file
716
reportlab/graphics/charts/legends.py
Normal file
@@ -0,0 +1,716 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2012
|
||||
#see license.txt for license details
|
||||
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/legends.py
|
||||
|
||||
__version__=''' $Id$ '''
|
||||
__doc__="""This will be a collection of legends to be used with charts."""
|
||||
|
||||
import copy, operator
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isNumber, OneOf, isString, isColorOrNone,\
|
||||
isNumberOrNone, isListOfNumbersOrNone, isStringOrNone, isBoolean,\
|
||||
EitherOr, NoneOr, AutoOr, isAuto, Auto, isBoxAnchor, SequenceOf, isInstanceOf
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth, getFont
|
||||
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
|
||||
from reportlab.graphics.shapes import Drawing, Group, String, Rect, Line, STATE_DEFAULTS
|
||||
from reportlab.graphics.charts.areas import PlotArea
|
||||
from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol
|
||||
from reportlab.lib.utils import isSeq, find_locals
|
||||
from reportlab.graphics.shapes import _baseGFontName
|
||||
from functools import reduce
|
||||
|
||||
def _transMax(n,A):
|
||||
X = n*[0]
|
||||
m = 0
|
||||
for a in A:
|
||||
m = max(m,len(a))
|
||||
for i,x in enumerate(a):
|
||||
X[i] = max(X[i],x)
|
||||
X = [0] + X[:m]
|
||||
for i in range(m):
|
||||
X[i+1] += X[i]
|
||||
return X
|
||||
|
||||
def _objStr(s):
|
||||
if isinstance(s,str):
|
||||
return s
|
||||
else:
|
||||
return str(s)
|
||||
|
||||
def _getStr(s):
|
||||
if isSeq(s):
|
||||
return list(map(_getStr,s))
|
||||
else:
|
||||
return _objStr(s)
|
||||
|
||||
def _getLines(s):
|
||||
if isSeq(s):
|
||||
return tuple([(x or '').split('\n') for x in s])
|
||||
else:
|
||||
return (s or '').split('\n')
|
||||
|
||||
def _getLineCount(s):
|
||||
T = _getLines(s)
|
||||
if isSeq(s):
|
||||
return max([len(x) for x in T])
|
||||
else:
|
||||
return len(T)
|
||||
|
||||
def _getWidths(i,s, fontName, fontSize, subCols):
|
||||
S = []
|
||||
aS = S.append
|
||||
if isSeq(s):
|
||||
for j,t in enumerate(s):
|
||||
sc = subCols[j,i]
|
||||
fN = getattr(sc,'fontName',fontName)
|
||||
fS = getattr(sc,'fontSize',fontSize)
|
||||
m = [stringWidth(x, fN, fS) for x in t.split('\n')]
|
||||
m = max(sc.minWidth,m and max(m) or 0)
|
||||
aS(m)
|
||||
aS(sc.rpad)
|
||||
del S[-1]
|
||||
else:
|
||||
sc = subCols[0,i]
|
||||
fN = getattr(sc,'fontName',fontName)
|
||||
fS = getattr(sc,'fontSize',fontSize)
|
||||
m = [stringWidth(x, fN, fS) for x in s.split('\n')]
|
||||
aS(max(sc.minWidth,m and max(m) or 0))
|
||||
return S
|
||||
|
||||
class SubColProperty(PropHolder):
|
||||
dividerLines = 0
|
||||
_attrMap = AttrMap(
|
||||
minWidth = AttrMapValue(isNumber,desc="minimum width for this subcol"),
|
||||
rpad = AttrMapValue(isNumber,desc="right padding for this subcol"),
|
||||
align = AttrMapValue(OneOf('left','right','center','centre','numeric'),desc='alignment in subCol'),
|
||||
fontName = AttrMapValue(isString, desc="Font name of the strings"),
|
||||
fontSize = AttrMapValue(isNumber, desc="Font size of the strings"),
|
||||
leading = AttrMapValue(isNumber, desc="leading for the strings"),
|
||||
fillColor = AttrMapValue(isColorOrNone, desc="fontColor"),
|
||||
underlines = AttrMapValue(EitherOr((NoneOr(isInstanceOf(Line)),SequenceOf(isInstanceOf(Line),emptyOK=0,lo=0,hi=0x7fffffff))), desc="underline definitions"),
|
||||
overlines = AttrMapValue(EitherOr((NoneOr(isInstanceOf(Line)),SequenceOf(isInstanceOf(Line),emptyOK=0,lo=0,hi=0x7fffffff))), desc="overline definitions"),
|
||||
dx = AttrMapValue(isNumber, desc="x offset from default position"),
|
||||
dy = AttrMapValue(isNumber, desc="y offset from default position"),
|
||||
)
|
||||
|
||||
class LegendCallout:
|
||||
def _legendValues(legend,*args):
|
||||
'''return a tuple of values from the first function up the stack with isinstance(self,legend)'''
|
||||
L = find_locals(lambda L: L.get('self',None) is legend and L or None)
|
||||
return tuple([L[a] for a in args])
|
||||
_legendValues = staticmethod(_legendValues)
|
||||
|
||||
def _selfOrLegendValues(self,legend,*args):
|
||||
L = find_locals(lambda L: L.get('self',None) is legend and L or None)
|
||||
return tuple([getattr(self,a,L[a]) for a in args])
|
||||
|
||||
def __call__(self,legend,g,thisx,y,colName):
|
||||
col, name = colName
|
||||
|
||||
class LegendSwatchCallout(LegendCallout):
|
||||
def __call__(self,legend,g,thisx,y,i,colName,swatch):
|
||||
col, name = colName
|
||||
|
||||
class LegendColEndCallout(LegendCallout):
|
||||
def __call__(self,legend, g, x, xt, y, width, lWidth):
|
||||
pass
|
||||
|
||||
class Legend(Widget):
|
||||
"""A simple legend containing rectangular swatches and strings.
|
||||
|
||||
The swatches are filled rectangles whenever the respective
|
||||
color object in 'colorNamePairs' is a subclass of Color in
|
||||
reportlab.lib.colors. Otherwise the object passed instead is
|
||||
assumed to have 'x', 'y', 'width' and 'height' attributes.
|
||||
A legend then tries to set them or catches any error. This
|
||||
lets you plug-in any widget you like as a replacement for
|
||||
the default rectangular swatches.
|
||||
|
||||
Strings can be nicely aligned left or right to the swatches.
|
||||
"""
|
||||
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumber, desc="x-coordinate of upper-left reference point"),
|
||||
y = AttrMapValue(isNumber, desc="y-coordinate of upper-left reference point"),
|
||||
deltax = AttrMapValue(isNumberOrNone, desc="x-distance between neighbouring swatches"),
|
||||
deltay = AttrMapValue(isNumberOrNone, desc="y-distance between neighbouring swatches"),
|
||||
dxTextSpace = AttrMapValue(isNumber, desc="Distance between swatch rectangle and text"),
|
||||
autoXPadding = AttrMapValue(isNumber, desc="x Padding between columns if deltax=None",advancedUsage=1),
|
||||
autoYPadding = AttrMapValue(isNumber, desc="y Padding between rows if deltay=None",advancedUsage=1),
|
||||
yGap = AttrMapValue(isNumber, desc="Additional gap between rows",advancedUsage=1),
|
||||
dx = AttrMapValue(isNumber, desc="Width of swatch rectangle"),
|
||||
dy = AttrMapValue(isNumber, desc="Height of swatch rectangle"),
|
||||
columnMaximum = AttrMapValue(isNumber, desc="Max. number of items per column"),
|
||||
alignment = AttrMapValue(OneOf("left", "right"), desc="Alignment of text with respect to swatches"),
|
||||
colorNamePairs = AttrMapValue(None, desc="List of color/name tuples (color can also be widget)"),
|
||||
fontName = AttrMapValue(isString, desc="Font name of the strings"),
|
||||
fontSize = AttrMapValue(isNumber, desc="Font size of the strings"),
|
||||
fillColor = AttrMapValue(isColorOrNone, desc="swatches filling color"),
|
||||
strokeColor = AttrMapValue(isColorOrNone, desc="Border color of the swatches"),
|
||||
strokeWidth = AttrMapValue(isNumber, desc="Width of the border color of the swatches"),
|
||||
swatchMarker = AttrMapValue(NoneOr(AutoOr(isSymbol)), desc="None, Auto() or makeMarker('Diamond') ...",advancedUsage=1),
|
||||
callout = AttrMapValue(None, desc="a user callout(self,g,x,y,(color,text))",advancedUsage=1),
|
||||
boxAnchor = AttrMapValue(isBoxAnchor,'Anchor point for the legend area'),
|
||||
variColumn = AttrMapValue(isBoolean,'If true column widths may vary (default is false)',advancedUsage=1),
|
||||
dividerLines = AttrMapValue(OneOf(0,1,2,3,4,5,6,7),'If 1 we have dividers between the rows | 2 for extra top | 4 for bottom',advancedUsage=1),
|
||||
dividerWidth = AttrMapValue(isNumber, desc="dividerLines width",advancedUsage=1),
|
||||
dividerColor = AttrMapValue(isColorOrNone, desc="dividerLines color",advancedUsage=1),
|
||||
dividerDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array for dividerLines.',advancedUsage=1),
|
||||
dividerOffsX = AttrMapValue(SequenceOf(isNumber,emptyOK=0,lo=2,hi=2), desc='divider lines X offsets',advancedUsage=1),
|
||||
dividerOffsY = AttrMapValue(isNumber, desc="dividerLines Y offset",advancedUsage=1),
|
||||
colEndCallout = AttrMapValue(None, desc="a user callout(self,g, x, xt, y,width, lWidth)",advancedUsage=1),
|
||||
subCols = AttrMapValue(None,desc="subColumn properties"),
|
||||
swatchCallout = AttrMapValue(None, desc="a user swatch callout(self,g,x,y,i,(col,name),swatch)",advancedUsage=1),
|
||||
swdx = AttrMapValue(isNumber, desc="x position adjustment for the swatch"),
|
||||
swdy = AttrMapValue(isNumber, desc="y position adjustment for the swatch"),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
# Upper-left reference point.
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
|
||||
# Alginment of text with respect to swatches.
|
||||
self.alignment = "left"
|
||||
|
||||
# x- and y-distances between neighbouring swatches.
|
||||
self.deltax = 75
|
||||
self.deltay = 20
|
||||
self.autoXPadding = 5
|
||||
self.autoYPadding = 2
|
||||
|
||||
# Size of swatch rectangle.
|
||||
self.dx = 10
|
||||
self.dy = 10
|
||||
|
||||
self.swdx = 0
|
||||
self.swdy = 0
|
||||
|
||||
# Distance between swatch rectangle and text.
|
||||
self.dxTextSpace = 10
|
||||
|
||||
# Max. number of items per column.
|
||||
self.columnMaximum = 3
|
||||
|
||||
# Color/name pairs.
|
||||
self.colorNamePairs = [ (colors.red, "red"),
|
||||
(colors.blue, "blue"),
|
||||
(colors.green, "green"),
|
||||
(colors.pink, "pink"),
|
||||
(colors.yellow, "yellow") ]
|
||||
|
||||
# Font name and size of the labels.
|
||||
self.fontName = STATE_DEFAULTS['fontName']
|
||||
self.fontSize = STATE_DEFAULTS['fontSize']
|
||||
self.fillColor = STATE_DEFAULTS['fillColor']
|
||||
self.strokeColor = STATE_DEFAULTS['strokeColor']
|
||||
self.strokeWidth = STATE_DEFAULTS['strokeWidth']
|
||||
self.swatchMarker = None
|
||||
self.boxAnchor = 'nw'
|
||||
self.yGap = 0
|
||||
self.variColumn = 0
|
||||
self.dividerLines = 0
|
||||
self.dividerWidth = 0.5
|
||||
self.dividerDashArray = None
|
||||
self.dividerColor = colors.black
|
||||
self.dividerOffsX = (0,0)
|
||||
self.dividerOffsY = 0
|
||||
self.colEndCallout = None
|
||||
self._init_subCols()
|
||||
|
||||
def _init_subCols(self):
|
||||
sc = self.subCols = TypedPropertyCollection(SubColProperty)
|
||||
sc.rpad = 1
|
||||
sc.dx = sc.dy = sc.minWidth = 0
|
||||
sc.align = 'right'
|
||||
sc[0].align = 'left'
|
||||
|
||||
def _getChartStyleName(self,chart):
|
||||
for a in 'lines', 'bars', 'slices', 'strands':
|
||||
if hasattr(chart,a): return a
|
||||
return None
|
||||
|
||||
def _getChartStyle(self,chart):
|
||||
return getattr(chart,self._getChartStyleName(chart),None)
|
||||
|
||||
def _getTexts(self,colorNamePairs):
|
||||
if not isAuto(colorNamePairs):
|
||||
texts = [_getStr(p[1]) for p in colorNamePairs]
|
||||
else:
|
||||
chart = getattr(colorNamePairs,'chart',getattr(colorNamePairs,'obj',None))
|
||||
texts = [chart.getSeriesName(i,'series %d' % i) for i in range(chart._seriesCount)]
|
||||
return texts
|
||||
|
||||
def _calculateMaxBoundaries(self, colorNamePairs):
|
||||
"Calculate the maximum width of some given strings."
|
||||
fontName = self.fontName
|
||||
fontSize = self.fontSize
|
||||
subCols = self.subCols
|
||||
|
||||
M = [_getWidths(i, m, fontName, fontSize, subCols) for i,m in enumerate(self._getTexts(colorNamePairs))]
|
||||
if not M:
|
||||
return [0,0]
|
||||
n = max([len(m) for m in M])
|
||||
if self.variColumn:
|
||||
columnMaximum = self.columnMaximum
|
||||
return [_transMax(n,M[r:r+columnMaximum]) for r in range(0,len(M),self.columnMaximum)]
|
||||
else:
|
||||
return _transMax(n,M)
|
||||
|
||||
def _calcHeight(self):
|
||||
dy = self.dy
|
||||
yGap = self.yGap
|
||||
thisy = upperlefty = self.y - dy
|
||||
fontSize = self.fontSize
|
||||
ascent=getFont(self.fontName).face.ascent/1000.
|
||||
if ascent==0: ascent=0.718 # default (from helvetica)
|
||||
ascent *= fontSize
|
||||
leading = fontSize*1.2
|
||||
deltay = self.deltay
|
||||
if not deltay: deltay = max(dy,leading)+self.autoYPadding
|
||||
columnCount = 0
|
||||
count = 0
|
||||
lowy = upperlefty
|
||||
lim = self.columnMaximum - 1
|
||||
for name in self._getTexts(self.colorNamePairs):
|
||||
y0 = thisy+(dy-ascent)*0.5
|
||||
y = y0 - _getLineCount(name)*leading
|
||||
leadingMove = 2*y0-y-thisy
|
||||
newy = thisy-max(deltay,leadingMove)-yGap
|
||||
lowy = min(y,newy,lowy)
|
||||
if count==lim:
|
||||
count = 0
|
||||
thisy = upperlefty
|
||||
columnCount = columnCount + 1
|
||||
else:
|
||||
thisy = newy
|
||||
count = count+1
|
||||
return upperlefty - lowy
|
||||
|
||||
def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor):
|
||||
return Rect(x, thisy, dx, dy,
|
||||
fillColor = fillColor,
|
||||
strokeColor = strokeColor,
|
||||
strokeWidth = strokeWidth,
|
||||
)
|
||||
|
||||
def draw(self):
|
||||
colorNamePairs = self.colorNamePairs
|
||||
autoCP = isAuto(colorNamePairs)
|
||||
if autoCP:
|
||||
chart = getattr(colorNamePairs,'chart',getattr(colorNamePairs,'obj',None))
|
||||
swatchMarker = None
|
||||
autoCP = Auto(obj=chart)
|
||||
n = chart._seriesCount
|
||||
chartTexts = self._getTexts(colorNamePairs)
|
||||
else:
|
||||
swatchMarker = getattr(self,'swatchMarker',None)
|
||||
if isAuto(swatchMarker):
|
||||
chart = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None))
|
||||
swatchMarker = Auto(obj=chart)
|
||||
n = len(colorNamePairs)
|
||||
dx = self.dx
|
||||
dy = self.dy
|
||||
alignment = self.alignment
|
||||
columnMaximum = self.columnMaximum
|
||||
deltax = self.deltax
|
||||
deltay = self.deltay
|
||||
dxTextSpace = self.dxTextSpace
|
||||
fontName = self.fontName
|
||||
fontSize = self.fontSize
|
||||
fillColor = self.fillColor
|
||||
strokeWidth = self.strokeWidth
|
||||
strokeColor = self.strokeColor
|
||||
subCols = self.subCols
|
||||
leading = fontSize*1.2
|
||||
yGap = self.yGap
|
||||
if not deltay:
|
||||
deltay = max(dy,leading)+self.autoYPadding
|
||||
ba = self.boxAnchor
|
||||
maxWidth = self._calculateMaxBoundaries(colorNamePairs)
|
||||
nCols = int((n+columnMaximum-1)/(columnMaximum*1.0))
|
||||
xW = dx+dxTextSpace+self.autoXPadding
|
||||
variColumn = self.variColumn
|
||||
if variColumn:
|
||||
width = reduce(operator.add,[m[-1] for m in maxWidth],0)+xW*nCols
|
||||
else:
|
||||
deltax = max(maxWidth[-1]+xW,deltax)
|
||||
width = maxWidth[-1]+nCols*deltax
|
||||
maxWidth = nCols*[maxWidth]
|
||||
|
||||
thisx = self.x
|
||||
thisy = self.y - self.dy
|
||||
if ba not in ('ne','n','nw','autoy'):
|
||||
height = self._calcHeight()
|
||||
if ba in ('e','c','w'):
|
||||
thisy += height/2.
|
||||
else:
|
||||
thisy += height
|
||||
if ba not in ('nw','w','sw','autox'):
|
||||
if ba in ('n','c','s'):
|
||||
thisx -= width/2
|
||||
else:
|
||||
thisx -= width
|
||||
upperlefty = thisy
|
||||
|
||||
g = Group()
|
||||
|
||||
ascent=getFont(fontName).face.ascent/1000.
|
||||
if ascent==0: ascent=0.718 # default (from helvetica)
|
||||
ascent *= fontSize # normalize
|
||||
|
||||
lim = columnMaximum - 1
|
||||
callout = getattr(self,'callout',None)
|
||||
scallout = getattr(self,'swatchCallout',None)
|
||||
dividerLines = self.dividerLines
|
||||
if dividerLines:
|
||||
dividerWidth = self.dividerWidth
|
||||
dividerColor = self.dividerColor
|
||||
dividerDashArray = self.dividerDashArray
|
||||
dividerOffsX = self.dividerOffsX
|
||||
dividerOffsY = self.dividerOffsY
|
||||
|
||||
for i in range(n):
|
||||
if autoCP:
|
||||
col = autoCP
|
||||
col.index = i
|
||||
name = chartTexts[i]
|
||||
else:
|
||||
col, name = colorNamePairs[i]
|
||||
if isAuto(swatchMarker):
|
||||
col = swatchMarker
|
||||
col.index = i
|
||||
if isAuto(name):
|
||||
name = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None)).getSeriesName(i,'series %d' % i)
|
||||
T = _getLines(name)
|
||||
S = []
|
||||
aS = S.append
|
||||
j = int(i/(columnMaximum*1.0))
|
||||
jOffs = maxWidth[j]
|
||||
|
||||
# thisy+dy/2 = y+leading/2
|
||||
y = y0 = thisy+(dy-ascent)*0.5
|
||||
|
||||
if callout: callout(self,g,thisx,y,(col,name))
|
||||
if alignment == "left":
|
||||
x = thisx
|
||||
xn = thisx+jOffs[-1]+dxTextSpace
|
||||
elif alignment == "right":
|
||||
x = thisx+dx+dxTextSpace
|
||||
xn = thisx
|
||||
else:
|
||||
raise ValueError("bad alignment")
|
||||
if not isSeq(name):
|
||||
T = [T]
|
||||
yd = y
|
||||
for k,lines in enumerate(T):
|
||||
y = y0
|
||||
kk = k*2
|
||||
x1 = x+jOffs[kk]
|
||||
x2 = x+jOffs[kk+1]
|
||||
sc = subCols[k,i]
|
||||
anchor = sc.align
|
||||
scdx = sc.dx
|
||||
scdy = sc.dy
|
||||
fN = getattr(sc,'fontName',fontName)
|
||||
fS = getattr(sc,'fontSize',fontSize)
|
||||
fC = getattr(sc,'fillColor',fillColor)
|
||||
fL = getattr(sc,'leading',1.2*fontSize)
|
||||
if fN==fontName:
|
||||
fA = (ascent*fS)/fontSize
|
||||
else:
|
||||
fA = getFont(fontName).face.ascent/1000.
|
||||
if fA==0: fA=0.718
|
||||
fA *= fS
|
||||
if anchor=='left':
|
||||
anchor = 'start'
|
||||
xoffs = x1
|
||||
elif anchor=='right':
|
||||
anchor = 'end'
|
||||
xoffs = x2
|
||||
elif anchor=='numeric':
|
||||
xoffs = x2
|
||||
else:
|
||||
anchor = 'middle'
|
||||
xoffs = 0.5*(x1+x2)
|
||||
for t in lines:
|
||||
aS(String(xoffs+scdx,y+scdy,t,fontName=fN,fontSize=fS,fillColor=fC, textAnchor = anchor))
|
||||
y -= fL
|
||||
yd = min(yd,y)
|
||||
y += fL
|
||||
for iy, a in ((y-max(fL-fA,0),'underlines'),(y+fA,'overlines')):
|
||||
il = getattr(sc,a,None)
|
||||
if il:
|
||||
if not isinstance(il,(tuple,list)): il = (il,)
|
||||
for l in il:
|
||||
l = copy.copy(l)
|
||||
l.y1 += iy
|
||||
l.y2 += iy
|
||||
l.x1 += x1
|
||||
l.x2 += x2
|
||||
aS(l)
|
||||
x = xn
|
||||
y = yd
|
||||
leadingMove = 2*y0-y-thisy
|
||||
|
||||
if dividerLines:
|
||||
xd = thisx+dx+dxTextSpace+jOffs[-1]+dividerOffsX[1]
|
||||
yd = thisy+dy*0.5+dividerOffsY
|
||||
if ((dividerLines&1) and i%columnMaximum) or ((dividerLines&2) and not i%columnMaximum):
|
||||
g.add(Line(thisx+dividerOffsX[0],yd,xd,yd,
|
||||
strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray))
|
||||
|
||||
if (dividerLines&4) and (i%columnMaximum==lim or i==(n-1)):
|
||||
yd -= max(deltay,leadingMove)+yGap
|
||||
g.add(Line(thisx+dividerOffsX[0],yd,xd,yd,
|
||||
strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray))
|
||||
|
||||
# Make a 'normal' color swatch...
|
||||
swatchX = x + getattr(self,'swdx',0)
|
||||
swatchY = thisy + getattr(self,'swdy',0)
|
||||
|
||||
if isAuto(col):
|
||||
chart = getattr(col,'chart',getattr(col,'obj',None))
|
||||
c = chart.makeSwatchSample(getattr(col,'index',i),swatchX,swatchY,dx,dy)
|
||||
elif isinstance(col, colors.Color):
|
||||
if isSymbol(swatchMarker):
|
||||
c = uSymbol2Symbol(swatchMarker,swatchX+dx/2.,swatchY+dy/2.,col)
|
||||
else:
|
||||
c = self._defaultSwatch(swatchX,swatchY,dx,dy,fillColor=col,strokeWidth=strokeWidth,strokeColor=strokeColor)
|
||||
elif col is not None:
|
||||
try:
|
||||
c = copy.deepcopy(col)
|
||||
c.x = swatchX
|
||||
c.y = swatchY
|
||||
c.width = dx
|
||||
c.height = dy
|
||||
except:
|
||||
c = None
|
||||
else:
|
||||
c = None
|
||||
|
||||
if c:
|
||||
g.add(c)
|
||||
if scallout: scallout(self,g,thisx,y0,i,(col,name),c)
|
||||
|
||||
for s in S: g.add(s)
|
||||
if self.colEndCallout and (i%columnMaximum==lim or i==(n-1)):
|
||||
if alignment == "left":
|
||||
xt = thisx
|
||||
else:
|
||||
xt = thisx+dx+dxTextSpace
|
||||
yd = thisy+dy*0.5+dividerOffsY - (max(deltay,leadingMove)+yGap)
|
||||
self.colEndCallout(self, g, thisx, xt, yd, jOffs[-1], jOffs[-1]+dx+dxTextSpace)
|
||||
|
||||
if i%columnMaximum==lim:
|
||||
if variColumn:
|
||||
thisx += jOffs[-1]+xW
|
||||
else:
|
||||
thisx = thisx+deltax
|
||||
thisy = upperlefty
|
||||
else:
|
||||
thisy = thisy-max(deltay,leadingMove)-yGap
|
||||
|
||||
return g
|
||||
|
||||
def demo(self):
|
||||
"Make sample legend."
|
||||
|
||||
d = Drawing(200, 100)
|
||||
|
||||
legend = Legend()
|
||||
legend.alignment = 'left'
|
||||
legend.x = 0
|
||||
legend.y = 100
|
||||
legend.dxTextSpace = 5
|
||||
items = 'red green blue yellow pink black white'.split()
|
||||
items = [(getattr(colors, i), i) for i in items]
|
||||
legend.colorNamePairs = items
|
||||
|
||||
d.add(legend, 'legend')
|
||||
|
||||
return d
|
||||
|
||||
class TotalAnnotator(LegendColEndCallout):
|
||||
def __init__(self, lText='Total', rText='0.0', fontName=_baseGFontName, fontSize=10,
|
||||
fillColor=colors.black, strokeWidth=0.5, strokeColor=colors.black, strokeDashArray=None,
|
||||
dx=0, dy=0, dly=0, dlx=(0,0)):
|
||||
self.lText = lText
|
||||
self.rText = rText
|
||||
self.fontName = fontName
|
||||
self.fontSize = fontSize
|
||||
self.fillColor = fillColor
|
||||
self.dy = dy
|
||||
self.dx = dx
|
||||
self.dly = dly
|
||||
self.dlx = dlx
|
||||
self.strokeWidth = strokeWidth
|
||||
self.strokeColor = strokeColor
|
||||
self.strokeDashArray = strokeDashArray
|
||||
|
||||
def __call__(self,legend, g, x, xt, y, width, lWidth):
|
||||
from reportlab.graphics.shapes import String, Line
|
||||
fontSize = self.fontSize
|
||||
fontName = self.fontName
|
||||
fillColor = self.fillColor
|
||||
strokeColor = self.strokeColor
|
||||
strokeWidth = self.strokeWidth
|
||||
ascent=getFont(fontName).face.ascent/1000.
|
||||
if ascent==0: ascent=0.718 # default (from helvetica)
|
||||
ascent *= fontSize
|
||||
leading = fontSize*1.2
|
||||
yt = y+self.dy-ascent*1.3
|
||||
if self.lText and fillColor:
|
||||
g.add(String(xt,yt,self.lText,
|
||||
fontName=fontName,
|
||||
fontSize=fontSize,
|
||||
fillColor=fillColor,
|
||||
textAnchor = "start"))
|
||||
if self.rText:
|
||||
g.add(String(xt+width,yt,self.rText,
|
||||
fontName=fontName,
|
||||
fontSize=fontSize,
|
||||
fillColor=fillColor,
|
||||
textAnchor = "end"))
|
||||
if strokeWidth and strokeColor:
|
||||
yL = y+self.dly-leading
|
||||
g.add(Line(x+self.dlx[0],yL,x+self.dlx[1]+lWidth,yL,
|
||||
strokeColor=strokeColor, strokeWidth=strokeWidth,
|
||||
strokeDashArray=self.strokeDashArray))
|
||||
|
||||
class LineSwatch(Widget):
|
||||
"""basically a Line with properties added so it can be used in a LineLegend"""
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumber, desc="x-coordinate for swatch line start point"),
|
||||
y = AttrMapValue(isNumber, desc="y-coordinate for swatch line start point"),
|
||||
width = AttrMapValue(isNumber, desc="length of swatch line"),
|
||||
height = AttrMapValue(isNumber, desc="used for line strokeWidth"),
|
||||
strokeColor = AttrMapValue(isColorOrNone, desc="color of swatch line"),
|
||||
strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc="dash array for swatch line"),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
from reportlab.lib.colors import red
|
||||
from reportlab.graphics.shapes import Line
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.width = 20
|
||||
self.height = 1
|
||||
self.strokeColor = red
|
||||
self.strokeDashArray = None
|
||||
|
||||
def draw(self):
|
||||
l = Line(self.x,self.y,self.x+self.width,self.y)
|
||||
l.strokeColor = self.strokeColor
|
||||
l.strokeDashArray = self.strokeDashArray
|
||||
l.strokeWidth = self.height
|
||||
return l
|
||||
|
||||
class LineLegend(Legend):
|
||||
"""A subclass of Legend for drawing legends with lines as the
|
||||
swatches rather than rectangles. Useful for lineCharts and
|
||||
linePlots. Should be similar in all other ways the the standard
|
||||
Legend class.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
Legend.__init__(self)
|
||||
|
||||
# Size of swatch rectangle.
|
||||
self.dx = 10
|
||||
self.dy = 2
|
||||
|
||||
def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor):
|
||||
l = LineSwatch()
|
||||
l.x = x
|
||||
l.y = thisy
|
||||
l.width = dx
|
||||
l.height = dy
|
||||
l.strokeColor = fillColor
|
||||
return l
|
||||
|
||||
def sample1c():
|
||||
"Make sample legend."
|
||||
|
||||
d = Drawing(200, 100)
|
||||
|
||||
legend = Legend()
|
||||
legend.alignment = 'right'
|
||||
legend.x = 0
|
||||
legend.y = 100
|
||||
legend.dxTextSpace = 5
|
||||
items = 'red green blue yellow pink black white'.split()
|
||||
items = [(getattr(colors, i), i) for i in items]
|
||||
legend.colorNamePairs = items
|
||||
|
||||
d.add(legend, 'legend')
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def sample2c():
|
||||
"Make sample legend."
|
||||
|
||||
d = Drawing(200, 100)
|
||||
|
||||
legend = Legend()
|
||||
legend.alignment = 'right'
|
||||
legend.x = 20
|
||||
legend.y = 90
|
||||
legend.deltax = 60
|
||||
legend.dxTextSpace = 10
|
||||
legend.columnMaximum = 4
|
||||
items = 'red green blue yellow pink black white'.split()
|
||||
items = [(getattr(colors, i), i) for i in items]
|
||||
legend.colorNamePairs = items
|
||||
|
||||
d.add(legend, 'legend')
|
||||
|
||||
return d
|
||||
|
||||
def sample3():
|
||||
"Make sample legend with line swatches."
|
||||
|
||||
d = Drawing(200, 100)
|
||||
|
||||
legend = LineLegend()
|
||||
legend.alignment = 'right'
|
||||
legend.x = 20
|
||||
legend.y = 90
|
||||
legend.deltax = 60
|
||||
legend.dxTextSpace = 10
|
||||
legend.columnMaximum = 4
|
||||
items = 'red green blue yellow pink black white'.split()
|
||||
items = [(getattr(colors, i), i) for i in items]
|
||||
legend.colorNamePairs = items
|
||||
d.add(legend, 'legend')
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def sample3a():
|
||||
"Make sample legend with line swatches and dasharrays on the lines."
|
||||
|
||||
d = Drawing(200, 100)
|
||||
|
||||
legend = LineLegend()
|
||||
legend.alignment = 'right'
|
||||
legend.x = 20
|
||||
legend.y = 90
|
||||
legend.deltax = 60
|
||||
legend.dxTextSpace = 10
|
||||
legend.columnMaximum = 4
|
||||
items = 'red green blue yellow pink black white'.split()
|
||||
darrays = ([2,1], [2,5], [2,2,5,5], [1,2,3,4], [4,2,3,4], [1,2,3,4,5,6], [1])
|
||||
cnp = []
|
||||
for i in range(0, len(items)):
|
||||
l = LineSwatch()
|
||||
l.strokeColor = getattr(colors, items[i])
|
||||
l.strokeDashArray = darrays[i]
|
||||
cnp.append((l, items[i]))
|
||||
legend.colorNamePairs = cnp
|
||||
d.add(legend, 'legend')
|
||||
|
||||
return d
|
||||
715
reportlab/graphics/charts/linecharts.py
Normal file
715
reportlab/graphics/charts/linecharts.py
Normal file
@@ -0,0 +1,715 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2004
|
||||
#see license.txt for license details
|
||||
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/linecharts.py
|
||||
|
||||
__version__=''' $Id$ '''
|
||||
__doc__="""This modules defines a very preliminary Line Chart example."""
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isNumber, isNumberOrNone, isColor, isColorOrNone, isListOfStrings, \
|
||||
isListOfStringsOrNone, SequenceOf, isBoolean, NoneOr, \
|
||||
isListOfNumbersOrNone, isStringOrNone, OneOf, Percentage
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
|
||||
from reportlab.graphics.shapes import Line, Rect, Group, Drawing, Polygon, PolyLine
|
||||
from reportlab.graphics.widgets.signsandsymbols import NoEntry
|
||||
from reportlab.graphics.charts.axes import XCategoryAxis, YValueAxis
|
||||
from reportlab.graphics.charts.textlabels import Label
|
||||
from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol, makeMarker
|
||||
from reportlab.graphics.charts.areas import PlotArea
|
||||
from reportlab.graphics.charts.legends import _objStr
|
||||
|
||||
class LineChartProperties(PropHolder):
|
||||
_attrMap = AttrMap(
|
||||
strokeWidth = AttrMapValue(isNumber, desc='Width of a line.'),
|
||||
strokeColor = AttrMapValue(isColorOrNone, desc='Color of a line or border.'),
|
||||
fillColor = AttrMapValue(isColorOrNone, desc='fill color of a bar.'),
|
||||
strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array of a line.'),
|
||||
symbol = AttrMapValue(NoneOr(isSymbol), desc='Widget placed at data points.',advancedUsage=1),
|
||||
shader = AttrMapValue(None, desc='Shader Class.',advancedUsage=1),
|
||||
filler = AttrMapValue(None, desc='Filler Class.',advancedUsage=1),
|
||||
name = AttrMapValue(isStringOrNone, desc='Name of the line.'),
|
||||
lineStyle = AttrMapValue(NoneOr(OneOf('line','joinedLine','bar')), desc="What kind of plot this line is",advancedUsage=1),
|
||||
barWidth = AttrMapValue(isNumberOrNone,desc="Percentage of available width to be used for a bar",advancedUsage=1),
|
||||
)
|
||||
|
||||
class AbstractLineChart(PlotArea):
|
||||
|
||||
def makeSwatchSample(self,rowNo, x, y, width, height):
|
||||
baseStyle = self.lines
|
||||
styleIdx = rowNo % len(baseStyle)
|
||||
style = baseStyle[styleIdx]
|
||||
color = style.strokeColor
|
||||
yh2 = y+height/2.
|
||||
lineStyle = getattr(style,'lineStyle',None)
|
||||
if lineStyle=='bar':
|
||||
dash = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None))
|
||||
strokeWidth= getattr(style, 'strokeWidth', getattr(style, 'strokeWidth',None))
|
||||
L = Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=color,strokeLineCap=0,strokeDashArray=dash,fillColor=getattr(style,'fillColor',color))
|
||||
elif self.joinedLines or lineStyle=='joinedLine':
|
||||
dash = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None))
|
||||
strokeWidth= getattr(style, 'strokeWidth', getattr(style, 'strokeWidth',None))
|
||||
L = Line(x,yh2,x+width,yh2,strokeColor=color,strokeLineCap=0)
|
||||
if strokeWidth: L.strokeWidth = strokeWidth
|
||||
if dash: L.strokeDashArray = dash
|
||||
else:
|
||||
L = None
|
||||
|
||||
if hasattr(style, 'symbol'):
|
||||
S = style.symbol
|
||||
elif hasattr(baseStyle, 'symbol'):
|
||||
S = baseStyle.symbol
|
||||
else:
|
||||
S = None
|
||||
|
||||
if S: S = uSymbol2Symbol(S,x+width/2.,yh2,color)
|
||||
if S and L:
|
||||
g = Group()
|
||||
g.add(L)
|
||||
g.add(S)
|
||||
return g
|
||||
return S or L
|
||||
|
||||
def getSeriesName(self,i,default=None):
|
||||
'''return series name i or default'''
|
||||
return _objStr(getattr(self.lines[i],'name',default))
|
||||
|
||||
class LineChart(AbstractLineChart):
|
||||
pass
|
||||
|
||||
# This is conceptually similar to the VerticalBarChart.
|
||||
# Still it is better named HorizontalLineChart... :-/
|
||||
|
||||
class HorizontalLineChart(LineChart):
|
||||
"""Line chart with multiple lines.
|
||||
|
||||
A line chart is assumed to have one category and one value axis.
|
||||
Despite its generic name this particular line chart class has
|
||||
a vertical value axis and a horizontal category one. It may
|
||||
evolve into individual horizontal and vertical variants (like
|
||||
with the existing bar charts).
|
||||
|
||||
Available attributes are:
|
||||
|
||||
x: x-position of lower-left chart origin
|
||||
y: y-position of lower-left chart origin
|
||||
width: chart width
|
||||
height: chart height
|
||||
|
||||
useAbsolute: disables auto-scaling of chart elements (?)
|
||||
lineLabelNudge: distance of data labels to data points
|
||||
lineLabels: labels associated with data values
|
||||
lineLabelFormat: format string or callback function
|
||||
groupSpacing: space between categories
|
||||
|
||||
joinedLines: enables drawing of lines
|
||||
|
||||
strokeColor: color of chart lines (?)
|
||||
fillColor: color for chart background (?)
|
||||
lines: style list, used cyclically for data series
|
||||
|
||||
valueAxis: value axis object
|
||||
categoryAxis: category axis object
|
||||
categoryNames: category names
|
||||
|
||||
data: chart data, a list of data series of equal length
|
||||
"""
|
||||
|
||||
_attrMap = AttrMap(BASE=LineChart,
|
||||
useAbsolute = AttrMapValue(isNumber, desc='Flag to use absolute spacing values.',advancedUsage=1),
|
||||
lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.',advancedUsage=1),
|
||||
lineLabels = AttrMapValue(None, desc='Handle to the list of data point labels.'),
|
||||
lineLabelFormat = AttrMapValue(None, desc='Formatting string or function used for data point labels.'),
|
||||
lineLabelArray = AttrMapValue(None, desc='explicit array of line label values, must match size of data if present.'),
|
||||
groupSpacing = AttrMapValue(isNumber, desc='? - Likely to disappear.'),
|
||||
joinedLines = AttrMapValue(isNumber, desc='Display data points joined with lines if true.'),
|
||||
lines = AttrMapValue(None, desc='Handle of the lines.'),
|
||||
valueAxis = AttrMapValue(None, desc='Handle of the value axis.'),
|
||||
categoryAxis = AttrMapValue(None, desc='Handle of the category axis.'),
|
||||
categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'),
|
||||
data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'),
|
||||
inFill = AttrMapValue(isBoolean, desc='Whether infilling should be done.',advancedUsage=1),
|
||||
reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.',advancedUsage=1),
|
||||
annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.',advancedUsage=1),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
LineChart.__init__(self)
|
||||
|
||||
# Allow for a bounding rectangle.
|
||||
self.strokeColor = None
|
||||
self.fillColor = None
|
||||
|
||||
# Named so we have less recoding for the horizontal one :-)
|
||||
self.categoryAxis = XCategoryAxis()
|
||||
self.valueAxis = YValueAxis()
|
||||
|
||||
# This defines two series of 3 points. Just an example.
|
||||
self.data = [(100,110,120,130),
|
||||
(70, 80, 80, 90)]
|
||||
self.categoryNames = ('North','South','East','West')
|
||||
|
||||
self.lines = TypedPropertyCollection(LineChartProperties)
|
||||
self.lines.strokeWidth = 1
|
||||
self.lines[0].strokeColor = colors.red
|
||||
self.lines[1].strokeColor = colors.green
|
||||
self.lines[2].strokeColor = colors.blue
|
||||
|
||||
# control spacing. if useAbsolute = 1 then
|
||||
# the next parameters are in points; otherwise
|
||||
# they are 'proportions' and are normalized to
|
||||
# fit the available space.
|
||||
self.useAbsolute = 0 #- not done yet
|
||||
self.groupSpacing = 1 #5
|
||||
|
||||
self.lineLabels = TypedPropertyCollection(Label)
|
||||
self.lineLabelFormat = None
|
||||
self.lineLabelArray = None
|
||||
|
||||
# This says whether the origin is above or below
|
||||
# the data point. +10 means put the origin ten points
|
||||
# above the data point if value > 0, or ten
|
||||
# points below if data value < 0. This is different
|
||||
# to label dx/dy which are not dependent on the
|
||||
# sign of the data.
|
||||
self.lineLabelNudge = 10
|
||||
# If you have multiple series, by default they butt
|
||||
# together.
|
||||
|
||||
# New line chart attributes.
|
||||
self.joinedLines = 1 # Connect items with straight lines.
|
||||
self.inFill = 0
|
||||
self.reversePlotOrder = 0
|
||||
|
||||
|
||||
def demo(self):
|
||||
"""Shows basic use of a line chart."""
|
||||
|
||||
drawing = Drawing(200, 100)
|
||||
|
||||
data = [
|
||||
(13, 5, 20, 22, 37, 45, 19, 4),
|
||||
(14, 10, 21, 28, 38, 46, 25, 5)
|
||||
]
|
||||
|
||||
lc = HorizontalLineChart()
|
||||
|
||||
lc.x = 20
|
||||
lc.y = 10
|
||||
lc.height = 85
|
||||
lc.width = 170
|
||||
lc.data = data
|
||||
lc.lines.symbol = makeMarker('Circle')
|
||||
|
||||
drawing.add(lc)
|
||||
|
||||
return drawing
|
||||
|
||||
|
||||
def calcPositions(self):
|
||||
"""Works out where they go.
|
||||
|
||||
Sets an attribute _positions which is a list of
|
||||
lists of (x, y) matching the data.
|
||||
"""
|
||||
|
||||
self._seriesCount = len(self.data)
|
||||
self._rowLength = max(list(map(len,self.data)))
|
||||
|
||||
if self.useAbsolute:
|
||||
# Dimensions are absolute.
|
||||
normFactor = 1.0
|
||||
else:
|
||||
# Dimensions are normalized to fit.
|
||||
normWidth = self.groupSpacing
|
||||
availWidth = self.categoryAxis.scale(0)[1]
|
||||
normFactor = availWidth / normWidth
|
||||
self._normFactor = normFactor
|
||||
self._yzero = yzero = self.valueAxis.scale(0)
|
||||
self._hngs = hngs = 0.5 * self.groupSpacing * normFactor
|
||||
|
||||
self._positions = []
|
||||
for rowNo in range(len(self.data)):
|
||||
lineRow = []
|
||||
for colNo in range(len(self.data[rowNo])):
|
||||
datum = self.data[rowNo][colNo]
|
||||
if datum is not None:
|
||||
(groupX, groupWidth) = self.categoryAxis.scale(colNo)
|
||||
x = groupX + hngs
|
||||
y = yzero
|
||||
height = self.valueAxis.scale(datum) - y
|
||||
lineRow.append((x, y+height))
|
||||
self._positions.append(lineRow)
|
||||
|
||||
|
||||
def _innerDrawLabel(self, rowNo, colNo, x, y):
|
||||
"Draw a label for a given item in the list."
|
||||
|
||||
labelFmt = self.lineLabelFormat
|
||||
labelValue = self.data[rowNo][colNo]
|
||||
|
||||
if labelFmt is None:
|
||||
labelText = None
|
||||
elif type(labelFmt) is str:
|
||||
if labelFmt == 'values':
|
||||
try:
|
||||
labelText = self.lineLabelArray[rowNo][colNo]
|
||||
except:
|
||||
labelText = None
|
||||
else:
|
||||
labelText = labelFmt % labelValue
|
||||
elif hasattr(labelFmt,'__call__'):
|
||||
labelText = labelFmt(labelValue)
|
||||
else:
|
||||
raise ValueError("Unknown formatter type %s, expected string or function"%labelFmt)
|
||||
|
||||
if labelText:
|
||||
label = self.lineLabels[(rowNo, colNo)]
|
||||
if not label.visible: return
|
||||
# Make sure labels are some distance off the data point.
|
||||
if y > 0:
|
||||
label.setOrigin(x, y + self.lineLabelNudge)
|
||||
else:
|
||||
label.setOrigin(x, y - self.lineLabelNudge)
|
||||
label.setText(labelText)
|
||||
else:
|
||||
label = None
|
||||
return label
|
||||
|
||||
def drawLabel(self, G, rowNo, colNo, x, y):
|
||||
'''Draw a label for a given item in the list.
|
||||
G must have an add method'''
|
||||
G.add(self._innerDrawLabel(rowNo,colNo,x,y))
|
||||
|
||||
def makeLines(self):
|
||||
g = Group()
|
||||
|
||||
labelFmt = self.lineLabelFormat
|
||||
P = list(range(len(self._positions)))
|
||||
if self.reversePlotOrder: P.reverse()
|
||||
inFill = self.inFill
|
||||
if inFill:
|
||||
inFillY = self.categoryAxis._y
|
||||
inFillX0 = self.valueAxis._x
|
||||
inFillX1 = inFillX0 + self.categoryAxis._length
|
||||
inFillG = getattr(self,'_inFillG',g)
|
||||
yzero = self._yzero
|
||||
|
||||
# Iterate over data rows.
|
||||
for rowNo in P:
|
||||
row = self._positions[rowNo]
|
||||
styleCount = len(self.lines)
|
||||
styleIdx = rowNo % styleCount
|
||||
rowStyle = self.lines[styleIdx]
|
||||
rowColor = rowStyle.strokeColor
|
||||
dash = getattr(rowStyle, 'strokeDashArray', None)
|
||||
lineStyle = getattr(rowStyle,'lineStyle',None)
|
||||
|
||||
if hasattr(rowStyle, 'strokeWidth'):
|
||||
strokeWidth = rowStyle.strokeWidth
|
||||
elif hasattr(self.lines, 'strokeWidth'):
|
||||
strokeWidth = self.lines.strokeWidth
|
||||
else:
|
||||
strokeWidth = None
|
||||
|
||||
# Iterate over data columns.
|
||||
if lineStyle=='bar':
|
||||
barWidth = getattr(rowStyle,'barWidth',Percentage(50))
|
||||
fillColor = getattr(rowStyle,'fillColor',rowColor)
|
||||
if isinstance(barWidth,Percentage):
|
||||
hbw = self._hngs*barWidth*0.01
|
||||
else:
|
||||
hbw = barWidth*0.5
|
||||
for colNo in range(len(row)):
|
||||
x,y = row[colNo]
|
||||
g.add(Rect(x-hbw,min(y,yzero),2*hbw,abs(y-yzero),strokeWidth=strokeWidth,strokeColor=rowColor,fillColor=fillColor))
|
||||
elif self.joinedLines or lineStyle=='joinedLine':
|
||||
points = []
|
||||
for colNo in range(len(row)):
|
||||
points += row[colNo]
|
||||
if inFill:
|
||||
points = points + [inFillX1,inFillY,inFillX0,inFillY]
|
||||
inFillG.add(Polygon(points,fillColor=rowColor,strokeColor=rowColor,strokeWidth=0.1))
|
||||
else:
|
||||
line = PolyLine(points,strokeColor=rowColor,strokeLineCap=0,strokeLineJoin=1)
|
||||
if strokeWidth:
|
||||
line.strokeWidth = strokeWidth
|
||||
if dash:
|
||||
line.strokeDashArray = dash
|
||||
g.add(line)
|
||||
|
||||
if hasattr(rowStyle, 'symbol'):
|
||||
uSymbol = rowStyle.symbol
|
||||
elif hasattr(self.lines, 'symbol'):
|
||||
uSymbol = self.lines.symbol
|
||||
else:
|
||||
uSymbol = None
|
||||
|
||||
if uSymbol:
|
||||
for colNo in range(len(row)):
|
||||
x1, y1 = row[colNo]
|
||||
symbol = uSymbol2Symbol(uSymbol,x1,y1,rowStyle.strokeColor)
|
||||
if symbol: g.add(symbol)
|
||||
|
||||
# Draw item labels.
|
||||
for colNo in range(len(row)):
|
||||
x1, y1 = row[colNo]
|
||||
self.drawLabel(g, rowNo, colNo, x1, y1)
|
||||
|
||||
return g
|
||||
|
||||
def draw(self):
|
||||
"Draws itself."
|
||||
|
||||
vA, cA = self.valueAxis, self.categoryAxis
|
||||
vA.setPosition(self.x, self.y, self.height)
|
||||
if vA: vA.joinAxis = cA
|
||||
if cA: cA.joinAxis = vA
|
||||
vA.configure(self.data)
|
||||
|
||||
# If zero is in chart, put x axis there, otherwise
|
||||
# use bottom.
|
||||
xAxisCrossesAt = vA.scale(0)
|
||||
if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)):
|
||||
y = self.y
|
||||
else:
|
||||
y = xAxisCrossesAt
|
||||
|
||||
cA.setPosition(self.x, y, self.width)
|
||||
cA.configure(self.data)
|
||||
|
||||
self.calcPositions()
|
||||
|
||||
g = Group()
|
||||
g.add(self.makeBackground())
|
||||
if self.inFill:
|
||||
self._inFillG = Group()
|
||||
g.add(self._inFillG)
|
||||
|
||||
g.add(cA)
|
||||
g.add(vA)
|
||||
cAdgl = getattr(cA,'drawGridLast',False)
|
||||
vAdgl = getattr(vA,'drawGridLast',False)
|
||||
if not cAdgl: cA.makeGrid(g,parent=self,dim=vA.getGridDims)
|
||||
if not vAdgl: vA.makeGrid(g,parent=self,dim=cA.getGridDims)
|
||||
g.add(self.makeLines())
|
||||
if cAdgl: cA.makeGrid(g,parent=self,dim=vA.getGridDims)
|
||||
if vAdgl: vA.makeGrid(g,parent=self,dim=cA.getGridDims)
|
||||
for a in getattr(self,'annotations',()): g.add(a(self,cA.scale,vA.scale))
|
||||
return g
|
||||
|
||||
def _fakeItemKey(a):
|
||||
'''t, z0, z1, x, y = a[:5]'''
|
||||
return (-a[1],a[3],a[0],-a[4])
|
||||
|
||||
class _FakeGroup:
|
||||
def __init__(self):
|
||||
self._data = []
|
||||
|
||||
def add(self,what):
|
||||
if what: self._data.append(what)
|
||||
|
||||
def value(self):
|
||||
return self._data
|
||||
|
||||
def sort(self):
|
||||
self._data.sort(key=_fakeItemKey)
|
||||
#for t in self._data: print t
|
||||
|
||||
class HorizontalLineChart3D(HorizontalLineChart):
|
||||
_attrMap = AttrMap(BASE=HorizontalLineChart,
|
||||
theta_x = AttrMapValue(isNumber, desc='dx/dz'),
|
||||
theta_y = AttrMapValue(isNumber, desc='dy/dz'),
|
||||
zDepth = AttrMapValue(isNumber, desc='depth of an individual series'),
|
||||
zSpace = AttrMapValue(isNumber, desc='z gap around series'),
|
||||
)
|
||||
theta_x = .5
|
||||
theta_y = .5
|
||||
zDepth = 10
|
||||
zSpace = 3
|
||||
|
||||
def calcPositions(self):
|
||||
HorizontalLineChart.calcPositions(self)
|
||||
nSeries = self._seriesCount
|
||||
zSpace = self.zSpace
|
||||
zDepth = self.zDepth
|
||||
if self.categoryAxis.style=='parallel_3d':
|
||||
_3d_depth = nSeries*zDepth+(nSeries+1)*zSpace
|
||||
else:
|
||||
_3d_depth = zDepth + 2*zSpace
|
||||
self._3d_dx = self.theta_x*_3d_depth
|
||||
self._3d_dy = self.theta_y*_3d_depth
|
||||
|
||||
def _calc_z0(self,rowNo):
|
||||
zSpace = self.zSpace
|
||||
if self.categoryAxis.style=='parallel_3d':
|
||||
z0 = rowNo*(self.zDepth+zSpace)+zSpace
|
||||
else:
|
||||
z0 = zSpace
|
||||
return z0
|
||||
|
||||
def _zadjust(self,x,y,z):
|
||||
return x+z*self.theta_x, y+z*self.theta_y
|
||||
|
||||
def makeLines(self):
|
||||
labelFmt = self.lineLabelFormat
|
||||
P = list(range(len(self._positions)))
|
||||
if self.reversePlotOrder: P.reverse()
|
||||
inFill = self.inFill
|
||||
assert not inFill, "inFill not supported for 3d yet"
|
||||
#if inFill:
|
||||
#inFillY = self.categoryAxis._y
|
||||
#inFillX0 = self.valueAxis._x
|
||||
#inFillX1 = inFillX0 + self.categoryAxis._length
|
||||
#inFillG = getattr(self,'_inFillG',g)
|
||||
zDepth = self.zDepth
|
||||
_zadjust = self._zadjust
|
||||
theta_x = self.theta_x
|
||||
theta_y = self.theta_y
|
||||
F = _FakeGroup()
|
||||
from reportlab.graphics.charts.utils3d import _make_3d_line_info
|
||||
tileWidth = getattr(self,'_3d_tilewidth',None)
|
||||
if not tileWidth and self.categoryAxis.style!='parallel_3d': tileWidth = 1
|
||||
|
||||
# Iterate over data rows.
|
||||
for rowNo in P:
|
||||
row = self._positions[rowNo]
|
||||
n = len(row)
|
||||
styleCount = len(self.lines)
|
||||
styleIdx = rowNo % styleCount
|
||||
rowStyle = self.lines[styleIdx]
|
||||
rowColor = rowStyle.strokeColor
|
||||
dash = getattr(rowStyle, 'strokeDashArray', None)
|
||||
z0 = self._calc_z0(rowNo)
|
||||
z1 = z0 + zDepth
|
||||
|
||||
if hasattr(self.lines[styleIdx], 'strokeWidth'):
|
||||
strokeWidth = self.lines[styleIdx].strokeWidth
|
||||
elif hasattr(self.lines, 'strokeWidth'):
|
||||
strokeWidth = self.lines.strokeWidth
|
||||
else:
|
||||
strokeWidth = None
|
||||
|
||||
# Iterate over data columns.
|
||||
if self.joinedLines:
|
||||
if n:
|
||||
x0, y0 = row[0]
|
||||
for colNo in range(1,n):
|
||||
x1, y1 = row[colNo]
|
||||
_make_3d_line_info( F, x0, x1, y0, y1, z0, z1,
|
||||
theta_x, theta_y,
|
||||
rowColor, fillColorShaded=None, tileWidth=tileWidth,
|
||||
strokeColor=None, strokeWidth=None, strokeDashArray=None,
|
||||
shading=0.1)
|
||||
x0, y0 = x1, y1
|
||||
|
||||
if hasattr(self.lines[styleIdx], 'symbol'):
|
||||
uSymbol = self.lines[styleIdx].symbol
|
||||
elif hasattr(self.lines, 'symbol'):
|
||||
uSymbol = self.lines.symbol
|
||||
else:
|
||||
uSymbol = None
|
||||
|
||||
if uSymbol:
|
||||
for colNo in range(n):
|
||||
x1, y1 = row[colNo]
|
||||
x1, y1 = _zadjust(x1,y1,z0)
|
||||
symbol = uSymbol2Symbol(uSymbol,x1,y1,rowColor)
|
||||
if symbol: F.add((2,z0,z0,x1,y1,symbol))
|
||||
|
||||
# Draw item labels.
|
||||
for colNo in range(n):
|
||||
x1, y1 = row[colNo]
|
||||
x1, y1 = _zadjust(x1,y1,z0)
|
||||
L = self._innerDrawLabel(rowNo, colNo, x1, y1)
|
||||
if L: F.add((2,z0,z0,x1,y1,L))
|
||||
|
||||
F.sort()
|
||||
g = Group()
|
||||
for v in F.value(): g.add(v[-1])
|
||||
return g
|
||||
|
||||
class VerticalLineChart(LineChart):
|
||||
pass
|
||||
|
||||
|
||||
def sample1():
|
||||
drawing = Drawing(400, 200)
|
||||
|
||||
data = [
|
||||
(13, 5, 20, 22, 37, 45, 19, 4),
|
||||
(5, 20, 46, 38, 23, 21, 6, 14)
|
||||
]
|
||||
|
||||
lc = HorizontalLineChart()
|
||||
|
||||
lc.x = 50
|
||||
lc.y = 50
|
||||
lc.height = 125
|
||||
lc.width = 300
|
||||
lc.data = data
|
||||
lc.joinedLines = 1
|
||||
lc.lines.symbol = makeMarker('FilledDiamond')
|
||||
lc.lineLabelFormat = '%2.0f'
|
||||
|
||||
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
|
||||
lc.categoryAxis.categoryNames = catNames
|
||||
lc.categoryAxis.labels.boxAnchor = 'n'
|
||||
|
||||
lc.valueAxis.valueMin = 0
|
||||
lc.valueAxis.valueMax = 60
|
||||
lc.valueAxis.valueStep = 15
|
||||
|
||||
drawing.add(lc)
|
||||
|
||||
return drawing
|
||||
|
||||
|
||||
class SampleHorizontalLineChart(HorizontalLineChart):
|
||||
"Sample class overwriting one method to draw additional horizontal lines."
|
||||
|
||||
def demo(self):
|
||||
"""Shows basic use of a line chart."""
|
||||
|
||||
drawing = Drawing(200, 100)
|
||||
|
||||
data = [
|
||||
(13, 5, 20, 22, 37, 45, 19, 4),
|
||||
(14, 10, 21, 28, 38, 46, 25, 5)
|
||||
]
|
||||
|
||||
lc = SampleHorizontalLineChart()
|
||||
|
||||
lc.x = 20
|
||||
lc.y = 10
|
||||
lc.height = 85
|
||||
lc.width = 170
|
||||
lc.data = data
|
||||
lc.strokeColor = colors.white
|
||||
lc.fillColor = colors.HexColor(0xCCCCCC)
|
||||
|
||||
drawing.add(lc)
|
||||
|
||||
return drawing
|
||||
|
||||
|
||||
def makeBackground(self):
|
||||
g = Group()
|
||||
|
||||
g.add(HorizontalLineChart.makeBackground(self))
|
||||
|
||||
valAxis = self.valueAxis
|
||||
valTickPositions = valAxis._tickValues
|
||||
|
||||
for y in valTickPositions:
|
||||
y = valAxis.scale(y)
|
||||
g.add(Line(self.x, y, self.x+self.width, y,
|
||||
strokeColor = self.strokeColor))
|
||||
|
||||
return g
|
||||
|
||||
|
||||
|
||||
def sample1a():
|
||||
drawing = Drawing(400, 200)
|
||||
|
||||
data = [
|
||||
(13, 5, 20, 22, 37, 45, 19, 4),
|
||||
(5, 20, 46, 38, 23, 21, 6, 14)
|
||||
]
|
||||
|
||||
lc = SampleHorizontalLineChart()
|
||||
|
||||
lc.x = 50
|
||||
lc.y = 50
|
||||
lc.height = 125
|
||||
lc.width = 300
|
||||
lc.data = data
|
||||
lc.joinedLines = 1
|
||||
lc.strokeColor = colors.white
|
||||
lc.fillColor = colors.HexColor(0xCCCCCC)
|
||||
lc.lines.symbol = makeMarker('FilledDiamond')
|
||||
lc.lineLabelFormat = '%2.0f'
|
||||
|
||||
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
|
||||
lc.categoryAxis.categoryNames = catNames
|
||||
lc.categoryAxis.labels.boxAnchor = 'n'
|
||||
|
||||
lc.valueAxis.valueMin = 0
|
||||
lc.valueAxis.valueMax = 60
|
||||
lc.valueAxis.valueStep = 15
|
||||
|
||||
drawing.add(lc)
|
||||
|
||||
return drawing
|
||||
|
||||
|
||||
def sample2():
|
||||
drawing = Drawing(400, 200)
|
||||
|
||||
data = [
|
||||
(13, 5, 20, 22, 37, 45, 19, 4),
|
||||
(5, 20, 46, 38, 23, 21, 6, 14)
|
||||
]
|
||||
|
||||
lc = HorizontalLineChart()
|
||||
|
||||
lc.x = 50
|
||||
lc.y = 50
|
||||
lc.height = 125
|
||||
lc.width = 300
|
||||
lc.data = data
|
||||
lc.joinedLines = 1
|
||||
lc.lines.symbol = makeMarker('Smiley')
|
||||
lc.lineLabelFormat = '%2.0f'
|
||||
lc.strokeColor = colors.black
|
||||
lc.fillColor = colors.lightblue
|
||||
|
||||
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
|
||||
lc.categoryAxis.categoryNames = catNames
|
||||
lc.categoryAxis.labels.boxAnchor = 'n'
|
||||
|
||||
lc.valueAxis.valueMin = 0
|
||||
lc.valueAxis.valueMax = 60
|
||||
lc.valueAxis.valueStep = 15
|
||||
|
||||
drawing.add(lc)
|
||||
|
||||
return drawing
|
||||
|
||||
|
||||
def sample3():
|
||||
drawing = Drawing(400, 200)
|
||||
|
||||
data = [
|
||||
(13, 5, 20, 22, 37, 45, 19, 4),
|
||||
(5, 20, 46, 38, 23, 21, 6, 14)
|
||||
]
|
||||
|
||||
lc = HorizontalLineChart()
|
||||
|
||||
lc.x = 50
|
||||
lc.y = 50
|
||||
lc.height = 125
|
||||
lc.width = 300
|
||||
lc.data = data
|
||||
lc.joinedLines = 1
|
||||
lc.lineLabelFormat = '%2.0f'
|
||||
lc.strokeColor = colors.black
|
||||
|
||||
lc.lines[0].symbol = makeMarker('Smiley')
|
||||
lc.lines[1].symbol = NoEntry
|
||||
lc.lines[0].strokeWidth = 2
|
||||
lc.lines[1].strokeWidth = 4
|
||||
|
||||
catNames = 'Jan Feb Mar Apr May Jun Jul Aug'.split(' ')
|
||||
lc.categoryAxis.categoryNames = catNames
|
||||
lc.categoryAxis.labels.boxAnchor = 'n'
|
||||
|
||||
lc.valueAxis.valueMin = 0
|
||||
lc.valueAxis.valueMax = 60
|
||||
lc.valueAxis.valueStep = 15
|
||||
|
||||
drawing.add(lc)
|
||||
|
||||
return drawing
|
||||
1150
reportlab/graphics/charts/lineplots.py
Normal file
1150
reportlab/graphics/charts/lineplots.py
Normal file
File diff suppressed because it is too large
Load Diff
82
reportlab/graphics/charts/markers.py
Normal file
82
reportlab/graphics/charts/markers.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2012
|
||||
#see license.txt for license details
|
||||
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/markers.py
|
||||
|
||||
__version__=''' $Id$ '''
|
||||
__doc__="""This modules defines a collection of markers used in charts.
|
||||
|
||||
The make* functions return a simple shape or a widget as for
|
||||
the smiley.
|
||||
"""
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.graphics.shapes import Rect, Line, Circle, Polygon
|
||||
from reportlab.graphics.widgets.signsandsymbols import SmileyFace
|
||||
|
||||
|
||||
def makeEmptySquare(x, y, size, color):
|
||||
"Make an empty square marker."
|
||||
|
||||
d = size/2.0
|
||||
rect = Rect(x-d, y-d, 2*d, 2*d)
|
||||
rect.strokeColor = color
|
||||
rect.fillColor = None
|
||||
|
||||
return rect
|
||||
|
||||
|
||||
def makeFilledSquare(x, y, size, color):
|
||||
"Make a filled square marker."
|
||||
|
||||
d = size/2.0
|
||||
rect = Rect(x-d, y-d, 2*d, 2*d)
|
||||
rect.strokeColor = color
|
||||
rect.fillColor = color
|
||||
|
||||
return rect
|
||||
|
||||
|
||||
def makeFilledDiamond(x, y, size, color):
|
||||
"Make a filled diamond marker."
|
||||
|
||||
d = size/2.0
|
||||
poly = Polygon((x-d,y, x,y+d, x+d,y, x,y-d))
|
||||
poly.strokeColor = color
|
||||
poly.fillColor = color
|
||||
|
||||
return poly
|
||||
|
||||
|
||||
def makeEmptyCircle(x, y, size, color):
|
||||
"Make a hollow circle marker."
|
||||
|
||||
d = size/2.0
|
||||
circle = Circle(x, y, d)
|
||||
circle.strokeColor = color
|
||||
circle.fillColor = colors.white
|
||||
|
||||
return circle
|
||||
|
||||
|
||||
def makeFilledCircle(x, y, size, color):
|
||||
"Make a hollow circle marker."
|
||||
|
||||
d = size/2.0
|
||||
circle = Circle(x, y, d)
|
||||
circle.strokeColor = color
|
||||
circle.fillColor = color
|
||||
|
||||
return circle
|
||||
|
||||
|
||||
def makeSmiley(x, y, size, color):
|
||||
"Make a smiley marker."
|
||||
|
||||
d = size
|
||||
s = SmileyFace()
|
||||
s.fillColor = color
|
||||
s.x = x-d
|
||||
s.y = y-d
|
||||
s.size = d*2
|
||||
|
||||
return s
|
||||
1660
reportlab/graphics/charts/piecharts.py
Normal file
1660
reportlab/graphics/charts/piecharts.py
Normal file
File diff suppressed because it is too large
Load Diff
186
reportlab/graphics/charts/slidebox.py
Normal file
186
reportlab/graphics/charts/slidebox.py
Normal file
@@ -0,0 +1,186 @@
|
||||
from reportlab.lib.colors import Color, white, black
|
||||
from reportlab.graphics.charts.textlabels import Label
|
||||
from reportlab.graphics.shapes import Polygon, Line, Circle, String, Drawing, PolyLine, Group, Rect
|
||||
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.lib.validators import *
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth, getFont
|
||||
from reportlab.graphics.widgets.grids import ShadedRect, Grid
|
||||
|
||||
class SlideBox(Widget):
|
||||
"""Returns a slidebox widget"""
|
||||
_attrMap = AttrMap(
|
||||
labelFontName = AttrMapValue(isString, desc="Name of font used for the labels"),
|
||||
labelFontSize = AttrMapValue(isNumber, desc="Size of font used for the labels"),
|
||||
labelStrokeColor = AttrMapValue(isColorOrNone, desc="Colour for for number outlines"),
|
||||
labelFillColor = AttrMapValue(isColorOrNone, desc="Colour for number insides"),
|
||||
startColor = AttrMapValue(isColor, desc='Color of first box'),
|
||||
endColor = AttrMapValue(isColor, desc='Color of last box'),
|
||||
numberOfBoxes = AttrMapValue(isInt, desc='How many boxes there are'),
|
||||
trianglePosition = AttrMapValue(isInt, desc='Which box is highlighted by the triangles'),
|
||||
triangleHeight = AttrMapValue(isNumber, desc="Height of indicator triangles"),
|
||||
triangleWidth = AttrMapValue(isNumber, desc="Width of indicator triangles"),
|
||||
triangleFillColor = AttrMapValue(isColor, desc="Colour of indicator triangles"),
|
||||
triangleStrokeColor = AttrMapValue(isColorOrNone, desc="Colour of indicator triangle outline"),
|
||||
triangleStrokeWidth = AttrMapValue(isNumber, desc="Colour of indicator triangle outline"),
|
||||
boxHeight = AttrMapValue(isNumber, desc="Height of the boxes"),
|
||||
boxWidth = AttrMapValue(isNumber, desc="Width of the boxes"),
|
||||
boxSpacing = AttrMapValue(isNumber, desc="Space between the boxes"),
|
||||
boxOutlineColor = AttrMapValue(isColorOrNone, desc="Colour used to outline the boxes (if any)"),
|
||||
boxOutlineWidth = AttrMapValue(isNumberOrNone, desc="Width of the box outline (if any)"),
|
||||
leftPadding = AttrMapValue(isNumber, desc='Padding on left of drawing'),
|
||||
rightPadding = AttrMapValue(isNumber, desc='Padding on right of drawing'),
|
||||
topPadding = AttrMapValue(isNumber, desc='Padding at top of drawing'),
|
||||
bottomPadding = AttrMapValue(isNumber, desc='Padding at bottom of drawing'),
|
||||
background = AttrMapValue(isColorOrNone, desc='Colour of the background to the drawing (if any)'),
|
||||
sourceLabelText = AttrMapValue(isNoneOrString, desc="Text used for the 'source' label (can be empty)"),
|
||||
sourceLabelOffset = AttrMapValue(isNumber, desc='Padding at bottom of drawing'),
|
||||
sourceLabelFontName = AttrMapValue(isString, desc="Name of font used for the 'source' label"),
|
||||
sourceLabelFontSize = AttrMapValue(isNumber, desc="Font size for the 'source' label"),
|
||||
sourceLabelFillColor = AttrMapValue(isColorOrNone, desc="Colour ink for the 'source' label (bottom right)"),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.labelFontName = "Helvetica-Bold"
|
||||
self.labelFontSize = 10
|
||||
self.labelStrokeColor = black
|
||||
self.labelFillColor = white
|
||||
self.startColor = colors.Color(232/255.0,224/255.0,119/255.0)
|
||||
self.endColor = colors.Color(25/255.0,77/255.0,135/255.0)
|
||||
self.numberOfBoxes = 7
|
||||
self.trianglePosition = 7
|
||||
self.triangleHeight = 0.12*cm
|
||||
self.triangleWidth = 0.38*cm
|
||||
self.triangleFillColor = white
|
||||
self.triangleStrokeColor = black
|
||||
self.triangleStrokeWidth = 0.58
|
||||
self.boxHeight = 0.55*cm
|
||||
self.boxWidth = 0.73*cm
|
||||
self.boxSpacing = 0.075*cm
|
||||
self.boxOutlineColor = black
|
||||
self.boxOutlineWidth = 0.58
|
||||
self.leftPadding=5
|
||||
self.rightPadding=5
|
||||
self.topPadding=5
|
||||
self.bottomPadding=5
|
||||
self.background=None
|
||||
self.sourceLabelText = "Source: ReportLab"
|
||||
self.sourceLabelOffset = 0.2*cm
|
||||
self.sourceLabelFontName = "Helvetica-Oblique"
|
||||
self.sourceLabelFontSize = 6
|
||||
self.sourceLabelFillColor = black
|
||||
|
||||
def _getDrawingDimensions(self):
|
||||
tx=(self.numberOfBoxes*self.boxWidth)
|
||||
if self.numberOfBoxes>1: tx=tx+((self.numberOfBoxes-1)*self.boxSpacing)
|
||||
tx=tx+self.leftPadding+self.rightPadding
|
||||
ty=self.boxHeight+self.triangleHeight
|
||||
ty=ty+self.topPadding+self.bottomPadding+self.sourceLabelOffset+self.sourceLabelFontSize
|
||||
return (tx,ty)
|
||||
|
||||
def _getColors(self):
|
||||
# for calculating intermediate colors...
|
||||
numShades = self.numberOfBoxes+1
|
||||
fillColorStart = self.startColor
|
||||
fillColorEnd = self.endColor
|
||||
colorsList =[]
|
||||
|
||||
for i in range(0,numShades):
|
||||
colorsList.append(colors.linearlyInterpolatedColor(fillColorStart, fillColorEnd, 0, numShades-1, i))
|
||||
return colorsList
|
||||
|
||||
def demo(self,drawing=None):
|
||||
from reportlab.lib import colors
|
||||
if not drawing:
|
||||
tx,ty=self._getDrawingDimensions()
|
||||
drawing = Drawing(tx,ty)
|
||||
drawing.add(self.draw())
|
||||
return drawing
|
||||
|
||||
def draw(self):
|
||||
g = Group()
|
||||
ys = self.bottomPadding+(self.triangleHeight/2)+self.sourceLabelOffset+self.sourceLabelFontSize
|
||||
if self.background:
|
||||
x,y = self._getDrawingDimensions()
|
||||
g.add(Rect(-self.leftPadding,-ys,x,y,
|
||||
strokeColor=None,
|
||||
strokeWidth=0,
|
||||
fillColor=self.background))
|
||||
|
||||
ascent=getFont(self.labelFontName).face.ascent/1000.
|
||||
if ascent==0: ascent=0.718 # default (from helvetica)
|
||||
ascent=ascent*self.labelFontSize # normalize
|
||||
|
||||
colorsList = self._getColors()
|
||||
|
||||
# Draw the boxes - now uses ShadedRect from grids
|
||||
x=0
|
||||
for f in range (0,self.numberOfBoxes):
|
||||
sr=ShadedRect()
|
||||
sr.x=x
|
||||
sr.y=0
|
||||
sr.width=self.boxWidth
|
||||
sr.height=self.boxHeight
|
||||
sr.orientation = 'vertical'
|
||||
sr.numShades = 30
|
||||
sr.fillColorStart = colorsList[f]
|
||||
sr.fillColorEnd = colorsList[f+1]
|
||||
sr.strokeColor = None
|
||||
sr.strokeWidth = 0
|
||||
|
||||
g.add(sr)
|
||||
|
||||
g.add(Rect(x,0,self.boxWidth,self.boxHeight,
|
||||
strokeColor=self.boxOutlineColor,
|
||||
strokeWidth=self.boxOutlineWidth,
|
||||
fillColor=None))
|
||||
|
||||
g.add(String(x+self.boxWidth/2.,(self.boxHeight-ascent)/2.,
|
||||
text = str(f+1),
|
||||
fillColor = self.labelFillColor,
|
||||
strokeColor=self.labelStrokeColor,
|
||||
textAnchor = 'middle',
|
||||
fontName = self.labelFontName,
|
||||
fontSize = self.labelFontSize))
|
||||
x=x+self.boxWidth+self.boxSpacing
|
||||
|
||||
#do triangles
|
||||
xt = (self.trianglePosition*self.boxWidth)
|
||||
if self.trianglePosition>1:
|
||||
xt = xt+(self.trianglePosition-1)*self.boxSpacing
|
||||
xt = xt-(self.boxWidth/2)
|
||||
g.add(Polygon(
|
||||
strokeColor = self.triangleStrokeColor,
|
||||
strokeWidth = self.triangleStrokeWidth,
|
||||
fillColor = self.triangleFillColor,
|
||||
points=[xt,self.boxHeight-(self.triangleHeight/2),
|
||||
xt-(self.triangleWidth/2),self.boxHeight+(self.triangleHeight/2),
|
||||
xt+(self.triangleWidth/2),self.boxHeight+(self.triangleHeight/2),
|
||||
xt,self.boxHeight-(self.triangleHeight/2)]))
|
||||
g.add(Polygon(
|
||||
strokeColor = self.triangleStrokeColor,
|
||||
strokeWidth = self.triangleStrokeWidth,
|
||||
fillColor = self.triangleFillColor,
|
||||
points=[xt,0+(self.triangleHeight/2),
|
||||
xt-(self.triangleWidth/2),0-(self.triangleHeight/2),
|
||||
xt+(self.triangleWidth/2),0-(self.triangleHeight/2),
|
||||
xt,0+(self.triangleHeight/2)]))
|
||||
|
||||
#source label
|
||||
if self.sourceLabelText != None:
|
||||
g.add(String(x-self.boxSpacing,0-(self.triangleHeight/2)-self.sourceLabelOffset-(self.sourceLabelFontSize),
|
||||
text = self.sourceLabelText,
|
||||
fillColor = self.sourceLabelFillColor,
|
||||
textAnchor = 'end',
|
||||
fontName = self.sourceLabelFontName,
|
||||
fontSize = self.sourceLabelFontSize))
|
||||
|
||||
g.shift(self.leftPadding, ys)
|
||||
|
||||
return g
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
d = SlideBox()
|
||||
d.demo().save(fnRoot="slidebox")
|
||||
408
reportlab/graphics/charts/spider.py
Normal file
408
reportlab/graphics/charts/spider.py
Normal file
@@ -0,0 +1,408 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2004
|
||||
#see license.txt for license details
|
||||
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/spider.py
|
||||
# spider chart, also known as radar chart
|
||||
|
||||
__version__=''' $Id$ '''
|
||||
__doc__="""Spider Chart
|
||||
|
||||
Normal use shows variation of 5-10 parameters against some 'norm' or target.
|
||||
When there is more than one series, place the series with the largest
|
||||
numbers first, as it will be overdrawn by each successive one.
|
||||
"""
|
||||
|
||||
import copy
|
||||
from math import sin, cos, pi
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\
|
||||
isListOfNumbers, isColorOrNone, isString,\
|
||||
isListOfStringsOrNone, OneOf, SequenceOf,\
|
||||
isBoolean, isListOfColors, isNumberOrNone,\
|
||||
isNoneOrListOfNoneOrStrings, isTextAnchor,\
|
||||
isNoneOrListOfNoneOrNumbers, isBoxAnchor,\
|
||||
isStringOrNone, isStringOrNone, EitherOr,\
|
||||
isCallable
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, PolyLine, Ellipse, \
|
||||
Wedge, String, STATE_DEFAULTS
|
||||
from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
|
||||
from reportlab.graphics.charts.areas import PlotArea
|
||||
from reportlab.graphics.charts.legends import _objStr
|
||||
from reportlab.graphics.charts.piecharts import WedgeLabel
|
||||
from reportlab.graphics.widgets.markers import makeMarker, uSymbol2Symbol, isSymbol
|
||||
|
||||
class StrandProperty(PropHolder):
|
||||
|
||||
_attrMap = AttrMap(
|
||||
strokeWidth = AttrMapValue(isNumber,desc='width'),
|
||||
fillColor = AttrMapValue(isColorOrNone,desc='filling color'),
|
||||
strokeColor = AttrMapValue(isColorOrNone,desc='stroke color'),
|
||||
strokeDashArray = AttrMapValue(isListOfNumbersOrNone,desc='dashing pattern, e.g. (3,2)'),
|
||||
symbol = AttrMapValue(EitherOr((isStringOrNone,isSymbol)), desc='Widget placed at data points.',advancedUsage=1),
|
||||
symbolSize= AttrMapValue(isNumber, desc='Symbol size.',advancedUsage=1),
|
||||
name = AttrMapValue(isStringOrNone, desc='Name of the strand.'),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.strokeWidth = 1
|
||||
self.fillColor = None
|
||||
self.strokeColor = STATE_DEFAULTS["strokeColor"]
|
||||
self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"]
|
||||
self.symbol = None
|
||||
self.symbolSize = 5
|
||||
self.name = None
|
||||
|
||||
class SpokeProperty(PropHolder):
|
||||
_attrMap = AttrMap(
|
||||
strokeWidth = AttrMapValue(isNumber,desc='width'),
|
||||
fillColor = AttrMapValue(isColorOrNone,desc='filling color'),
|
||||
strokeColor = AttrMapValue(isColorOrNone,desc='stroke color'),
|
||||
strokeDashArray = AttrMapValue(isListOfNumbersOrNone,desc='dashing pattern, e.g. (2,1)'),
|
||||
labelRadius = AttrMapValue(isNumber,desc='label radius',advancedUsage=1),
|
||||
visible = AttrMapValue(isBoolean,desc="True if the spoke line is to be drawn"),
|
||||
)
|
||||
|
||||
def __init__(self,**kw):
|
||||
self.strokeWidth = 0.5
|
||||
self.fillColor = None
|
||||
self.strokeColor = STATE_DEFAULTS["strokeColor"]
|
||||
self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"]
|
||||
self.visible = 1
|
||||
self.labelRadius = 1.05
|
||||
|
||||
class SpokeLabel(WedgeLabel):
|
||||
def __init__(self,**kw):
|
||||
WedgeLabel.__init__(self,**kw)
|
||||
if '_text' not in list(kw.keys()): self._text = ''
|
||||
|
||||
class StrandLabel(SpokeLabel):
|
||||
_attrMap = AttrMap(BASE=SpokeLabel,
|
||||
format = AttrMapValue(EitherOr((isStringOrNone,isCallable)),desc="Format for the label"),
|
||||
dR = AttrMapValue(isNumberOrNone,desc="radial shift for label"),
|
||||
)
|
||||
def __init__(self,**kw):
|
||||
self.format = ''
|
||||
self.dR = 0
|
||||
SpokeLabel.__init__(self,**kw)
|
||||
|
||||
def _setupLabel(labelClass, text, radius, cx, cy, angle, car, sar, sty):
|
||||
L = labelClass()
|
||||
L._text = text
|
||||
L.x = cx + radius*car
|
||||
L.y = cy + radius*sar
|
||||
L._pmv = angle*180/pi
|
||||
L.boxAnchor = sty.boxAnchor
|
||||
L.dx = sty.dx
|
||||
L.dy = sty.dy
|
||||
L.angle = sty.angle
|
||||
L.boxAnchor = sty.boxAnchor
|
||||
L.boxStrokeColor = sty.boxStrokeColor
|
||||
L.boxStrokeWidth = sty.boxStrokeWidth
|
||||
L.boxFillColor = sty.boxFillColor
|
||||
L.strokeColor = sty.strokeColor
|
||||
L.strokeWidth = sty.strokeWidth
|
||||
L.leading = sty.leading
|
||||
L.width = sty.width
|
||||
L.maxWidth = sty.maxWidth
|
||||
L.height = sty.height
|
||||
L.textAnchor = sty.textAnchor
|
||||
L.visible = sty.visible
|
||||
L.topPadding = sty.topPadding
|
||||
L.leftPadding = sty.leftPadding
|
||||
L.rightPadding = sty.rightPadding
|
||||
L.bottomPadding = sty.bottomPadding
|
||||
L.fontName = sty.fontName
|
||||
L.fontSize = sty.fontSize
|
||||
L.fillColor = sty.fillColor
|
||||
return L
|
||||
|
||||
class SpiderChart(PlotArea):
|
||||
_attrMap = AttrMap(BASE=PlotArea,
|
||||
data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'),
|
||||
labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"),
|
||||
startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"),
|
||||
direction = AttrMapValue( OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"),
|
||||
strands = AttrMapValue(None, desc="collection of strand descriptor objects"),
|
||||
spokes = AttrMapValue(None, desc="collection of spoke descriptor objects"),
|
||||
strandLabels = AttrMapValue(None, desc="collection of strand label descriptor objects"),
|
||||
spokeLabels = AttrMapValue(None, desc="collection of spoke label descriptor objects"),
|
||||
)
|
||||
|
||||
def makeSwatchSample(self, rowNo, x, y, width, height):
|
||||
baseStyle = self.strands
|
||||
styleIdx = rowNo % len(baseStyle)
|
||||
style = baseStyle[styleIdx]
|
||||
strokeColor = getattr(style, 'strokeColor', getattr(baseStyle,'strokeColor',None))
|
||||
fillColor = getattr(style, 'fillColor', getattr(baseStyle,'fillColor',None))
|
||||
strokeDashArray = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None))
|
||||
strokeWidth = getattr(style, 'strokeWidth', getattr(baseStyle, 'strokeWidth',0))
|
||||
symbol = getattr(style, 'symbol', getattr(baseStyle, 'symbol',None))
|
||||
ym = y+height/2.0
|
||||
if fillColor is None and strokeColor is not None and strokeWidth>0:
|
||||
bg = Line(x,ym,x+width,ym,strokeWidth=strokeWidth,strokeColor=strokeColor,
|
||||
strokeDashArray=strokeDashArray)
|
||||
elif fillColor is not None:
|
||||
bg = Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor,
|
||||
strokeDashArray=strokeDashArray,fillColor=fillColor)
|
||||
else:
|
||||
bg = None
|
||||
if symbol:
|
||||
symbol = uSymbol2Symbol(symbol,x+width/2.,ym,color)
|
||||
if bg:
|
||||
g = Group()
|
||||
g.add(bg)
|
||||
g.add(symbol)
|
||||
return g
|
||||
return symbol or bg
|
||||
|
||||
def getSeriesName(self,i,default=None):
|
||||
'''return series name i or default'''
|
||||
return _objStr(getattr(self.strands[i],'name',default))
|
||||
|
||||
def __init__(self):
|
||||
PlotArea.__init__(self)
|
||||
|
||||
self.data = [[10,12,14,16,14,12], [6,8,10,12,9,11]]
|
||||
self.labels = None # or list of strings
|
||||
self.labels = ['a','b','c','d','e','f']
|
||||
self.startAngle = 90
|
||||
self.direction = "clockwise"
|
||||
|
||||
self.strands = TypedPropertyCollection(StrandProperty)
|
||||
self.spokes = TypedPropertyCollection(SpokeProperty)
|
||||
self.spokeLabels = TypedPropertyCollection(SpokeLabel)
|
||||
self.spokeLabels._text = None
|
||||
self.strandLabels = TypedPropertyCollection(StrandLabel)
|
||||
self.x = 10
|
||||
self.y = 10
|
||||
self.width = 180
|
||||
self.height = 180
|
||||
|
||||
def demo(self):
|
||||
d = Drawing(200, 200)
|
||||
d.add(SpiderChart())
|
||||
return d
|
||||
|
||||
def normalizeData(self, outer = 0.0):
|
||||
"""Turns data into normalized ones where each datum is < 1.0,
|
||||
and 1.0 = maximum radius. Adds 10% at outside edge by default"""
|
||||
data = self.data
|
||||
assert min(list(map(min,data))) >=0, "Cannot do spider plots of negative numbers!"
|
||||
norm = max(list(map(max,data)))
|
||||
norm *= (1.0+outer)
|
||||
if norm<1e-9: norm = 1.0
|
||||
self._norm = norm
|
||||
return [[e/norm for e in row] for row in data]
|
||||
|
||||
def _innerDrawLabel(self, sty, radius, cx, cy, angle, car, sar, labelClass=StrandLabel):
|
||||
"Draw a label for a given item in the list."
|
||||
fmt = sty.format
|
||||
value = radius*self._norm
|
||||
if not fmt:
|
||||
text = None
|
||||
elif isinstance(fmt,str):
|
||||
if fmt == 'values':
|
||||
text = sty._text
|
||||
else:
|
||||
text = fmt % value
|
||||
elif hasattr(fmt,'__call__'):
|
||||
text = fmt(value)
|
||||
else:
|
||||
raise ValueError("Unknown formatter type %s, expected string or function" % fmt)
|
||||
|
||||
if text:
|
||||
dR = sty.dR
|
||||
if dR:
|
||||
radius += dR/self._radius
|
||||
L = _setupLabel(labelClass, text, radius, cx, cy, angle, car, sar, sty)
|
||||
if dR<0: L._anti = 1
|
||||
else:
|
||||
L = None
|
||||
return L
|
||||
|
||||
def draw(self):
|
||||
# normalize slice data
|
||||
g = self.makeBackground() or Group()
|
||||
|
||||
xradius = self.width/2.0
|
||||
yradius = self.height/2.0
|
||||
self._radius = radius = min(xradius, yradius)
|
||||
cx = self.x + xradius
|
||||
cy = self.y + yradius
|
||||
|
||||
data = self.normalizeData()
|
||||
|
||||
self._seriesCount = len(data)
|
||||
n = len(data[0])
|
||||
|
||||
#labels
|
||||
if self.labels is None:
|
||||
labels = [''] * n
|
||||
else:
|
||||
labels = self.labels
|
||||
#there's no point in raising errors for less than enough errors if
|
||||
#we silently create all for the extreme case of no labels.
|
||||
i = n-len(labels)
|
||||
if i>0:
|
||||
labels = labels + ['']*i
|
||||
|
||||
S = []
|
||||
STRANDS = []
|
||||
STRANDAREAS = []
|
||||
syms = []
|
||||
labs = []
|
||||
csa = []
|
||||
angle = self.startAngle*pi/180
|
||||
direction = self.direction == "clockwise" and -1 or 1
|
||||
angleBetween = direction*(2 * pi)/float(n)
|
||||
spokes = self.spokes
|
||||
spokeLabels = self.spokeLabels
|
||||
for i in range(n):
|
||||
car = cos(angle)*radius
|
||||
sar = sin(angle)*radius
|
||||
csa.append((car,sar,angle))
|
||||
si = self.spokes[i]
|
||||
if si.visible:
|
||||
spoke = Line(cx, cy, cx + car, cy + sar, strokeWidth = si.strokeWidth, strokeColor=si.strokeColor, strokeDashArray=si.strokeDashArray)
|
||||
S.append(spoke)
|
||||
sli = spokeLabels[i]
|
||||
text = sli._text
|
||||
if not text: text = labels[i]
|
||||
if text:
|
||||
S.append(_setupLabel(WedgeLabel, text, si.labelRadius, cx, cy, angle, car, sar, sli))
|
||||
angle += angleBetween
|
||||
|
||||
# now plot the polygons
|
||||
rowIdx = 0
|
||||
strands = self.strands
|
||||
strandLabels = self.strandLabels
|
||||
for row in data:
|
||||
# series plot
|
||||
rsty = strands[rowIdx]
|
||||
points = []
|
||||
car, sar = csa[-1][:2]
|
||||
r = row[-1]
|
||||
points.append(cx+car*r)
|
||||
points.append(cy+sar*r)
|
||||
for i in range(n):
|
||||
car, sar, angle = csa[i]
|
||||
r = row[i]
|
||||
points.append(cx+car*r)
|
||||
points.append(cy+sar*r)
|
||||
L = self._innerDrawLabel(strandLabels[(rowIdx,i)], r, cx, cy, angle, car, sar, labelClass=StrandLabel)
|
||||
if L: labs.append(L)
|
||||
sty = strands[(rowIdx,i)]
|
||||
uSymbol = sty.symbol
|
||||
|
||||
# put in a marker, if it needs one
|
||||
if uSymbol:
|
||||
s_x = cx+car*r
|
||||
s_y = cy+sar*r
|
||||
s_fillColor = sty.fillColor
|
||||
s_strokeColor = sty.strokeColor
|
||||
s_strokeWidth = sty.strokeWidth
|
||||
s_angle = 0
|
||||
s_size = sty.symbolSize
|
||||
if type(uSymbol) is type(''):
|
||||
symbol = makeMarker(uSymbol,
|
||||
size = s_size,
|
||||
x = s_x,
|
||||
y = s_y,
|
||||
fillColor = s_fillColor,
|
||||
strokeColor = s_strokeColor,
|
||||
strokeWidth = s_strokeWidth,
|
||||
angle = s_angle,
|
||||
)
|
||||
else:
|
||||
symbol = uSymbol2Symbol(uSymbol,s_x,s_y,s_fillColor)
|
||||
for k,v in (('size', s_size), ('fillColor', s_fillColor),
|
||||
('x', s_x), ('y', s_y),
|
||||
('strokeColor',s_strokeColor), ('strokeWidth',s_strokeWidth),
|
||||
('angle',s_angle),):
|
||||
if getattr(symbol,k,None) is None:
|
||||
try:
|
||||
setattr(symbol,k,v)
|
||||
except:
|
||||
pass
|
||||
syms.append(symbol)
|
||||
|
||||
# make up the 'strand'
|
||||
if rsty.fillColor:
|
||||
strand = Polygon(points)
|
||||
strand.fillColor = rsty.fillColor
|
||||
strand.strokeColor = None
|
||||
strand.strokeWidth = 0
|
||||
STRANDAREAS.append(strand)
|
||||
if rsty.strokeColor and rsty.strokeWidth:
|
||||
strand = PolyLine(points)
|
||||
strand.strokeColor = rsty.strokeColor
|
||||
strand.strokeWidth = rsty.strokeWidth
|
||||
strand.strokeDashArray = rsty.strokeDashArray
|
||||
STRANDS.append(strand)
|
||||
rowIdx += 1
|
||||
|
||||
for s in (STRANDAREAS+STRANDS+syms+S+labs): g.add(s)
|
||||
return g
|
||||
|
||||
def sample1():
|
||||
"Make a simple spider chart"
|
||||
d = Drawing(400, 400)
|
||||
sp = SpiderChart()
|
||||
sp.x = 50
|
||||
sp.y = 50
|
||||
sp.width = 300
|
||||
sp.height = 300
|
||||
sp.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8]]
|
||||
sp.labels = ['a','b','c','d','e','f']
|
||||
sp.strands[0].strokeColor = colors.cornsilk
|
||||
sp.strands[1].strokeColor = colors.cyan
|
||||
sp.strands[2].strokeColor = colors.palegreen
|
||||
sp.strands[0].fillColor = colors.cornsilk
|
||||
sp.strands[1].fillColor = colors.cyan
|
||||
sp.strands[2].fillColor = colors.palegreen
|
||||
sp.spokes.strokeDashArray = (2,2)
|
||||
d.add(sp)
|
||||
return d
|
||||
|
||||
|
||||
def sample2():
|
||||
"Make a spider chart with markers, but no fill"
|
||||
d = Drawing(400, 400)
|
||||
sp = SpiderChart()
|
||||
sp.x = 50
|
||||
sp.y = 50
|
||||
sp.width = 300
|
||||
sp.height = 300
|
||||
sp.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8]]
|
||||
sp.labels = ['U','V','W','X','Y','Z']
|
||||
sp.strands.strokeWidth = 1
|
||||
sp.strands[0].fillColor = colors.pink
|
||||
sp.strands[1].fillColor = colors.lightblue
|
||||
sp.strands[2].fillColor = colors.palegreen
|
||||
sp.strands[0].strokeColor = colors.red
|
||||
sp.strands[1].strokeColor = colors.blue
|
||||
sp.strands[2].strokeColor = colors.green
|
||||
sp.strands.symbol = "FilledDiamond"
|
||||
sp.strands[1].symbol = makeMarker("Circle")
|
||||
sp.strands[1].symbol.strokeWidth = 0.5
|
||||
sp.strands[1].symbol.fillColor = colors.yellow
|
||||
sp.strands.symbolSize = 6
|
||||
sp.strandLabels[0,3]._text = 'special'
|
||||
sp.strandLabels[0,1]._text = 'one'
|
||||
sp.strandLabels[0,0]._text = 'zero'
|
||||
sp.strandLabels[1,0]._text = 'Earth'
|
||||
sp.strandLabels[2,2]._text = 'Mars'
|
||||
sp.strandLabels.format = 'values'
|
||||
sp.strandLabels.dR = -5
|
||||
d.add(sp)
|
||||
return d
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
d = sample1()
|
||||
from reportlab.graphics.renderPDF import drawToFile
|
||||
drawToFile(d, 'spider.pdf')
|
||||
d = sample2()
|
||||
drawToFile(d, 'spider2.pdf')
|
||||
466
reportlab/graphics/charts/textlabels.py
Normal file
466
reportlab/graphics/charts/textlabels.py
Normal file
@@ -0,0 +1,466 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2012
|
||||
#see license.txt for license details
|
||||
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/textlabels.py
|
||||
__version__=''' $Id$ '''
|
||||
import string
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.utils import simpleSplit, _simpleSplit
|
||||
from reportlab.lib.validators import isNumber, isNumberOrNone, OneOf, isColorOrNone, isString, \
|
||||
isTextAnchor, isBoxAnchor, isBoolean, NoneOr, isInstanceOf, isNoneOrString, isNoneOrCallable
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth, getAscentDescent
|
||||
from reportlab.graphics.shapes import Drawing, Group, Circle, Rect, String, STATE_DEFAULTS
|
||||
from reportlab.graphics.shapes import _PATH_OP_ARG_COUNT, _PATH_OP_NAMES, definePath
|
||||
from reportlab.graphics.widgetbase import Widget, PropHolder
|
||||
from reportlab.graphics.shapes import _baseGFontName
|
||||
|
||||
_gs = None
|
||||
_A2BA= {
|
||||
'x': {0:'n', 45:'ne', 90:'e', 135:'se', 180:'s', 225:'sw', 270:'w', 315: 'nw', -45: 'nw'},
|
||||
'y': {0:'e', 45:'se', 90:'s', 135:'sw', 180:'w', 225:'nw', 270:'n', 315: 'ne', -45: 'ne'},
|
||||
}
|
||||
|
||||
def _pathNumTrunc(n):
|
||||
if int(n)==n: return int(n)
|
||||
return round(n,5)
|
||||
|
||||
def _processGlyph(G, truncate=1, pathReverse=0):
|
||||
O = []
|
||||
P = []
|
||||
R = []
|
||||
if G and len(G)==1 and G[0][0]=='lineTo':
|
||||
G = (('moveToClosed',)+G[0][1:],)+G #hack fix for some errors
|
||||
for g in G+(('end',),):
|
||||
op = g[0]
|
||||
if O and op in ['moveTo', 'moveToClosed','end']:
|
||||
if O[0]=='moveToClosed':
|
||||
O = O[1:]
|
||||
if pathReverse:
|
||||
for i in range(0,len(P),2):
|
||||
P[i+1], P[i] = P[i:i+2]
|
||||
P.reverse()
|
||||
O.reverse()
|
||||
O.insert(0,'moveTo')
|
||||
O.append('closePath')
|
||||
i = 0
|
||||
if truncate: P = list(map(_pathNumTrunc,P))
|
||||
for o in O:
|
||||
j = i + _PATH_OP_ARG_COUNT[_PATH_OP_NAMES.index(o)]
|
||||
if o=='closePath':
|
||||
R.append(o)
|
||||
else:
|
||||
R.append((o,)+ tuple(P[i:j]))
|
||||
i = j
|
||||
O = []
|
||||
P = []
|
||||
O.append(op)
|
||||
P.extend(g[1:])
|
||||
return R
|
||||
|
||||
def _text2PathDescription(text, x=0, y=0, fontName=_baseGFontName, fontSize=1000,
|
||||
anchor='start', truncate=1, pathReverse=0):
|
||||
from reportlab.graphics import renderPM, _renderPM
|
||||
_gs = _renderPM.gstate(1,1)
|
||||
renderPM._setFont(_gs,fontName,fontSize)
|
||||
P = []
|
||||
if not anchor=='start':
|
||||
textLen = stringWidth(text, fontName,fontSize)
|
||||
if anchor=='end':
|
||||
x = x-textLen
|
||||
elif anchor=='middle':
|
||||
x = x - textLen/2.
|
||||
for g in _gs._stringPath(text,x,y):
|
||||
P.extend(_processGlyph(g,truncate=truncate,pathReverse=pathReverse))
|
||||
return P
|
||||
|
||||
def _text2Path(text, x=0, y=0, fontName=_baseGFontName, fontSize=1000,
|
||||
anchor='start', truncate=1, pathReverse=0,**kwds):
|
||||
return definePath(_text2PathDescription(text,x=x,y=y,fontName=fontName,
|
||||
fontSize=fontSize,anchor=anchor,truncate=truncate,pathReverse=pathReverse),**kwds)
|
||||
|
||||
_BA2TA={'w':'start','nw':'start','sw':'start','e':'end', 'ne': 'end', 'se':'end', 'n':'middle','s':'middle','c':'middle'}
|
||||
class Label(Widget):
|
||||
"""A text label to attach to something else, such as a chart axis.
|
||||
|
||||
This allows you to specify an offset, angle and many anchor
|
||||
properties relative to the label's origin. It allows, for example,
|
||||
angled multiline axis labels.
|
||||
"""
|
||||
# fairly straight port of Robin Becker's textbox.py to new widgets
|
||||
# framework.
|
||||
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumber,desc=''),
|
||||
y = AttrMapValue(isNumber,desc=''),
|
||||
dx = AttrMapValue(isNumber,desc='delta x - offset'),
|
||||
dy = AttrMapValue(isNumber,desc='delta y - offset'),
|
||||
angle = AttrMapValue(isNumber,desc='angle of label: default (0), 90 is vertical, 180 is upside down, etc'),
|
||||
boxAnchor = AttrMapValue(isBoxAnchor,desc='anchoring point of the label'),
|
||||
boxStrokeColor = AttrMapValue(isColorOrNone,desc='border color of the box'),
|
||||
boxStrokeWidth = AttrMapValue(isNumber,desc='border width'),
|
||||
boxFillColor = AttrMapValue(isColorOrNone,desc='the filling color of the box'),
|
||||
boxTarget = AttrMapValue(OneOf('normal','anti','lo','hi'),desc="one of ('normal','anti','lo','hi')"),
|
||||
fillColor = AttrMapValue(isColorOrNone,desc='label text color'),
|
||||
strokeColor = AttrMapValue(isColorOrNone,desc='label text border color'),
|
||||
strokeWidth = AttrMapValue(isNumber,desc='label text border width'),
|
||||
text = AttrMapValue(isString,desc='the actual text to display'),
|
||||
fontName = AttrMapValue(isString,desc='the name of the font used'),
|
||||
fontSize = AttrMapValue(isNumber,desc='the size of the font'),
|
||||
leading = AttrMapValue(isNumberOrNone,desc=''),
|
||||
width = AttrMapValue(isNumberOrNone,desc='the width of the label'),
|
||||
maxWidth = AttrMapValue(isNumberOrNone,desc='maximum width the label can grow to'),
|
||||
height = AttrMapValue(isNumberOrNone,desc='the height of the text'),
|
||||
textAnchor = AttrMapValue(isTextAnchor,desc='the anchoring point of the text inside the label'),
|
||||
visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
|
||||
topPadding = AttrMapValue(isNumber,desc='padding at top of box'),
|
||||
leftPadding = AttrMapValue(isNumber,desc='padding at left of box'),
|
||||
rightPadding = AttrMapValue(isNumber,desc='padding at right of box'),
|
||||
bottomPadding = AttrMapValue(isNumber,desc='padding at bottom of box'),
|
||||
useAscentDescent = AttrMapValue(isBoolean,desc="If True then the font's Ascent & Descent will be used to compute default heights and baseline."),
|
||||
customDrawChanger = AttrMapValue(isNoneOrCallable,desc="An instance of CustomDrawChanger to modify the behavior at draw time", _advancedUsage=1),
|
||||
)
|
||||
|
||||
def __init__(self,**kw):
|
||||
self._setKeywords(**kw)
|
||||
self._setKeywords(
|
||||
_text = 'Multi-Line\nString',
|
||||
boxAnchor = 'c',
|
||||
angle = 0,
|
||||
x = 0,
|
||||
y = 0,
|
||||
dx = 0,
|
||||
dy = 0,
|
||||
topPadding = 0,
|
||||
leftPadding = 0,
|
||||
rightPadding = 0,
|
||||
bottomPadding = 0,
|
||||
boxStrokeWidth = 0.5,
|
||||
boxStrokeColor = None,
|
||||
boxTarget = 'normal',
|
||||
strokeColor = None,
|
||||
boxFillColor = None,
|
||||
leading = None,
|
||||
width = None,
|
||||
maxWidth = None,
|
||||
height = None,
|
||||
fillColor = STATE_DEFAULTS['fillColor'],
|
||||
fontName = STATE_DEFAULTS['fontName'],
|
||||
fontSize = STATE_DEFAULTS['fontSize'],
|
||||
strokeWidth = 0.1,
|
||||
textAnchor = 'start',
|
||||
visible = 1,
|
||||
useAscentDescent = False,
|
||||
)
|
||||
|
||||
def setText(self, text):
|
||||
"""Set the text property. May contain embedded newline characters.
|
||||
Called by the containing chart or axis."""
|
||||
self._text = text
|
||||
|
||||
|
||||
def setOrigin(self, x, y):
|
||||
"""Set the origin. This would be the tick mark or bar top relative to
|
||||
which it is defined. Called by the containing chart or axis."""
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
|
||||
def demo(self):
|
||||
"""This shows a label positioned with its top right corner
|
||||
at the top centre of the drawing, and rotated 45 degrees."""
|
||||
|
||||
d = Drawing(200, 100)
|
||||
|
||||
# mark the origin of the label
|
||||
d.add(Circle(100,90, 5, fillColor=colors.green))
|
||||
|
||||
lab = Label()
|
||||
lab.setOrigin(100,90)
|
||||
lab.boxAnchor = 'ne'
|
||||
lab.angle = 45
|
||||
lab.dx = 0
|
||||
lab.dy = -20
|
||||
lab.boxStrokeColor = colors.green
|
||||
lab.setText('Another\nMulti-Line\nString')
|
||||
d.add(lab)
|
||||
|
||||
return d
|
||||
|
||||
def _getBoxAnchor(self):
|
||||
'''hook for allowing special box anchor effects'''
|
||||
ba = self.boxAnchor
|
||||
if ba in ('autox', 'autoy'):
|
||||
angle = self.angle
|
||||
na = (int((angle%360)/45.)*45)%360
|
||||
if not (na % 90): # we have a right angle case
|
||||
da = (angle - na) % 360
|
||||
if abs(da)>5:
|
||||
na = na + (da>0 and 45 or -45)
|
||||
ba = _A2BA[ba[-1]][na]
|
||||
return ba
|
||||
|
||||
def computeSize(self):
|
||||
# the thing will draw in its own coordinate system
|
||||
self._lineWidths = []
|
||||
topPadding = self.topPadding
|
||||
leftPadding = self.leftPadding
|
||||
rightPadding = self.rightPadding
|
||||
bottomPadding = self.bottomPadding
|
||||
self._lines = simpleSplit(self._text,self.fontName,self.fontSize,self.maxWidth)
|
||||
if not self.width:
|
||||
self._width = leftPadding+rightPadding
|
||||
if self._lines:
|
||||
self._lineWidths = [stringWidth(line,self.fontName,self.fontSize) for line in self._lines]
|
||||
self._width += max(self._lineWidths)
|
||||
else:
|
||||
self._width = self.width
|
||||
if self.useAscentDescent:
|
||||
self._ascent, self._descent = getAscentDescent(self.fontName,self.fontSize)
|
||||
self._baselineRatio = self._ascent/(self._ascent-self._descent)
|
||||
else:
|
||||
self._baselineRatio = 1/1.2
|
||||
if self.leading:
|
||||
self._leading = self.leading
|
||||
elif self.useAscentDescent:
|
||||
self._leading = self._ascent - self._descent
|
||||
else:
|
||||
self._leading = self.fontSize*1.2
|
||||
self._height = self.height or (self._leading*len(self._lines) + topPadding + bottomPadding)
|
||||
self._ewidth = (self._width-leftPadding-rightPadding)
|
||||
self._eheight = (self._height-topPadding-bottomPadding)
|
||||
boxAnchor = self._getBoxAnchor()
|
||||
if boxAnchor in ['n','ne','nw']:
|
||||
self._top = -topPadding
|
||||
elif boxAnchor in ['s','sw','se']:
|
||||
self._top = self._height-topPadding
|
||||
else:
|
||||
self._top = 0.5*self._eheight
|
||||
self._bottom = self._top - self._eheight
|
||||
|
||||
if boxAnchor in ['ne','e','se']:
|
||||
self._left = leftPadding - self._width
|
||||
elif boxAnchor in ['nw','w','sw']:
|
||||
self._left = leftPadding
|
||||
else:
|
||||
self._left = -self._ewidth*0.5
|
||||
self._right = self._left+self._ewidth
|
||||
|
||||
def _getTextAnchor(self):
|
||||
'''This can be overridden to allow special effects'''
|
||||
ta = self.textAnchor
|
||||
if ta=='boxauto': ta = _BA2TA[self._getBoxAnchor()]
|
||||
return ta
|
||||
|
||||
def _rawDraw(self):
|
||||
_text = self._text
|
||||
self._text = _text or ''
|
||||
self.computeSize()
|
||||
self._text = _text
|
||||
g = Group()
|
||||
g.translate(self.x + self.dx, self.y + self.dy)
|
||||
g.rotate(self.angle)
|
||||
|
||||
y = self._top - self._leading*self._baselineRatio
|
||||
textAnchor = self._getTextAnchor()
|
||||
if textAnchor == 'start':
|
||||
x = self._left
|
||||
elif textAnchor == 'middle':
|
||||
x = self._left + self._ewidth*0.5
|
||||
else:
|
||||
x = self._right
|
||||
|
||||
# paint box behind text just in case they
|
||||
# fill it
|
||||
if self.boxFillColor or (self.boxStrokeColor and self.boxStrokeWidth):
|
||||
g.add(Rect( self._left-self.leftPadding,
|
||||
self._bottom-self.bottomPadding,
|
||||
self._width,
|
||||
self._height,
|
||||
strokeColor=self.boxStrokeColor,
|
||||
strokeWidth=self.boxStrokeWidth,
|
||||
fillColor=self.boxFillColor)
|
||||
)
|
||||
|
||||
fillColor, fontName, fontSize = self.fillColor, self.fontName, self.fontSize
|
||||
strokeColor, strokeWidth, leading = self.strokeColor, self.strokeWidth, self._leading
|
||||
svgAttrs=getattr(self,'_svgAttrs',{})
|
||||
if strokeColor:
|
||||
for line in self._lines:
|
||||
s = _text2Path(line, x, y, fontName, fontSize, textAnchor)
|
||||
s.fillColor = fillColor
|
||||
s.strokeColor = strokeColor
|
||||
s.strokeWidth = strokeWidth
|
||||
g.add(s)
|
||||
y -= leading
|
||||
else:
|
||||
for line in self._lines:
|
||||
s = String(x, y, line, _svgAttrs=svgAttrs)
|
||||
s.textAnchor = textAnchor
|
||||
s.fontName = fontName
|
||||
s.fontSize = fontSize
|
||||
s.fillColor = fillColor
|
||||
g.add(s)
|
||||
y -= leading
|
||||
|
||||
return g
|
||||
|
||||
def draw(self):
|
||||
customDrawChanger = getattr(self,'customDrawChanger',None)
|
||||
if customDrawChanger:
|
||||
customDrawChanger(True,self)
|
||||
try:
|
||||
return self._rawDraw()
|
||||
finally:
|
||||
customDrawChanger(False,self)
|
||||
else:
|
||||
return self._rawDraw()
|
||||
|
||||
class LabelDecorator:
|
||||
_attrMap = AttrMap(
|
||||
x = AttrMapValue(isNumberOrNone,desc=''),
|
||||
y = AttrMapValue(isNumberOrNone,desc=''),
|
||||
dx = AttrMapValue(isNumberOrNone,desc=''),
|
||||
dy = AttrMapValue(isNumberOrNone,desc=''),
|
||||
angle = AttrMapValue(isNumberOrNone,desc=''),
|
||||
boxAnchor = AttrMapValue(isBoxAnchor,desc=''),
|
||||
boxStrokeColor = AttrMapValue(isColorOrNone,desc=''),
|
||||
boxStrokeWidth = AttrMapValue(isNumberOrNone,desc=''),
|
||||
boxFillColor = AttrMapValue(isColorOrNone,desc=''),
|
||||
fillColor = AttrMapValue(isColorOrNone,desc=''),
|
||||
strokeColor = AttrMapValue(isColorOrNone,desc=''),
|
||||
strokeWidth = AttrMapValue(isNumberOrNone),desc='',
|
||||
fontName = AttrMapValue(isNoneOrString,desc=''),
|
||||
fontSize = AttrMapValue(isNumberOrNone,desc=''),
|
||||
leading = AttrMapValue(isNumberOrNone,desc=''),
|
||||
width = AttrMapValue(isNumberOrNone,desc=''),
|
||||
maxWidth = AttrMapValue(isNumberOrNone,desc=''),
|
||||
height = AttrMapValue(isNumberOrNone,desc=''),
|
||||
textAnchor = AttrMapValue(isTextAnchor,desc=''),
|
||||
visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.textAnchor = 'start'
|
||||
self.boxAnchor = 'w'
|
||||
for a in self._attrMap.keys():
|
||||
if not hasattr(self,a): setattr(self,a,None)
|
||||
|
||||
def decorate(self,l,L):
|
||||
chart,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0 = l._callOutInfo
|
||||
L.setText(chart.categoryAxis.categoryNames[colNo])
|
||||
g.add(L)
|
||||
|
||||
def __call__(self,l):
|
||||
from copy import deepcopy
|
||||
L = Label()
|
||||
for a,v in self.__dict__.items():
|
||||
if v is None: v = getattr(l,a,None)
|
||||
setattr(L,a,v)
|
||||
self.decorate(l,L)
|
||||
|
||||
isOffsetMode=OneOf('high','low','bar','axis')
|
||||
class LabelOffset(PropHolder):
|
||||
_attrMap = AttrMap(
|
||||
posMode = AttrMapValue(isOffsetMode,desc="Where to base +ve offset"),
|
||||
pos = AttrMapValue(isNumber,desc='Value for positive elements'),
|
||||
negMode = AttrMapValue(isOffsetMode,desc="Where to base -ve offset"),
|
||||
neg = AttrMapValue(isNumber,desc='Value for negative elements'),
|
||||
)
|
||||
def __init__(self):
|
||||
self.posMode=self.negMode='axis'
|
||||
self.pos = self.neg = 0
|
||||
|
||||
def _getValue(self, chart, val):
|
||||
flipXY = chart._flipXY
|
||||
A = chart.categoryAxis
|
||||
jA = A.joinAxis
|
||||
if val>=0:
|
||||
mode = self.posMode
|
||||
delta = self.pos
|
||||
else:
|
||||
mode = self.negMode
|
||||
delta = self.neg
|
||||
if flipXY:
|
||||
v = A._x
|
||||
else:
|
||||
v = A._y
|
||||
if jA:
|
||||
if flipXY:
|
||||
_v = jA._x
|
||||
else:
|
||||
_v = jA._y
|
||||
if mode=='high':
|
||||
v = _v + jA._length
|
||||
elif mode=='low':
|
||||
v = _v
|
||||
elif mode=='bar':
|
||||
v = _v+val
|
||||
return v+delta
|
||||
|
||||
NoneOrInstanceOfLabelOffset=NoneOr(isInstanceOf(LabelOffset))
|
||||
|
||||
class PMVLabel(Label):
|
||||
_attrMap = AttrMap(
|
||||
BASE=Label,
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
Label.__init__(self)
|
||||
self._pmv = 0
|
||||
|
||||
def _getBoxAnchor(self):
|
||||
a = Label._getBoxAnchor(self)
|
||||
if self._pmv<0: a = {'nw':'se','n':'s','ne':'sw','w':'e','c':'c','e':'w','sw':'ne','s':'n','se':'nw'}[a]
|
||||
return a
|
||||
|
||||
def _getTextAnchor(self):
|
||||
a = Label._getTextAnchor(self)
|
||||
if self._pmv<0: a = {'start':'end', 'middle':'middle', 'end':'start'}[a]
|
||||
return a
|
||||
|
||||
class BarChartLabel(PMVLabel):
|
||||
"""
|
||||
An extended Label allowing for nudging, lines visibility etc
|
||||
"""
|
||||
_attrMap = AttrMap(
|
||||
BASE=PMVLabel,
|
||||
lineStrokeWidth = AttrMapValue(isNumberOrNone, desc="Non-zero for a drawn line"),
|
||||
lineStrokeColor = AttrMapValue(isColorOrNone, desc="Color for a drawn line"),
|
||||
fixedEnd = AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw ends +/-"),
|
||||
fixedStart = AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw starts +/-"),
|
||||
nudge = AttrMapValue(isNumber, desc="Non-zero sign dependent nudge"),
|
||||
boxTarget = AttrMapValue(OneOf('normal','anti','lo','hi','mid'),desc="one of ('normal','anti','lo','hi','mid')"),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
PMVLabel.__init__(self)
|
||||
self.lineStrokeWidth = 0
|
||||
self.lineStrokeColor = None
|
||||
self.fixedStart = self.fixedEnd = None
|
||||
self.nudge = 0
|
||||
|
||||
class NA_Label(BarChartLabel):
|
||||
"""
|
||||
An extended Label allowing for nudging, lines visibility etc
|
||||
"""
|
||||
_attrMap = AttrMap(
|
||||
BASE=BarChartLabel,
|
||||
text = AttrMapValue(isNoneOrString, desc="Text to be used for N/A values"),
|
||||
)
|
||||
def __init__(self):
|
||||
BarChartLabel.__init__(self)
|
||||
self.text = 'n/a'
|
||||
NoneOrInstanceOfNA_Label=NoneOr(isInstanceOf(NA_Label))
|
||||
|
||||
from reportlab.graphics.charts.utils import CustomDrawChanger
|
||||
class RedNegativeChanger(CustomDrawChanger):
|
||||
def __init__(self,fillColor=colors.red):
|
||||
CustomDrawChanger.__init__(self)
|
||||
self.fillColor = fillColor
|
||||
def _changer(self,obj):
|
||||
R = {}
|
||||
if obj._text.startswith('-'):
|
||||
R['fillColor'] = obj.fillColor
|
||||
obj.fillColor = self.fillColor
|
||||
return R
|
||||
390
reportlab/graphics/charts/utils.py
Normal file
390
reportlab/graphics/charts/utils.py
Normal file
@@ -0,0 +1,390 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2012
|
||||
#see license.txt for license details
|
||||
#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/utils.py
|
||||
|
||||
__version__=''' $Id$ '''
|
||||
__doc__="Utilities used here and there."
|
||||
from time import mktime, gmtime, strftime
|
||||
from math import log10, pi, floor, sin, cos, sqrt, hypot
|
||||
import weakref
|
||||
from reportlab.graphics.shapes import transformPoint, transformPoints, inverse, Ellipse, Group, String, Path, numericXShift
|
||||
from reportlab.lib.utils import flatten
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||||
|
||||
### Dinu's stuff used in some line plots (likely to vansih).
|
||||
def mkTimeTuple(timeString):
|
||||
"Convert a 'dd/mm/yyyy' formatted string to a tuple for use in the time module."
|
||||
|
||||
list = [0] * 9
|
||||
dd, mm, yyyy = list(map(int, timeString.split('/')))
|
||||
list[:3] = [yyyy, mm, dd]
|
||||
|
||||
return tuple(list)
|
||||
|
||||
def str2seconds(timeString):
|
||||
"Convert a number of seconds since the epoch into a date string."
|
||||
|
||||
return mktime(mkTimeTuple(timeString))
|
||||
|
||||
def seconds2str(seconds):
|
||||
"Convert a date string into the number of seconds since the epoch."
|
||||
|
||||
return strftime('%Y-%m-%d', gmtime(seconds))
|
||||
|
||||
### Aaron's rounding function for making nice values on axes.
|
||||
def nextRoundNumber(x):
|
||||
"""Return the first 'nice round number' greater than or equal to x
|
||||
|
||||
Used in selecting apropriate tick mark intervals; we say we want
|
||||
an interval which places ticks at least 10 points apart, work out
|
||||
what that is in chart space, and ask for the nextRoundNumber().
|
||||
Tries the series 1,2,5,10,20,50,100.., going up or down as needed.
|
||||
"""
|
||||
|
||||
#guess to nearest order of magnitude
|
||||
if x in (0, 1):
|
||||
return x
|
||||
|
||||
if x < 0:
|
||||
return -1.0 * nextRoundNumber(-x)
|
||||
else:
|
||||
lg = int(log10(x))
|
||||
|
||||
if lg == 0:
|
||||
if x < 1:
|
||||
base = 0.1
|
||||
else:
|
||||
base = 1.0
|
||||
elif lg < 0:
|
||||
base = 10.0 ** (lg - 1)
|
||||
else:
|
||||
base = 10.0 ** lg # e.g. base(153) = 100
|
||||
# base will always be lower than x
|
||||
|
||||
if base >= x:
|
||||
return base * 1.0
|
||||
elif (base * 2) >= x:
|
||||
return base * 2.0
|
||||
elif (base * 5) >= x:
|
||||
return base * 5.0
|
||||
else:
|
||||
return base * 10.0
|
||||
|
||||
_intervals=(.1, .2, .25, .5)
|
||||
_j_max=len(_intervals)-1
|
||||
def find_interval(lo,hi,I=5):
|
||||
'determine tick parameters for range [lo, hi] using I intervals'
|
||||
|
||||
if lo >= hi:
|
||||
if lo==hi:
|
||||
if lo==0:
|
||||
lo = -.1
|
||||
hi = .1
|
||||
else:
|
||||
lo = 0.9*lo
|
||||
hi = 1.1*hi
|
||||
else:
|
||||
raise ValueError("lo>hi")
|
||||
x=(hi - lo)/float(I)
|
||||
b= (x>0 and (x<1 or x>10)) and 10**floor(log10(x)) or 1
|
||||
b = b
|
||||
while 1:
|
||||
a = x/b
|
||||
if a<=_intervals[-1]: break
|
||||
b = b*10
|
||||
|
||||
j = 0
|
||||
while a>_intervals[j]: j = j + 1
|
||||
|
||||
while 1:
|
||||
ss = _intervals[j]*b
|
||||
n = lo/ss
|
||||
l = int(n)-(n<0)
|
||||
n = ss*l
|
||||
x = ss*(l+I)
|
||||
a = I*ss
|
||||
if n>0:
|
||||
if a>=hi:
|
||||
n = 0.0
|
||||
x = a
|
||||
elif hi<0:
|
||||
a = -a
|
||||
if lo>a:
|
||||
n = a
|
||||
x = 0
|
||||
if hi<=x and n<=lo: break
|
||||
j = j + 1
|
||||
if j>_j_max:
|
||||
j = 0
|
||||
b = b*10
|
||||
return n, x, ss, lo - n + x - hi
|
||||
|
||||
def find_good_grid(lower,upper,n=(4,5,6,7,8,9), grid=None):
|
||||
if grid:
|
||||
t = divmod(lower,grid)[0] * grid
|
||||
hi, z = divmod(upper,grid)
|
||||
if z>1e-8: hi = hi+1
|
||||
hi = hi*grid
|
||||
else:
|
||||
try:
|
||||
n[0]
|
||||
except TypeError:
|
||||
n = range(max(1,n-2),max(n+3,2))
|
||||
|
||||
w = 1e308
|
||||
for i in n:
|
||||
z=find_interval(lower,upper,i)
|
||||
if z[3]<w:
|
||||
t, hi, grid = z[:3]
|
||||
w=z[3]
|
||||
return t, hi, grid
|
||||
|
||||
def ticks(lower, upper, n=(4,5,6,7,8,9), split=1, percent=0, grid=None, labelVOffset=0):
|
||||
'''
|
||||
return tick positions and labels for range lower<=x<=upper
|
||||
n=number of intervals to try (can be a list or sequence)
|
||||
split=1 return ticks then labels else (tick,label) pairs
|
||||
'''
|
||||
t, hi, grid = find_good_grid(lower, upper, n, grid)
|
||||
power = floor(log10(grid))
|
||||
if power==0: power = 1
|
||||
w = grid/10.**power
|
||||
w = int(w)!=w
|
||||
|
||||
if power > 3 or power < -3:
|
||||
format = '%+'+repr(w+7)+'.0e'
|
||||
else:
|
||||
if power >= 0:
|
||||
digits = int(power)+w
|
||||
format = '%' + repr(digits)+'.0f'
|
||||
else:
|
||||
digits = w-int(power)
|
||||
format = '%'+repr(digits+2)+'.'+repr(digits)+'f'
|
||||
|
||||
if percent: format=format+'%%'
|
||||
T = []
|
||||
n = int(float(hi-t)/grid+0.1)+1
|
||||
if split:
|
||||
labels = []
|
||||
for i in range(n):
|
||||
v = t+grid*i
|
||||
T.append(v)
|
||||
labels.append(format % (v+labelVOffset))
|
||||
return T, labels
|
||||
else:
|
||||
for i in range(n):
|
||||
v = t+grid*i
|
||||
T.append((v, format % (v+labelVOffset)))
|
||||
return T
|
||||
|
||||
def findNones(data):
|
||||
m = len(data)
|
||||
if None in data:
|
||||
b = 0
|
||||
while b<m and data[b] is None:
|
||||
b += 1
|
||||
if b==m: return data
|
||||
l = m-1
|
||||
while data[l] is None:
|
||||
l -= 1
|
||||
l+=1
|
||||
if b or l: data = data[b:l]
|
||||
I = [i for i in range(len(data)) if data[i] is None]
|
||||
for i in I:
|
||||
data[i] = 0.5*(data[i-1]+data[i+1])
|
||||
return b, l, data
|
||||
return 0,m,data
|
||||
|
||||
def pairFixNones(pairs):
|
||||
Y = [x[1] for x in pairs]
|
||||
b,l,nY = findNones(Y)
|
||||
m = len(Y)
|
||||
if b or l<m or nY!=Y:
|
||||
if b or l<m: pairs = pairs[b:l]
|
||||
pairs = [(x[0],y) for x,y in zip(pairs,nY)]
|
||||
return pairs
|
||||
|
||||
def maverage(data,n=6):
|
||||
data = (n-1)*[data[0]]+data
|
||||
data = [float(sum(data[i-n:i]))/n for i in range(n,len(data)+1)]
|
||||
return data
|
||||
|
||||
def pairMaverage(data,n=6):
|
||||
return [(x[0],s) for x,s in zip(data, maverage([x[1] for x in data],n))]
|
||||
|
||||
class DrawTimeCollector(object):
|
||||
'''
|
||||
generic mechanism for collecting information about nodes at the time they are about to be drawn
|
||||
'''
|
||||
def __init__(self,formats=['gif']):
|
||||
self._nodes = weakref.WeakKeyDictionary()
|
||||
self.clear()
|
||||
self._pmcanv = None
|
||||
self.formats = formats
|
||||
self.disabled = False
|
||||
|
||||
def clear(self):
|
||||
self._info = []
|
||||
self._info_append = self._info.append
|
||||
|
||||
def record(self,func,node,*args,**kwds):
|
||||
self._nodes[node] = (func,args,kwds)
|
||||
node.__dict__['_drawTimeCallback'] = self
|
||||
|
||||
def __call__(self,node,canvas,renderer):
|
||||
func = self._nodes.get(node,None)
|
||||
if func:
|
||||
func, args, kwds = func
|
||||
i = func(node,canvas,renderer, *args, **kwds)
|
||||
if i is not None: self._info_append(i)
|
||||
|
||||
@staticmethod
|
||||
def rectDrawTimeCallback(node,canvas,renderer,**kwds):
|
||||
A = getattr(canvas,'ctm',None)
|
||||
if not A: return
|
||||
x1 = node.x
|
||||
y1 = node.y
|
||||
x2 = x1 + node.width
|
||||
y2 = y1 + node.height
|
||||
|
||||
D = kwds.copy()
|
||||
D['rect']=DrawTimeCollector.transformAndFlatten(A,((x1,y1),(x2,y2)))
|
||||
return D
|
||||
|
||||
@staticmethod
|
||||
def transformAndFlatten(A,p):
|
||||
''' transform an flatten a list of points
|
||||
A transformation matrix
|
||||
p points [(x0,y0),....(xk,yk).....]
|
||||
'''
|
||||
if tuple(A)!=(1,0,0,1,0,0):
|
||||
iA = inverse(A)
|
||||
p = transformPoints(iA,p)
|
||||
return tuple(flatten(p))
|
||||
|
||||
@property
|
||||
def pmcanv(self):
|
||||
if not self._pmcanv:
|
||||
import renderPM
|
||||
self._pmcanv = renderPM.PMCanvas(1,1)
|
||||
return self._pmcanv
|
||||
|
||||
def wedgeDrawTimeCallback(self,node,canvas,renderer,**kwds):
|
||||
A = getattr(canvas,'ctm',None)
|
||||
if not A: return
|
||||
if isinstance(node,Ellipse):
|
||||
c = self.pmcanv
|
||||
c.ellipse(node.cx, node.cy, node.rx,node.ry)
|
||||
p = c.vpath
|
||||
p = [(x[1],x[2]) for x in p]
|
||||
else:
|
||||
p = node.asPolygon().points
|
||||
p = [(p[i],p[i+1]) for i in range(0,len(p),2)]
|
||||
|
||||
D = kwds.copy()
|
||||
D['poly'] = self.transformAndFlatten(A,p)
|
||||
return D
|
||||
|
||||
def save(self,fnroot):
|
||||
'''
|
||||
save the current information known to this collector
|
||||
fnroot is the root name of a resource to name the saved info
|
||||
override this to get the right semantics for your collector
|
||||
'''
|
||||
import pprint
|
||||
f=open(fnroot+'.default-collector.out','w')
|
||||
try:
|
||||
pprint.pprint(self._info,f)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def xyDist(xxx_todo_changeme, xxx_todo_changeme1 ):
|
||||
'''return distance between two points'''
|
||||
(x0,y0) = xxx_todo_changeme
|
||||
(x1,y1) = xxx_todo_changeme1
|
||||
return hypot((x1-x0),(y1-y0))
|
||||
|
||||
def lineSegmentIntersect(xxx_todo_changeme2, xxx_todo_changeme3, xxx_todo_changeme4, xxx_todo_changeme5
|
||||
):
|
||||
(x00,y00) = xxx_todo_changeme2
|
||||
(x01,y01) = xxx_todo_changeme3
|
||||
(x10,y10) = xxx_todo_changeme4
|
||||
(x11,y11) = xxx_todo_changeme5
|
||||
p = x00,y00
|
||||
r = x01-x00,y01-y00
|
||||
|
||||
|
||||
q = x10,y10
|
||||
s = x11-x10,y11-y10
|
||||
|
||||
rs = float(r[0]*s[1]-r[1]*s[0])
|
||||
qp = q[0]-p[0],q[1]-p[1]
|
||||
|
||||
qpr = qp[0]*r[1]-qp[1]*r[0]
|
||||
qps = qp[0]*s[1]-qp[1]*s[0]
|
||||
|
||||
if abs(rs)<1e-8:
|
||||
if abs(qpr)<1e-8: return 'collinear'
|
||||
return None
|
||||
|
||||
t = qps/rs
|
||||
u = qpr/rs
|
||||
|
||||
if 0<=t<=1 and 0<=u<=1:
|
||||
return p[0]+t*r[0], p[1]+t*r[1]
|
||||
|
||||
def makeCircularString(x, y, radius, angle, text, fontName, fontSize, inside=0, G=None,textAnchor='start'):
|
||||
'''make a group with circular text in it'''
|
||||
if not G: G = Group()
|
||||
|
||||
angle %= 360
|
||||
pi180 = pi/180
|
||||
phi = angle*pi180
|
||||
width = stringWidth(text, fontName, fontSize)
|
||||
sig = inside and -1 or 1
|
||||
hsig = sig*0.5
|
||||
sig90 = sig*90
|
||||
|
||||
if textAnchor!='start':
|
||||
if textAnchor=='middle':
|
||||
phi += sig*(0.5*width)/radius
|
||||
elif textAnchor=='end':
|
||||
phi += sig*float(width)/radius
|
||||
elif textAnchor=='numeric':
|
||||
phi += sig*float(numericXShift(textAnchor,text,width,fontName,fontSize,None))/radius
|
||||
|
||||
for letter in text:
|
||||
width = stringWidth(letter, fontName, fontSize)
|
||||
beta = float(width)/radius
|
||||
h = Group()
|
||||
h.add(String(0, 0, letter, fontName=fontName,fontSize=fontSize,textAnchor="start"))
|
||||
h.translate(x+cos(phi)*radius,y+sin(phi)*radius) #translate to radius and angle
|
||||
h.rotate((phi-hsig*beta)/pi180-sig90) # rotate as needed
|
||||
G.add(h) #add to main group
|
||||
phi -= sig*beta #increment
|
||||
|
||||
return G
|
||||
|
||||
class CustomDrawChanger:
|
||||
'''
|
||||
a class to simplify making changes at draw time
|
||||
'''
|
||||
def __init__(self):
|
||||
self.store = None
|
||||
|
||||
def __call__(self,change,obj):
|
||||
if change:
|
||||
self.store = self._changer(obj)
|
||||
assert isinstance(self.store,dict), '%s.changer should return a dict of changed attributes' % self.__class__.__name__
|
||||
elif self.store is not None:
|
||||
for a,v in self.store.items():
|
||||
setattr(obj,a,v)
|
||||
self.store = None
|
||||
|
||||
def _changer(self,obj):
|
||||
'''
|
||||
When implemented this method should return a dictionary of
|
||||
original attribute values so that a future self(False,obj)
|
||||
can restore them.
|
||||
'''
|
||||
raise RuntimeError('Abstract method _changer called')
|
||||
233
reportlab/graphics/charts/utils3d.py
Normal file
233
reportlab/graphics/charts/utils3d.py
Normal file
@@ -0,0 +1,233 @@
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.graphics.shapes import Group, Drawing, Ellipse, Wedge, String, STATE_DEFAULTS, Polygon, Line
|
||||
|
||||
def _getShaded(col,shd=None,shading=0.1):
|
||||
if shd is None:
|
||||
from reportlab.lib.colors import Blacker
|
||||
if col: shd = Blacker(col,1-shading)
|
||||
return shd
|
||||
|
||||
def _getLit(col,shd=None,lighting=0.1):
|
||||
if shd is None:
|
||||
from reportlab.lib.colors import Whiter
|
||||
if col: shd = Whiter(col,1-lighting)
|
||||
return shd
|
||||
|
||||
|
||||
def _draw_3d_bar(G, x1, x2, y0, yhigh, xdepth, ydepth,
|
||||
fillColor=None, fillColorShaded=None,
|
||||
strokeColor=None, strokeWidth=1, shading=0.1):
|
||||
fillColorShaded = _getShaded(fillColor,None,shading)
|
||||
fillColorShadedTop = _getShaded(fillColor,None,shading/2.0)
|
||||
|
||||
def _add_3d_bar(x1, x2, y1, y2, xoff, yoff,
|
||||
G=G,strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor):
|
||||
G.add(Polygon((x1,y1, x1+xoff,y1+yoff, x2+xoff,y2+yoff, x2,y2),
|
||||
strokeWidth=strokeWidth, strokeColor=strokeColor, fillColor=fillColor,strokeLineJoin=1))
|
||||
|
||||
usd = max(y0, yhigh)
|
||||
if xdepth or ydepth:
|
||||
if y0!=yhigh: #non-zero height
|
||||
_add_3d_bar( x2, x2, y0, yhigh, xdepth, ydepth, fillColor=fillColorShaded) #side
|
||||
|
||||
_add_3d_bar(x1, x2, usd, usd, xdepth, ydepth, fillColor=fillColorShadedTop) #top
|
||||
|
||||
G.add(Polygon((x1,y0,x2,y0,x2,yhigh,x1,yhigh),
|
||||
strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor,strokeLineJoin=1)) #front
|
||||
|
||||
if xdepth or ydepth:
|
||||
G.add(Line( x1, usd, x2, usd, strokeWidth=strokeWidth, strokeColor=strokeColor or fillColorShaded))
|
||||
|
||||
class _YStrip:
|
||||
def __init__(self,y0,y1, slope, fillColor, fillColorShaded, shading=0.1):
|
||||
self.y0 = y0
|
||||
self.y1 = y1
|
||||
self.slope = slope
|
||||
self.fillColor = fillColor
|
||||
self.fillColorShaded = _getShaded(fillColor,fillColorShaded,shading)
|
||||
|
||||
def _ystrip_poly( x0, x1, y0, y1, xoff, yoff):
|
||||
return [x0,y0,x0+xoff,y0+yoff,x1+xoff,y1+yoff,x1,y1]
|
||||
|
||||
|
||||
def _make_3d_line_info( G, x0, x1, y0, y1, z0, z1,
|
||||
theta_x, theta_y,
|
||||
fillColor, fillColorShaded=None, tileWidth=1,
|
||||
strokeColor=None, strokeWidth=None, strokeDashArray=None,
|
||||
shading=0.1):
|
||||
zwidth = abs(z1-z0)
|
||||
xdepth = zwidth*theta_x
|
||||
ydepth = zwidth*theta_y
|
||||
depth_slope = xdepth==0 and 1e150 or -ydepth/float(xdepth)
|
||||
|
||||
x = float(x1-x0)
|
||||
slope = x==0 and 1e150 or (y1-y0)/x
|
||||
|
||||
c = slope>depth_slope and _getShaded(fillColor,fillColorShaded,shading) or fillColor
|
||||
zy0 = z0*theta_y
|
||||
zx0 = z0*theta_x
|
||||
|
||||
tileStrokeWidth = 0.6
|
||||
if tileWidth is None:
|
||||
D = [(x1,y1)]
|
||||
else:
|
||||
T = ((y1-y0)**2+(x1-x0)**2)**0.5
|
||||
tileStrokeWidth *= tileWidth
|
||||
if T<tileWidth:
|
||||
D = [(x1,y1)]
|
||||
else:
|
||||
n = int(T/float(tileWidth))+1
|
||||
dx = float(x1-x0)/n
|
||||
dy = float(y1-y0)/n
|
||||
D = []
|
||||
a = D.append
|
||||
for i in range(1,n):
|
||||
a((x0+dx*i,y0+dy*i))
|
||||
|
||||
a = G.add
|
||||
x_0 = x0+zx0
|
||||
y_0 = y0+zy0
|
||||
for x,y in D:
|
||||
x_1 = x+zx0
|
||||
y_1 = y+zy0
|
||||
P = Polygon(_ystrip_poly(x_0, x_1, y_0, y_1, xdepth, ydepth),
|
||||
fillColor = c, strokeColor=c, strokeWidth=tileStrokeWidth)
|
||||
a((0,z0,z1,x_0,y_0,P))
|
||||
x_0 = x_1
|
||||
y_0 = y_1
|
||||
|
||||
from math import pi, sin, cos
|
||||
_pi_2 = pi*0.5
|
||||
_2pi = 2*pi
|
||||
_180_pi=180./pi
|
||||
|
||||
def _2rad(angle):
|
||||
return angle/_180_pi
|
||||
|
||||
def mod_2pi(radians):
|
||||
radians = radians % _2pi
|
||||
if radians<-1e-6: radians += _2pi
|
||||
return radians
|
||||
|
||||
def _2deg(o):
|
||||
return o*_180_pi
|
||||
|
||||
def _360(a):
|
||||
a %= 360
|
||||
if a<-1e-6: a += 360
|
||||
return a
|
||||
|
||||
_ZERO = 1e-8
|
||||
_ONE = 1-_ZERO
|
||||
class _Segment:
|
||||
def __init__(self,s,i,data):
|
||||
S = data[s]
|
||||
x0 = S[i-1][0]
|
||||
y0 = S[i-1][1]
|
||||
x1 = S[i][0]
|
||||
y1 = S[i][1]
|
||||
if x1<x0:
|
||||
x0,y0,x1,y1 = x1,y1,x0,y0
|
||||
# (y-y0)*(x1-x0) = (y1-y0)*(x-x0)
|
||||
# (x1-x0)*y + (y0-y1)*x = y0*(x1-x0)+x0*(y0-y1)
|
||||
# a*y+b*x = c
|
||||
self.a = float(x1-x0)
|
||||
self.b = float(y1-y0)
|
||||
self.x0 = x0
|
||||
self.x1 = x1
|
||||
self.y0 = y0
|
||||
self.y1 = y1
|
||||
self.series = s
|
||||
self.i = i
|
||||
self.s = s
|
||||
|
||||
def __str__(self):
|
||||
return '[(%s,%s),(%s,%s)]' % (self.x0,self.y0,self.x1,self.y1)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def intersect(self,o,I):
|
||||
'''try to find an intersection with _Segment o
|
||||
'''
|
||||
x0 = self.x0
|
||||
ox0 = o.x0
|
||||
assert x0<=ox0
|
||||
if ox0>self.x1: return 1
|
||||
if o.s==self.s and o.i in (self.i-1,self.i+1): return
|
||||
a = self.a
|
||||
b = self.b
|
||||
oa = o.a
|
||||
ob = o.b
|
||||
det = ob*a - oa*b
|
||||
if -1e-8<det<1e-8: return
|
||||
dx = x0 - ox0
|
||||
dy = self.y0 - o.y0
|
||||
u = (oa*dy - ob*dx)/det
|
||||
ou = (a*dy - b*dx)/det
|
||||
if u<0 or u>1 or ou<0 or ou>1: return
|
||||
x = x0 + u*a
|
||||
y = self.y0 + u*b
|
||||
if _ZERO<u<_ONE:
|
||||
t = self.s,self.i,x,y
|
||||
if t not in I: I.append(t)
|
||||
if _ZERO<ou<_ONE:
|
||||
t = o.s,o.i,x,y
|
||||
if t not in I: I.append(t)
|
||||
|
||||
def _segKey(a):
|
||||
return (a.x0,a.x1,a.y0,a.y1,a.s,a.i)
|
||||
|
||||
def find_intersections(data,small=0):
|
||||
'''
|
||||
data is a sequence of series
|
||||
each series is a list of (x,y) coordinates
|
||||
where x & y are ints or floats
|
||||
|
||||
find_intersections returns a sequence of 4-tuples
|
||||
i, j, x, y
|
||||
|
||||
where i is a data index j is an insertion position for data[i]
|
||||
and x, y are coordinates of an intersection of series data[i]
|
||||
with some other series. If correctly implemented we get all such
|
||||
intersections. We don't count endpoint intersections and consider
|
||||
parallel lines as non intersecting (even when coincident).
|
||||
We ignore segments that have an estimated size less than small.
|
||||
'''
|
||||
|
||||
#find all line segments
|
||||
S = []
|
||||
a = S.append
|
||||
for s in range(len(data)):
|
||||
ds = data[s]
|
||||
if not ds: continue
|
||||
n = len(ds)
|
||||
if n==1: continue
|
||||
for i in range(1,n):
|
||||
seg = _Segment(s,i,data)
|
||||
if seg.a+abs(seg.b)>=small: a(seg)
|
||||
S.sort(key=_segKey)
|
||||
I = []
|
||||
n = len(S)
|
||||
for i in range(0,n-1):
|
||||
s = S[i]
|
||||
for j in range(i+1,n):
|
||||
if s.intersect(S[j],I)==1: break
|
||||
I.sort()
|
||||
return I
|
||||
|
||||
if __name__=='__main__':
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.lib.colors import lightgrey, pink
|
||||
D = Drawing(300,200)
|
||||
_draw_3d_bar(D, 10, 20, 10, 50, 5, 5, fillColor=lightgrey, strokeColor=pink)
|
||||
_draw_3d_bar(D, 30, 40, 10, 45, 5, 5, fillColor=lightgrey, strokeColor=pink)
|
||||
|
||||
D.save(formats=['pdf'],outDir='.',fnRoot='_draw_3d_bar')
|
||||
|
||||
print(find_intersections([[(0,0.5),(1,0.5),(0.5,0),(0.5,1)],[(.2666666667,0.4),(0.1,0.4),(0.1,0.2),(0,0),(1,1)],[(0,1),(0.4,0.1),(1,0.1)]]))
|
||||
print(find_intersections([[(0.1, 0.2), (0.1, 0.4)], [(0, 1), (0.4, 0.1)]]))
|
||||
print(find_intersections([[(0.2, 0.4), (0.1, 0.4)], [(0.1, 0.8), (0.4, 0.1)]]))
|
||||
print(find_intersections([[(0,0),(1,1)],[(0.4,0.1),(1,0.1)]]))
|
||||
print(find_intersections([[(0,0.5),(1,0.5),(0.5,0),(0.5,1)],[(0,0),(1,1)],[(0.1,0.8),(0.4,0.1),(1,0.1)]]))
|
||||
Reference in New Issue
Block a user