Initial commit: IT Site Survey AI v2 with all features
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/__init__.py
|
||||
__version__='3.3.0'
|
||||
__doc__='''Business charts'''
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,94 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/areas.py
|
||||
|
||||
__version__='3.3.0'
|
||||
__doc__='''This module defines a Area mixin classes'''
|
||||
|
||||
from reportlab.lib.validators import isNumber, 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
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,165 @@
|
||||
from reportlab.lib.colors import _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")
|
||||
@@ -0,0 +1,473 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/doughnut.py
|
||||
# doughnut chart
|
||||
|
||||
__version__='3.3.0'
|
||||
__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).
|
||||
|
||||
"""
|
||||
|
||||
from math import sin, cos, pi
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isNumber, isListOfStringsOrNone, OneOf,\
|
||||
isBoolean, isNumberOrNone, isListOfNoneOrNumber,\
|
||||
isListOfListOfNoneOrNumber, EitherOr, NoneOr, \
|
||||
isCallable
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.graphics.shapes import Group, Drawing, Wedge
|
||||
from reportlab.graphics.widgetbase import TypedPropertyCollection
|
||||
from reportlab.graphics.charts.piecharts import AbstractPieChart, WedgeProperties, _addWedgeLabel, fixLabelOverlaps
|
||||
from reportlab.graphics.charts.areas import PlotArea
|
||||
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(BASE=AbstractPieChart,
|
||||
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(EitherOr((isListOfNoneOrNumber,isListOfListOfNoneOrNumber)), 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),
|
||||
innerRadiusFraction = AttrMapValue(isNumberOrNone,
|
||||
desc='None or the fraction of the radius to be used as the inner hole.\nIf not a suitable default will be used.'),
|
||||
labelClass=AttrMapValue(NoneOr(isCallable), desc="A class factory to use for non simple labels"),
|
||||
angleRange = AttrMapValue(isNumber, desc='total degree range for the doughnut defaults to 360'),
|
||||
)
|
||||
|
||||
def __init__(self,**kwds):
|
||||
PlotArea.__init__(self)
|
||||
setattr(self,'x',kwds.pop('x',0))
|
||||
setattr(self,'y',kwds.pop('y',0))
|
||||
setattr(self,'width',kwds.pop('width',100))
|
||||
setattr(self,'height',kwds.pop('height',100))
|
||||
setattr(self,'data',kwds.pop('data',[1,1]))
|
||||
setattr(self,'labels',kwds.pop('labels',None))
|
||||
setattr(self,'startAngle',kwds.pop('startAngle',90))
|
||||
setattr(self,'direction',kwds.pop('direction',"clockwise"))
|
||||
setattr(self,'simpleLabels',kwds.pop('simpleLabels',1))
|
||||
setattr(self,'checkLabelOverlap',kwds.pop('checkLabelOverlap',0))
|
||||
setattr(self,'sideLabels',kwds.pop('sideLabels',0))
|
||||
setattr(self,'innerRadiusFraction',kwds.pop('innerRadiusFraction',None))
|
||||
setattr(self,'slices',kwds.pop('slices',TypedPropertyCollection(SectorProperties)))
|
||||
setattr(self,'angleRange',kwds.pop('angleRange',360))
|
||||
|
||||
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):
|
||||
s = sum(data)
|
||||
f = min(360,self.angleRange)/s if s!=0 else 1
|
||||
return [f*d for d in data]
|
||||
|
||||
def makeSectors(self):
|
||||
# normalize slice data
|
||||
data = self.data
|
||||
multi = isListOfListOfNoneOrNumber(data)
|
||||
if multi:
|
||||
#it's a nested list, more than one sequence
|
||||
normData = []
|
||||
n = []
|
||||
for l in data:
|
||||
t = self.normalizeData(l)
|
||||
normData.append(t)
|
||||
n.append(len(t))
|
||||
self._seriesCount = max(n)
|
||||
else:
|
||||
normData = self.normalizeData(data)
|
||||
n = len(normData)
|
||||
self._seriesCount = n
|
||||
|
||||
#labels
|
||||
checkLabelOverlap = self.checkLabelOverlap
|
||||
L = []
|
||||
L_add = L.append
|
||||
|
||||
labels = self.labels
|
||||
if labels is None:
|
||||
labels = []
|
||||
if not multi:
|
||||
labels = [''] * n
|
||||
else:
|
||||
for m in n:
|
||||
labels = list(labels) + [''] * m
|
||||
else:
|
||||
#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 multi:
|
||||
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
|
||||
self.labels = labels
|
||||
|
||||
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)
|
||||
irf = self.innerRadiusFraction
|
||||
|
||||
if multi:
|
||||
#multi-series doughnut
|
||||
ndata = len(data)
|
||||
if irf is None:
|
||||
yir = (yradius/2.5)/ndata
|
||||
xir = (xradius/2.5)/ndata
|
||||
else:
|
||||
yir = yradius*irf
|
||||
xir = xradius*irf
|
||||
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
|
||||
aa = abs(startAngle-endAngle)
|
||||
if aa<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[sn,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 len(series) > 1:
|
||||
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
|
||||
|
||||
shader = sectorStyle.shadingKind
|
||||
if shader:
|
||||
nshades = aa / float(sectorStyle.shadingAngle)
|
||||
if nshades > 1:
|
||||
shader = colors.Whiter if shader=='lighten' else colors.Blacker
|
||||
nshades = 1+int(nshades)
|
||||
shadingAmount = 1-sectorStyle.shadingAmount
|
||||
if sectorStyle.shadingDirection=='normal':
|
||||
dsh = (1-shadingAmount)/float(nshades-1)
|
||||
shf1 = shadingAmount
|
||||
else:
|
||||
dsh = (shadingAmount-1)/float(nshades-1)
|
||||
shf1 = 1
|
||||
shda = (a2-a1)/float(nshades)
|
||||
shsc = sectorStyle.fillColor
|
||||
theSector.fillColor = None
|
||||
for ish in range(nshades):
|
||||
sha1 = a1 + ish*shda
|
||||
sha2 = a1 + (ish+1)*shda
|
||||
shc = shader(shsc,shf1 + dsh*ish)
|
||||
if len(series)>1:
|
||||
shSector = Wedge(cx, cy, xr, sha1, sha2, yradius=yr, radius1=xr1, yradius1=yr1)
|
||||
else:
|
||||
shSector = Wedge(cx, cy, xr, sha1, sha2, yradius=yr, radius1=xr1, yradius1=yr1, annular=True)
|
||||
shSector.fillColor = shc
|
||||
shSector.strokeColor = None
|
||||
shSector.strokeWidth = 0
|
||||
g.add(shSector)
|
||||
|
||||
g.add(theSector)
|
||||
|
||||
if sn == 0 and sectorStyle.visible and sectorStyle.label_visible:
|
||||
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
|
||||
if irf is None:
|
||||
yir = yradius/2.5
|
||||
xir = xradius/2.5
|
||||
else:
|
||||
yir = yradius*irf
|
||||
xir = xradius*irf
|
||||
for i,angle in enumerate(normData):
|
||||
endAngle = (startAngle + (angle * whichWay)) #% 360
|
||||
aa = abs(startAngle-endAngle)
|
||||
if aa<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
|
||||
|
||||
shader = sectorStyle.shadingKind
|
||||
if shader:
|
||||
nshades = aa / float(sectorStyle.shadingAngle)
|
||||
if nshades > 1:
|
||||
shader = colors.Whiter if shader=='lighten' else colors.Blacker
|
||||
nshades = 1+int(nshades)
|
||||
shadingAmount = 1-sectorStyle.shadingAmount
|
||||
if sectorStyle.shadingDirection=='normal':
|
||||
dsh = (1-shadingAmount)/float(nshades-1)
|
||||
shf1 = shadingAmount
|
||||
else:
|
||||
dsh = (shadingAmount-1)/float(nshades-1)
|
||||
shf1 = 1
|
||||
shda = (a2-a1)/float(nshades)
|
||||
shsc = sectorStyle.fillColor
|
||||
theSector.fillColor = None
|
||||
for ish in range(nshades):
|
||||
sha1 = a1 + ish*shda
|
||||
sha2 = a1 + (ish+1)*shda
|
||||
shc = shader(shsc,shf1 + dsh*ish)
|
||||
if n > 1:
|
||||
shSector = Wedge(cx, cy, xradius, sha1, sha2, yradius=yradius, radius1=xir, yradius1=yir)
|
||||
elif n==1:
|
||||
shSector = Wedge(cx, cy, xradius, sha1, sha2, yradius=yradius, radius1=xir, yradius1=yir, annular=True)
|
||||
shSector.fillColor = shc
|
||||
shSector.strokeColor = None
|
||||
shSector.strokeWidth = 0
|
||||
g.add(shSector)
|
||||
|
||||
g.add(theSector)
|
||||
|
||||
# now draw a label
|
||||
if labels[i] and sectorStyle.visible and sectorStyle.label_visible:
|
||||
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')
|
||||
@@ -0,0 +1,647 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/legends.py
|
||||
|
||||
__version__='3.3.0'
|
||||
__doc__="""This will be a collection of legends to be used with charts."""
|
||||
|
||||
import copy
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isNumber, OneOf, isString, isColorOrNone,\
|
||||
isNumberOrNone, isListOfNumbersOrNone, 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.widgets.markers import uSymbol2Symbol, isSymbol
|
||||
from reportlab.lib.utils import isSeq, find_locals, isStr, asNative
|
||||
from reportlab.graphics.shapes import _baseGFontName
|
||||
|
||||
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 isStr(s):
|
||||
return asNative(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(isNumberOrNone, 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"),
|
||||
vAlign = AttrMapValue(OneOf('top','bottom','middle'),desc='vertical alignment in the row'),
|
||||
)
|
||||
|
||||
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"),
|
||||
leading = AttrMapValue(isNumberOrNone, desc="text leading"),
|
||||
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.leading = None #will be used as 1.2*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'
|
||||
sc.vAlign = 'top' #that's current
|
||||
sc.leading = None
|
||||
|
||||
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
|
||||
fontName = self.fontName
|
||||
ascent=getFont(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 += 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 = sum([m[-1] for m in maxWidth])+xW*nCols
|
||||
else:
|
||||
deltax = max(maxWidth[-1]+xW,deltax)
|
||||
width = 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]
|
||||
lineCount = _getLineCount(name)
|
||||
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 = sc.leading or 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
|
||||
|
||||
vA = sc.vAlign
|
||||
if vA=='top':
|
||||
vAdy = 0
|
||||
else:
|
||||
vAdy = -fL * (lineCount - len(lines))
|
||||
if vA=='middle': vAdy *= 0.5
|
||||
|
||||
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+vAdy,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"),
|
||||
strokeWidth = AttrMapValue(isNumberOrNone, desc="thickness of the swatch"),
|
||||
strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc="dash array for swatch line"),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
from reportlab.lib.colors import red
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.width = 20
|
||||
self.height = 1
|
||||
self.strokeColor = red
|
||||
self.strokeDashArray = None
|
||||
self.strokeWidth = 1
|
||||
|
||||
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.strokeWidth
|
||||
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
|
||||
l.strokeWidth = strokeWidth
|
||||
return l
|
||||
@@ -0,0 +1,801 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/linecharts.py
|
||||
|
||||
__version__='3.3.0'
|
||||
__doc__="""This modules defines a very preliminary Line Chart example."""
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isNumber, isNumberOrNone, isColorOrNone, \
|
||||
isListOfStringsOrNone, isBoolean, NoneOr, \
|
||||
isListOfNumbersOrNone, isStringOrNone, OneOf, Percentage
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.lib.utils import flatten
|
||||
from reportlab.graphics.widgetbase import TypedPropertyCollection, PropHolder, tpcGetItem
|
||||
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, YCategoryAxis, XValueAxis
|
||||
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
|
||||
from .utils import FillPairedData
|
||||
|
||||
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),
|
||||
inFill = AttrMapValue(isBoolean, desc='If true flood fill to x axis',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
|
||||
"""
|
||||
_flipXY = 0
|
||||
|
||||
_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 :-)
|
||||
if self._flipXY:
|
||||
self.categoryAxis = YCategoryAxis()
|
||||
self.valueAxis = XValueAxis()
|
||||
else:
|
||||
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._vzero = vzero = self.valueAxis.scale(0)
|
||||
self._hngs = hngs = 0.5 * self.groupSpacing * normFactor
|
||||
|
||||
pairs = set()
|
||||
P = [].append
|
||||
cscale = self.categoryAxis.scale
|
||||
vscale = self.valueAxis.scale
|
||||
data = self.data
|
||||
flipXY = self._flipXY
|
||||
n = len(data)
|
||||
for rowNo,row in enumerate(data):
|
||||
if isinstance(row, FillPairedData):
|
||||
other = row.other
|
||||
if 0<=other<n:
|
||||
if other==rowNo:
|
||||
raise ValueError('data row %r may not be paired with itself' % rowNo)
|
||||
t = (rowNo,other)
|
||||
pairs.add((min(t),max(t)))
|
||||
else:
|
||||
raise ValueError('data row %r is paired with invalid data row %r' % (rowNo, other))
|
||||
line = [].append
|
||||
for colNo,datum in enumerate(row):
|
||||
if datum is not None:
|
||||
c, g = cscale(colNo)
|
||||
v = vscale(datum)
|
||||
line((v, c+hngs) if flipXY else (c+hngs, v))
|
||||
P(line.__self__)
|
||||
P = P.__self__
|
||||
|
||||
#if there are some paired lines we ensure only one is created
|
||||
for rowNo, other in pairs:
|
||||
P[rowNo] = FillPairedData(P[rowNo],other)
|
||||
self._pairInFills = len(pairs)
|
||||
self._positions = P
|
||||
|
||||
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 = self._positions
|
||||
if self.reversePlotOrder: P.reverse()
|
||||
lines = self.lines
|
||||
styleCount = len(lines)
|
||||
flipXY = self._flipXY
|
||||
cA = self.categoryAxis
|
||||
vA = self.valueAxis
|
||||
_inFill = self.inFill
|
||||
if (_inFill or self._pairInFills or
|
||||
[rowNo for rowNo in range(len(P))
|
||||
if getattr(lines[rowNo%styleCount],'inFill',False)]
|
||||
):
|
||||
if flipXY:
|
||||
infillC = cA._x
|
||||
infillV0 = vA._y
|
||||
infillV1 = infillV0 + cA._length
|
||||
else:
|
||||
infillC = cA._y
|
||||
infillV0 = vA._x
|
||||
infillV1 = infillV0 + cA._length
|
||||
inFillG = getattr(self,'_inFillG',g)
|
||||
vzero = self._vzero
|
||||
bypos = None
|
||||
|
||||
# Iterate over data rows.
|
||||
for rowNo, row in enumerate(reversed(P) if self.reversePlotOrder else P):
|
||||
styleIdx = rowNo % styleCount
|
||||
rowStyle = lines[styleIdx]
|
||||
strokeColor = rowStyle.strokeColor
|
||||
fillColor = getattr(rowStyle,'fillColor',strokeColor)
|
||||
inFill = getattr(rowStyle,'inFill',_inFill)
|
||||
dash = getattr(rowStyle, 'strokeDashArray', None)
|
||||
lineStyle = getattr(rowStyle,'lineStyle',None)
|
||||
|
||||
if hasattr(rowStyle, 'strokeWidth'):
|
||||
strokeWidth = rowStyle.strokeWidth
|
||||
elif hasattr(lines, 'strokeWidth'):
|
||||
strokeWidth = lines.strokeWidth
|
||||
else:
|
||||
strokeWidth = None
|
||||
|
||||
# Iterate over data columns.
|
||||
if lineStyle=='bar':
|
||||
if bypos is None:
|
||||
if flipXY:
|
||||
bypos = max(vA._x,vzero)
|
||||
byneg = min(vA._x+vA._length,vzero)
|
||||
else:
|
||||
bypos = max(vA._y,vzero)
|
||||
byneg = min(vA._y+vA._length,vzero)
|
||||
barWidth = getattr(rowStyle,'barWidth',Percentage(50))
|
||||
if isinstance(barWidth,Percentage):
|
||||
hbw = self._hngs*barWidth*0.01
|
||||
else:
|
||||
hbw = barWidth*0.5
|
||||
for x, y in row:
|
||||
if flipXY:
|
||||
v0 = byneg if x<vzero else bypos
|
||||
t = v0, y-hbw, x-v0, 2*hbw
|
||||
else:
|
||||
v0 = byneg if y<vzero else bypos
|
||||
t = x-hbw,v0,2*hbw,y-v0
|
||||
g.add(Rect(*t,strokeWidth=strokeWidth,strokeColor=strokeColor,fillColor=fillColor))
|
||||
elif self.joinedLines or lineStyle=='joinedLine':
|
||||
points = flatten(row)
|
||||
if inFill or isinstance(row,FillPairedData):
|
||||
filler = getattr(rowStyle, 'filler', None)
|
||||
if isinstance(row,FillPairedData):
|
||||
fpoints = points + flatten(reversed(P[row.other]))
|
||||
else:
|
||||
if flipXY:
|
||||
fpoints = [infillC,infillV0] + points + [infillC,infillV1]
|
||||
else:
|
||||
fpoints = [infillV0,infillC] + points + [infillV1,infillC]
|
||||
if filler:
|
||||
filler.fill(self,inFillG,rowNo,fillColor,fpoints)
|
||||
else:
|
||||
inFillG.add(Polygon(fpoints,fillColor=fillColor,strokeColor=strokeColor if strokeColor==fillColor else None,strokeWidth=strokeWidth or 0.1))
|
||||
if not inFill or inFill==2 or strokeColor!=fillColor:
|
||||
line = PolyLine(points,strokeColor=strokeColor,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(lines, 'symbol'):
|
||||
uSymbol = lines.symbol
|
||||
else:
|
||||
uSymbol = None
|
||||
|
||||
if uSymbol:
|
||||
for colNo,(x,y) in enumerate(row):
|
||||
symbol = uSymbol2Symbol(tpcGetItem(uSymbol,colNo),x,y,rowStyle.strokeColor)
|
||||
if symbol: g.add(symbol)
|
||||
|
||||
# Draw item labels.
|
||||
for colNo, (x, y) in enumerate(row):
|
||||
self.drawLabel(g, rowNo, colNo, x, y)
|
||||
|
||||
return g
|
||||
|
||||
def draw(self):
|
||||
"Draws itself."
|
||||
|
||||
vA, cA = self.valueAxis, self.categoryAxis
|
||||
if self._flipXY:
|
||||
vA.setPosition(self.x, self.y, self.width)
|
||||
else:
|
||||
vA.setPosition(self.x, self.y, self.height)
|
||||
if vA: vA.joinAxis = cA
|
||||
if cA: cA.joinAxis = vA
|
||||
vA.configure(self.data)
|
||||
|
||||
y = self.y
|
||||
x = self.x
|
||||
if self._flipXY:
|
||||
# If zero is in chart, put y axis there, otherwise
|
||||
# use bottom.
|
||||
crossesAt = vA.scale(0)
|
||||
if not ((crossesAt > x + self.width) or (crossesAt < x)):
|
||||
x = crossesAt
|
||||
cA.setPosition(x, y, self.height)
|
||||
else:
|
||||
# If zero is in chart, put x axis there, otherwise
|
||||
# use bottom.
|
||||
crossesAt = vA.scale(0)
|
||||
if not ((crossesAt > y + self.height) or (crossesAt < y)):
|
||||
y = crossesAt
|
||||
cA.setPosition(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(HorizontalLineChart):
|
||||
_flipXY = 1
|
||||
|
||||
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
|
||||
|
||||
def sampleCandleStick():
|
||||
from reportlab.graphics.widgetbase import CandleSticks
|
||||
d = Drawing(400, 200)
|
||||
chart = HorizontalLineChart()
|
||||
d.add(chart)
|
||||
chart.y = 20
|
||||
boxMid = (100, 110, 120, 130)
|
||||
hi = [m+10 for m in boxMid]
|
||||
lo = [m-10 for m in boxMid]
|
||||
boxHi = [m+6 for m in boxMid]
|
||||
boxLo = [m-4 for m in boxMid]
|
||||
boxFillColor = colors.pink
|
||||
boxWidth = 20
|
||||
crossWidth = 10
|
||||
candleStrokeWidth = 0.5
|
||||
candleStrokeColor = colors.black
|
||||
chart.valueAxis.avoidBoundSpace = 5
|
||||
|
||||
chart.valueAxis.valueMin = min(min(boxMid),min(hi),min(lo),min(boxLo),min(boxHi))
|
||||
chart.valueAxis.valueMax = max(max(boxMid),max(hi),max(lo),max(boxLo),max(boxHi))
|
||||
lines = chart.lines
|
||||
lines[0].strokeColor = None
|
||||
I = range(len(boxMid))
|
||||
chart.data = [boxMid]
|
||||
lines[0].symbol = candles = CandleSticks(chart=chart, boxFillColor=boxFillColor, boxWidth=boxWidth, crossWidth=crossWidth, strokeWidth=candleStrokeWidth, strokeColor=candleStrokeColor)
|
||||
for i in I: candles[i].setProperties(dict(position=i,boxMid=boxMid[i],crossLo=lo[i],crossHi=hi[i],boxLo=boxLo[i],boxHi=boxHi[i]))
|
||||
return d
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/markers.py
|
||||
|
||||
__version__='3.3.0'
|
||||
__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, 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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,185 @@
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.colors import black, white
|
||||
from reportlab.graphics.shapes import Polygon, String, Drawing, Group, Rect
|
||||
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.widgets.grids import ShadedRect
|
||||
|
||||
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):
|
||||
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")
|
||||
@@ -0,0 +1,411 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/spider.py
|
||||
# spider chart, also known as radar chart
|
||||
|
||||
__version__='3.3.0'
|
||||
__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.
|
||||
"""
|
||||
|
||||
from math import sin, cos, pi
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.validators import isNumber, isListOfNumbersOrNone,\
|
||||
isColorOrNone, isListOfStringsOrNone, OneOf,\
|
||||
isBoolean, isNumberOrNone,\
|
||||
isStringOrNone, isStringOrNone, EitherOr,\
|
||||
isCallable, NoneOr
|
||||
from reportlab.lib.attrmap import *
|
||||
from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, PolyLine, \
|
||||
STATE_DEFAULTS
|
||||
from reportlab.graphics.widgetbase import 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"),
|
||||
strandLabelClass=AttrMapValue(NoneOr(isCallable), desc="A class factory to use for the strand labels"),
|
||||
spokeLabels = AttrMapValue(None, desc="collection of spoke label descriptor objects"),
|
||||
spokeLabelClass=AttrMapValue(NoneOr(isCallable), desc="A class factory to use for the spoke labels"),
|
||||
)
|
||||
|
||||
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=None):
|
||||
"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 labelClass(self,kind):
|
||||
klass = getattr(self,f'{kind}LabelClass',None)
|
||||
if not klass:
|
||||
klass = globals()[f'{kind.capitalize()}Label']
|
||||
return klass
|
||||
|
||||
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(self.labelClass('spoke'), 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=self.labelClass('strand'))
|
||||
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')
|
||||
@@ -0,0 +1,581 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2017
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/textlabels.py
|
||||
__version__='3.3.0'
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.utils import simpleSplit
|
||||
from reportlab.lib.geomutils import normalizeTRBL
|
||||
from reportlab.lib.validators import isNumber, isNumberOrNone, OneOf, isColorOrNone, isString, \
|
||||
isTextAnchor, isBoxAnchor, isBoolean, NoneOr, isInstanceOf, isNoneOrString, isNoneOrCallable, \
|
||||
isSubclassOf, EitherOr, isListOfNumbers
|
||||
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.widgetbase import Widget, PropHolder
|
||||
from reportlab.graphics.shapes import DirectDraw
|
||||
from reportlab.platypus import XPreformatted, Flowable
|
||||
from reportlab.lib.styles import ParagraphStyle, PropertySet
|
||||
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER
|
||||
_ta2al = dict(start=TA_LEFT,end=TA_RIGHT,middle=TA_CENTER)
|
||||
from ..utils import text2Path as _text2Path #here for continuity
|
||||
|
||||
_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'},
|
||||
}
|
||||
|
||||
try:
|
||||
from rlextra.graphics.canvasadapter import DirectDrawFlowable
|
||||
except ImportError:
|
||||
DirectDrawFlowable = None
|
||||
|
||||
_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')"),
|
||||
boxRx = AttrMapValue(isNumber,desc='box corner x radius'),
|
||||
boxRy = AttrMapValue(isNumber,desc='box corner y radius'),
|
||||
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'),
|
||||
padding = AttrMapValue(EitherOr((isNumberOrNone,isListOfNumbers)),'TRBL css like padding'),
|
||||
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),
|
||||
ddf = AttrMapValue(NoneOr(isSubclassOf(DirectDraw),'NoneOrDirectDraw'),desc="A DirectDrawFlowable instance", _advancedUsage=1),
|
||||
ddfKlass = AttrMapValue(NoneOr(isSubclassOf(Flowable),'NoneOrDirectDraw'),desc="A Flowable class for direct drawing (default is XPreformatted", _advancedUsage=1),
|
||||
ddfStyle = AttrMapValue(NoneOr((isSubclassOf(PropertySet),isInstanceOf(PropertySet))),desc="A style or style class for a ddfKlass or None", _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',
|
||||
boxRx = 0,
|
||||
boxRy = 0,
|
||||
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,
|
||||
ddf = DirectDrawFlowable,
|
||||
ddfKlass = getattr(self.__class__,'ddfKlass',None),
|
||||
ddfStyle = getattr(self.__class__,'ddfStyle',None),
|
||||
)
|
||||
|
||||
@property
|
||||
def padding(self):
|
||||
p = self.topPadding, self.rightPadding, self.bottomPadding, self.leftPadding
|
||||
n = len(set(p))
|
||||
if n==1: return p[0]
|
||||
elif n==2 and p[0]==p[2] and p[1]==p[3]: return p[:2]
|
||||
elif n==3 and p[1]==p[3]: return p[:3]
|
||||
return p
|
||||
|
||||
@padding.setter
|
||||
def padding(self,p):
|
||||
self.topPadding, self.rightPadding, self.bottomPadding, self.leftPadding = normalizeTRBL(p)
|
||||
|
||||
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 _getBaseLineRatio(self):
|
||||
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
|
||||
|
||||
def _computeSizeEnd(self,objH):
|
||||
self._height = self.height or (objH + self.topPadding + self.bottomPadding)
|
||||
self._ewidth = (self._width-self.leftPadding-self.rightPadding)
|
||||
self._eheight = (self._height-self.topPadding-self.bottomPadding)
|
||||
boxAnchor = self._getBoxAnchor()
|
||||
if boxAnchor in ['n','ne','nw']:
|
||||
self._top = -self.topPadding
|
||||
elif boxAnchor in ['s','sw','se']:
|
||||
self._top = self._height-self.topPadding
|
||||
else:
|
||||
self._top = 0.5*self._eheight
|
||||
self._bottom = self._top - self._eheight
|
||||
|
||||
if boxAnchor in ['ne','e','se']:
|
||||
self._left = self.leftPadding - self._width
|
||||
elif boxAnchor in ['nw','w','sw']:
|
||||
self._left = self.leftPadding
|
||||
else:
|
||||
self._left = -self._ewidth*0.5
|
||||
self._right = self._left+self._ewidth
|
||||
|
||||
def computeSize(self):
|
||||
# the thing will draw in its own coordinate system
|
||||
ddfKlass = getattr(self,'ddfKlass',None)
|
||||
if not ddfKlass:
|
||||
self._lineWidths = []
|
||||
self._lines = simpleSplit(self._text,self.fontName,self.fontSize,self.maxWidth)
|
||||
if not self.width:
|
||||
self._width = self.leftPadding+self.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
|
||||
self._getBaseLineRatio()
|
||||
if self.leading:
|
||||
self._leading = self.leading
|
||||
elif self.useAscentDescent:
|
||||
self._leading = self._ascent - self._descent
|
||||
else:
|
||||
self._leading = self.fontSize*1.2
|
||||
objH = self._leading*len(self._lines)
|
||||
else:
|
||||
if self.ddf is None:
|
||||
raise RuntimeError('DirectDrawFlowable class is not available you need the rlextra package as well as reportlab')
|
||||
sty = dict(
|
||||
name='xlabel-generated',
|
||||
fontName=self.fontName,
|
||||
fontSize=self.fontSize,
|
||||
fillColor=self.fillColor,
|
||||
strokeColor=self.strokeColor,
|
||||
)
|
||||
|
||||
if not self.ddfStyle:
|
||||
sty = ParagraphStyle(**sty)
|
||||
elif isinstance(self.ddfStyle,PropertySet):
|
||||
sty = self.ddfStyle.clone(**sty)
|
||||
elif isinstance(self.ddfStyle,type) and issubclass(self.ddfStyle,PropertySet):
|
||||
sty = self.ddfStyle(**sty)
|
||||
else:
|
||||
raise ValueError(f'ddfStyle has invalid type {type(self.ddfStyle)}')
|
||||
|
||||
self._style = sty
|
||||
self._getBaseLineRatio()
|
||||
if self.useAscentDescent:
|
||||
sty.autoLeading = True
|
||||
sty.leading = self._ascent - self._descent
|
||||
else:
|
||||
sty.leading = self.leading if self.leading else self.fontSize*1.2
|
||||
self._leading = sty.leading
|
||||
ta = self._getTextAnchor()
|
||||
|
||||
aW = self.maxWidth or 0x7fffffff
|
||||
if ta!='start':
|
||||
sty.alignment = TA_LEFT
|
||||
obj = ddfKlass(self._text,style=sty)
|
||||
_, objH = obj.wrap(aW,0x7fffffff)
|
||||
aW = self.maxWidth or obj._width_max
|
||||
sty.alignment = _ta2al[ta]
|
||||
self._ddfObj = obj = ddfKlass(self._text,style=sty)
|
||||
_, objH = obj.wrap(aW,0x7fffffff)
|
||||
|
||||
if not self.width:
|
||||
self._width = self.leftPadding+self.rightPadding
|
||||
self._width += obj._width_max
|
||||
else:
|
||||
self._width = self.width
|
||||
self._computeSizeEnd(objH)
|
||||
|
||||
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)
|
||||
|
||||
ddfKlass = getattr(self,'ddfKlass',None)
|
||||
if ddfKlass:
|
||||
x = self._left
|
||||
else:
|
||||
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,
|
||||
rx=self.boxRx,
|
||||
ry=self.boxRy,
|
||||
))
|
||||
|
||||
if ddfKlass:
|
||||
g1 = Group()
|
||||
g1.translate(x,self._top-self._eheight)
|
||||
g1.add(self.ddf(self._ddfObj))
|
||||
g.add(g1)
|
||||
else:
|
||||
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):
|
||||
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, **kwds):
|
||||
Label.__init__(self, **kwds)
|
||||
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, **kwds):
|
||||
PMVLabel.__init__(self, **kwds)
|
||||
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
|
||||
|
||||
class XLabel(Label):
|
||||
'''like label but uses XPreFormatted/Paragraph to draw the _text'''
|
||||
_attrMap = AttrMap(BASE=Label,
|
||||
)
|
||||
def __init__(self,*args,**kwds):
|
||||
Label.__init__(self,*args,**kwds)
|
||||
self.ddfKlass = kwds.pop('ddfKlass',XPreformatted)
|
||||
self.ddf = kwds.pop('directDrawClass',self.ddf)
|
||||
|
||||
if False:
|
||||
def __init__(self,*args,**kwds):
|
||||
self._flowableClass = kwds.pop('flowableClass',XPreformatted)
|
||||
ddf = kwds.pop('directDrawClass',DirectDrawFlowable)
|
||||
if ddf is None:
|
||||
raise RuntimeError('DirectDrawFlowable class is not available you need the rlextra package as well as reportlab')
|
||||
self._ddf = ddf
|
||||
Label.__init__(self,*args,**kwds)
|
||||
def computeSize(self):
|
||||
# the thing will draw in its own coordinate system
|
||||
self._lineWidths = []
|
||||
sty = self._style = ParagraphStyle('xlabel-generated',
|
||||
fontName=self.fontName,
|
||||
fontSize=self.fontSize,
|
||||
fillColor=self.fillColor,
|
||||
strokeColor=self.strokeColor,
|
||||
)
|
||||
self._getBaseLineRatio()
|
||||
if self.useAscentDescent:
|
||||
sty.autoLeading = True
|
||||
sty.leading = self._ascent - self._descent
|
||||
else:
|
||||
sty.leading = self.leading if self.leading else self.fontSize*1.2
|
||||
self._leading = sty.leading
|
||||
ta = self._getTextAnchor()
|
||||
aW = self.maxWidth or 0x7fffffff
|
||||
if ta!='start':
|
||||
sty.alignment = TA_LEFT
|
||||
obj = self._flowableClass(self._text,style=sty)
|
||||
_, objH = obj.wrap(aW,0x7fffffff)
|
||||
aW = self.maxWidth or obj._width_max
|
||||
sty.alignment = _ta2al[ta]
|
||||
self._obj = obj = self._flowableClass(self._text,style=sty)
|
||||
_, objH = obj.wrap(aW,0x7fffffff)
|
||||
|
||||
if not self.width:
|
||||
self._width = self.leftPadding+self.rightPadding
|
||||
self._width += self._obj._width_max
|
||||
else:
|
||||
self._width = self.width
|
||||
self._computeSizeEnd(objH)
|
||||
|
||||
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)
|
||||
|
||||
x = self._left
|
||||
|
||||
# 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)
|
||||
)
|
||||
g1 = Group()
|
||||
g1.translate(x,self._top-self._eheight)
|
||||
g1.add(self._ddf(self._obj))
|
||||
g.add(g1)
|
||||
return g
|
||||
@@ -0,0 +1,483 @@
|
||||
#Copyright ReportLab Europe Ltd. 2000-2026
|
||||
#see license.txt for license details
|
||||
#history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/utils.py
|
||||
__all__ = (
|
||||
'angle2corner',
|
||||
'angle2dir',
|
||||
'boxCornerCoords',
|
||||
'CustomDrawChanger',
|
||||
'DrawTimeCollector',
|
||||
'FillPairedData',
|
||||
'find_good_grid',
|
||||
'find_interval',
|
||||
'findNones',
|
||||
'lineSegmentIntersect',
|
||||
'makeCircularString',
|
||||
'maverage',
|
||||
'mkTimeTuple',
|
||||
'nextRoundNumber',
|
||||
'pairFixNones',
|
||||
'pairMaverage',
|
||||
'seconds2str',
|
||||
'str2seconds',
|
||||
'ticks',
|
||||
'xyDist',
|
||||
)
|
||||
|
||||
__version__='3.4.8'
|
||||
__doc__="Utilities used here and there."
|
||||
from time import mktime, gmtime, strftime
|
||||
from math import log10, pi, floor, sin, cos, hypot
|
||||
import weakref
|
||||
from reportlab.graphics.shapes import transformPoints, inverse, Ellipse, Group, String, 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."
|
||||
|
||||
L = [0] * 9
|
||||
dd, mm, yyyy = list(map(int, timeString.split('/')))
|
||||
L[:3] = [yyyy, mm, dd]
|
||||
|
||||
return tuple(L)
|
||||
|
||||
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:
|
||||
'''
|
||||
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')
|
||||
|
||||
class FillPairedData(list):
|
||||
def __init__(self,v,other=0):
|
||||
list.__init__(self,v)
|
||||
self.other = other
|
||||
|
||||
_arange2dirs = [
|
||||
(-1,22.5,'e'),
|
||||
(22.5,67.5,'ne'),
|
||||
(67.5,112.5,'n'),
|
||||
(112.5,157.5,'nw'),
|
||||
(157.5,202.5,'w'),
|
||||
(202.5,247.5,'sw'),
|
||||
(247.5,292.5,'s'),
|
||||
(292.5,337.5,'se'),
|
||||
(337.5,361,'e'),
|
||||
]
|
||||
def angle2dir(angle):
|
||||
'''converts mathematical angle to a compass point from a math angle where
|
||||
0 degrees lies along the x axis ie east==0 degrees
|
||||
|
||||
>>> [angle2dir(_) for _ in [0,360]+[__[0] for __ in _arange2dirs]+[__[1] for __ in _arange2dirs]]
|
||||
['e', 'e', 'e', 'e', 'ne', 'n', 'nw', 'w', 'sw', 's', 'se', 'e', 'ne', 'n', 'nw', 'w', 'sw', 's', 'se', 'e']
|
||||
'''
|
||||
a = angle % 360
|
||||
for lo, hi, d in _arange2dirs:
|
||||
if lo<a<=hi: return d
|
||||
return 'c'
|
||||
#I tested the bisect version below; it works, but is fractionally slower
|
||||
#import bisect
|
||||
#_elemk = lambda _: _[1]
|
||||
#return _arange2dirs[bisect.bisect_left(_arange2dirs,angle % 360,key=_elemk)][2]
|
||||
|
||||
_cornerNames=dict(e='w',ne='sw',n='s',nw='se',w='e',sw='ne',s='n',se='nw',c='c')
|
||||
def angle2corner(angle):
|
||||
'''converts a direction angle to a box corner name effectively the reverse direction
|
||||
>>> [angle2corner(_) for _ in [0,360]+[__[0] for __ in _arange2dirs]+[__[1] for __ in _arange2dirs]]
|
||||
['w', 'w', 'w', 'w', 'sw', 's', 'se', 'e', 'ne', 'n', 'nw', 'w', 'sw', 's', 'se', 'e', 'ne', 'n', 'nw', 'w']
|
||||
'''
|
||||
return _cornerNames[angle2dir(angle)]
|
||||
|
||||
def boxCornerCoords(bb, cn):
|
||||
'''return (x,y) for bounding box and corner name
|
||||
>>> bb=(1,0,0,1);[boxCornerCoords(bb,_) for _ in 'c n ne e se s sw w nw'.split()]
|
||||
[(0.5, 0.5), (0.5, 1), (1, 1), (1, 0.5), (1, 0), (0.5, 0), (0, 0), (0, 0.5), (0, 1)]
|
||||
>>> boxCornerCoords(bb,'z')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: invalid box corner name 'z'
|
||||
'''
|
||||
if bb[0]>bb[2] or bb[1]>bb[3]:
|
||||
bb = (min(bb[0],bb[2]),min(bb[1],bb[3]),max(bb[0],bb[2]),max(bb[1],bb[3]))
|
||||
if cn not in ('n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'c'):
|
||||
raise ValueError(f'invalid box corner name {cn!r}')
|
||||
if cn in ('c','s','n'):
|
||||
x = (bb[0]+bb[2])/2
|
||||
elif cn in ('ne','e','se'):
|
||||
x = bb[2]
|
||||
else:
|
||||
x = bb[0]
|
||||
if cn in ('e','c','w'):
|
||||
y = (bb[1]+bb[3])/2
|
||||
elif cn in ('nw','n','ne'):
|
||||
y = bb[3]
|
||||
else:
|
||||
y = bb[1]
|
||||
return x, y
|
||||
|
||||
if __name__=='__main__':
|
||||
import doctest
|
||||
doctest.testmod()
|
||||
@@ -0,0 +1,230 @@
|
||||
from reportlab.graphics.shapes import Drawing, 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
|
||||
_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