legends.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. #Copyright ReportLab Europe Ltd. 2000-2017
  2. #see license.txt for license details
  3. #history https://hg.reportlab.com/hg-public/reportlab/log/tip/src/reportlab/graphics/charts/legends.py
  4. __version__='3.3.0'
  5. __doc__="""This will be a collection of legends to be used with charts."""
  6. import copy, operator
  7. from reportlab.lib import colors
  8. from reportlab.lib.validators import isNumber, OneOf, isString, isColorOrNone,\
  9. isNumberOrNone, isListOfNumbersOrNone, isStringOrNone, isBoolean,\
  10. EitherOr, NoneOr, AutoOr, isAuto, Auto, isBoxAnchor, SequenceOf, isInstanceOf
  11. from reportlab.lib.attrmap import *
  12. from reportlab.pdfbase.pdfmetrics import stringWidth, getFont
  13. from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder
  14. from reportlab.graphics.shapes import Drawing, Group, String, Rect, Line, STATE_DEFAULTS
  15. from reportlab.graphics.charts.areas import PlotArea
  16. from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol
  17. from reportlab.lib.utils import isSeq, find_locals, isStr, asNative
  18. from reportlab.graphics.shapes import _baseGFontName
  19. from functools import reduce
  20. def _transMax(n,A):
  21. X = n*[0]
  22. m = 0
  23. for a in A:
  24. m = max(m,len(a))
  25. for i,x in enumerate(a):
  26. X[i] = max(X[i],x)
  27. X = [0] + X[:m]
  28. for i in range(m):
  29. X[i+1] += X[i]
  30. return X
  31. def _objStr(s):
  32. if isStr(s):
  33. return asNative(s)
  34. else:
  35. return str(s)
  36. def _getStr(s):
  37. if isSeq(s):
  38. return list(map(_getStr,s))
  39. else:
  40. return _objStr(s)
  41. def _getLines(s):
  42. if isSeq(s):
  43. return tuple([(x or '').split('\n') for x in s])
  44. else:
  45. return (s or '').split('\n')
  46. def _getLineCount(s):
  47. T = _getLines(s)
  48. if isSeq(s):
  49. return max([len(x) for x in T])
  50. else:
  51. return len(T)
  52. def _getWidths(i,s, fontName, fontSize, subCols):
  53. S = []
  54. aS = S.append
  55. if isSeq(s):
  56. for j,t in enumerate(s):
  57. sc = subCols[j,i]
  58. fN = getattr(sc,'fontName',fontName)
  59. fS = getattr(sc,'fontSize',fontSize)
  60. m = [stringWidth(x, fN, fS) for x in t.split('\n')]
  61. m = max(sc.minWidth,m and max(m) or 0)
  62. aS(m)
  63. aS(sc.rpad)
  64. del S[-1]
  65. else:
  66. sc = subCols[0,i]
  67. fN = getattr(sc,'fontName',fontName)
  68. fS = getattr(sc,'fontSize',fontSize)
  69. m = [stringWidth(x, fN, fS) for x in s.split('\n')]
  70. aS(max(sc.minWidth,m and max(m) or 0))
  71. return S
  72. class SubColProperty(PropHolder):
  73. dividerLines = 0
  74. _attrMap = AttrMap(
  75. minWidth = AttrMapValue(isNumber,desc="minimum width for this subcol"),
  76. rpad = AttrMapValue(isNumber,desc="right padding for this subcol"),
  77. align = AttrMapValue(OneOf('left','right','center','centre','numeric'),desc='alignment in subCol'),
  78. fontName = AttrMapValue(isString, desc="Font name of the strings"),
  79. fontSize = AttrMapValue(isNumber, desc="Font size of the strings"),
  80. leading = AttrMapValue(isNumberOrNone, desc="leading for the strings"),
  81. fillColor = AttrMapValue(isColorOrNone, desc="fontColor"),
  82. underlines = AttrMapValue(EitherOr((NoneOr(isInstanceOf(Line)),SequenceOf(isInstanceOf(Line),emptyOK=0,lo=0,hi=0x7fffffff))), desc="underline definitions"),
  83. overlines = AttrMapValue(EitherOr((NoneOr(isInstanceOf(Line)),SequenceOf(isInstanceOf(Line),emptyOK=0,lo=0,hi=0x7fffffff))), desc="overline definitions"),
  84. dx = AttrMapValue(isNumber, desc="x offset from default position"),
  85. dy = AttrMapValue(isNumber, desc="y offset from default position"),
  86. vAlign = AttrMapValue(OneOf('top','bottom','middle'),desc='vertical alignment in the row'),
  87. )
  88. class LegendCallout:
  89. def _legendValues(legend,*args):
  90. '''return a tuple of values from the first function up the stack with isinstance(self,legend)'''
  91. L = find_locals(lambda L: L.get('self',None) is legend and L or None)
  92. return tuple([L[a] for a in args])
  93. _legendValues = staticmethod(_legendValues)
  94. def _selfOrLegendValues(self,legend,*args):
  95. L = find_locals(lambda L: L.get('self',None) is legend and L or None)
  96. return tuple([getattr(self,a,L[a]) for a in args])
  97. def __call__(self,legend,g,thisx,y,colName):
  98. col, name = colName
  99. class LegendSwatchCallout(LegendCallout):
  100. def __call__(self,legend,g,thisx,y,i,colName,swatch):
  101. col, name = colName
  102. class LegendColEndCallout(LegendCallout):
  103. def __call__(self,legend, g, x, xt, y, width, lWidth):
  104. pass
  105. class Legend(Widget):
  106. """A simple legend containing rectangular swatches and strings.
  107. The swatches are filled rectangles whenever the respective
  108. color object in 'colorNamePairs' is a subclass of Color in
  109. reportlab.lib.colors. Otherwise the object passed instead is
  110. assumed to have 'x', 'y', 'width' and 'height' attributes.
  111. A legend then tries to set them or catches any error. This
  112. lets you plug-in any widget you like as a replacement for
  113. the default rectangular swatches.
  114. Strings can be nicely aligned left or right to the swatches.
  115. """
  116. _attrMap = AttrMap(
  117. x = AttrMapValue(isNumber, desc="x-coordinate of upper-left reference point"),
  118. y = AttrMapValue(isNumber, desc="y-coordinate of upper-left reference point"),
  119. deltax = AttrMapValue(isNumberOrNone, desc="x-distance between neighbouring swatches"),
  120. deltay = AttrMapValue(isNumberOrNone, desc="y-distance between neighbouring swatches"),
  121. dxTextSpace = AttrMapValue(isNumber, desc="Distance between swatch rectangle and text"),
  122. autoXPadding = AttrMapValue(isNumber, desc="x Padding between columns if deltax=None",advancedUsage=1),
  123. autoYPadding = AttrMapValue(isNumber, desc="y Padding between rows if deltay=None",advancedUsage=1),
  124. yGap = AttrMapValue(isNumber, desc="Additional gap between rows",advancedUsage=1),
  125. dx = AttrMapValue(isNumber, desc="Width of swatch rectangle"),
  126. dy = AttrMapValue(isNumber, desc="Height of swatch rectangle"),
  127. columnMaximum = AttrMapValue(isNumber, desc="Max. number of items per column"),
  128. alignment = AttrMapValue(OneOf("left", "right"), desc="Alignment of text with respect to swatches"),
  129. colorNamePairs = AttrMapValue(None, desc="List of color/name tuples (color can also be widget)"),
  130. fontName = AttrMapValue(isString, desc="Font name of the strings"),
  131. fontSize = AttrMapValue(isNumber, desc="Font size of the strings"),
  132. leading = AttrMapValue(isNumberOrNone, desc="text leading"),
  133. fillColor = AttrMapValue(isColorOrNone, desc="swatches filling color"),
  134. strokeColor = AttrMapValue(isColorOrNone, desc="Border color of the swatches"),
  135. strokeWidth = AttrMapValue(isNumber, desc="Width of the border color of the swatches"),
  136. swatchMarker = AttrMapValue(NoneOr(AutoOr(isSymbol)), desc="None, Auto() or makeMarker('Diamond') ...",advancedUsage=1),
  137. callout = AttrMapValue(None, desc="a user callout(self,g,x,y,(color,text))",advancedUsage=1),
  138. boxAnchor = AttrMapValue(isBoxAnchor,'Anchor point for the legend area'),
  139. variColumn = AttrMapValue(isBoolean,'If true column widths may vary (default is false)',advancedUsage=1),
  140. 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),
  141. dividerWidth = AttrMapValue(isNumber, desc="dividerLines width",advancedUsage=1),
  142. dividerColor = AttrMapValue(isColorOrNone, desc="dividerLines color",advancedUsage=1),
  143. dividerDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array for dividerLines.',advancedUsage=1),
  144. dividerOffsX = AttrMapValue(SequenceOf(isNumber,emptyOK=0,lo=2,hi=2), desc='divider lines X offsets',advancedUsage=1),
  145. dividerOffsY = AttrMapValue(isNumber, desc="dividerLines Y offset",advancedUsage=1),
  146. colEndCallout = AttrMapValue(None, desc="a user callout(self,g, x, xt, y,width, lWidth)",advancedUsage=1),
  147. subCols = AttrMapValue(None,desc="subColumn properties"),
  148. swatchCallout = AttrMapValue(None, desc="a user swatch callout(self,g,x,y,i,(col,name),swatch)",advancedUsage=1),
  149. swdx = AttrMapValue(isNumber, desc="x position adjustment for the swatch"),
  150. swdy = AttrMapValue(isNumber, desc="y position adjustment for the swatch"),
  151. )
  152. def __init__(self):
  153. # Upper-left reference point.
  154. self.x = 0
  155. self.y = 0
  156. # Alginment of text with respect to swatches.
  157. self.alignment = "left"
  158. # x- and y-distances between neighbouring swatches.
  159. self.deltax = 75
  160. self.deltay = 20
  161. self.autoXPadding = 5
  162. self.autoYPadding = 2
  163. # Size of swatch rectangle.
  164. self.dx = 10
  165. self.dy = 10
  166. self.swdx = 0
  167. self.swdy = 0
  168. # Distance between swatch rectangle and text.
  169. self.dxTextSpace = 10
  170. # Max. number of items per column.
  171. self.columnMaximum = 3
  172. # Color/name pairs.
  173. self.colorNamePairs = [ (colors.red, "red"),
  174. (colors.blue, "blue"),
  175. (colors.green, "green"),
  176. (colors.pink, "pink"),
  177. (colors.yellow, "yellow") ]
  178. # Font name and size of the labels.
  179. self.fontName = STATE_DEFAULTS['fontName']
  180. self.fontSize = STATE_DEFAULTS['fontSize']
  181. self.leading = None #will be used as 1.2*fontSize
  182. self.fillColor = STATE_DEFAULTS['fillColor']
  183. self.strokeColor = STATE_DEFAULTS['strokeColor']
  184. self.strokeWidth = STATE_DEFAULTS['strokeWidth']
  185. self.swatchMarker = None
  186. self.boxAnchor = 'nw'
  187. self.yGap = 0
  188. self.variColumn = 0
  189. self.dividerLines = 0
  190. self.dividerWidth = 0.5
  191. self.dividerDashArray = None
  192. self.dividerColor = colors.black
  193. self.dividerOffsX = (0,0)
  194. self.dividerOffsY = 0
  195. self.colEndCallout = None
  196. self._init_subCols()
  197. def _init_subCols(self):
  198. sc = self.subCols = TypedPropertyCollection(SubColProperty)
  199. sc.rpad = 1
  200. sc.dx = sc.dy = sc.minWidth = 0
  201. sc.align = 'right'
  202. sc[0].align = 'left'
  203. sc.vAlign = 'top' #that's current
  204. sc.leading = None
  205. def _getChartStyleName(self,chart):
  206. for a in 'lines', 'bars', 'slices', 'strands':
  207. if hasattr(chart,a): return a
  208. return None
  209. def _getChartStyle(self,chart):
  210. return getattr(chart,self._getChartStyleName(chart),None)
  211. def _getTexts(self,colorNamePairs):
  212. if not isAuto(colorNamePairs):
  213. texts = [_getStr(p[1]) for p in colorNamePairs]
  214. else:
  215. chart = getattr(colorNamePairs,'chart',getattr(colorNamePairs,'obj',None))
  216. texts = [chart.getSeriesName(i,'series %d' % i) for i in range(chart._seriesCount)]
  217. return texts
  218. def _calculateMaxBoundaries(self, colorNamePairs):
  219. "Calculate the maximum width of some given strings."
  220. fontName = self.fontName
  221. fontSize = self.fontSize
  222. subCols = self.subCols
  223. M = [_getWidths(i, m, fontName, fontSize, subCols) for i,m in enumerate(self._getTexts(colorNamePairs))]
  224. if not M:
  225. return [0,0]
  226. n = max([len(m) for m in M])
  227. if self.variColumn:
  228. columnMaximum = self.columnMaximum
  229. return [_transMax(n,M[r:r+columnMaximum]) for r in range(0,len(M),self.columnMaximum)]
  230. else:
  231. return _transMax(n,M)
  232. def _calcHeight(self):
  233. dy = self.dy
  234. yGap = self.yGap
  235. thisy = upperlefty = self.y - dy
  236. fontSize = self.fontSize
  237. fontName = self.fontName
  238. ascent=getFont(fontName).face.ascent/1000.
  239. if ascent==0: ascent=0.718 # default (from helvetica)
  240. ascent *= fontSize
  241. leading = fontSize*1.2
  242. deltay = self.deltay
  243. if not deltay: deltay = max(dy,leading)+self.autoYPadding
  244. columnCount = 0
  245. count = 0
  246. lowy = upperlefty
  247. lim = self.columnMaximum - 1
  248. for name in self._getTexts(self.colorNamePairs):
  249. y0 = thisy+(dy-ascent)*0.5
  250. y = y0 - _getLineCount(name)*leading
  251. leadingMove = 2*y0-y-thisy
  252. newy = thisy-max(deltay,leadingMove)-yGap
  253. lowy = min(y,newy,lowy)
  254. if count==lim:
  255. count = 0
  256. thisy = upperlefty
  257. columnCount += 1
  258. else:
  259. thisy = newy
  260. count = count+1
  261. return upperlefty - lowy
  262. def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor):
  263. return Rect(x, thisy, dx, dy,
  264. fillColor = fillColor,
  265. strokeColor = strokeColor,
  266. strokeWidth = strokeWidth,
  267. )
  268. def draw(self):
  269. colorNamePairs = self.colorNamePairs
  270. autoCP = isAuto(colorNamePairs)
  271. if autoCP:
  272. chart = getattr(colorNamePairs,'chart',getattr(colorNamePairs,'obj',None))
  273. swatchMarker = None
  274. autoCP = Auto(obj=chart)
  275. n = chart._seriesCount
  276. chartTexts = self._getTexts(colorNamePairs)
  277. else:
  278. swatchMarker = getattr(self,'swatchMarker',None)
  279. if isAuto(swatchMarker):
  280. chart = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None))
  281. swatchMarker = Auto(obj=chart)
  282. n = len(colorNamePairs)
  283. dx = self.dx
  284. dy = self.dy
  285. alignment = self.alignment
  286. columnMaximum = self.columnMaximum
  287. deltax = self.deltax
  288. deltay = self.deltay
  289. dxTextSpace = self.dxTextSpace
  290. fontName = self.fontName
  291. fontSize = self.fontSize
  292. fillColor = self.fillColor
  293. strokeWidth = self.strokeWidth
  294. strokeColor = self.strokeColor
  295. subCols = self.subCols
  296. leading = fontSize*1.2
  297. yGap = self.yGap
  298. if not deltay:
  299. deltay = max(dy,leading)+self.autoYPadding
  300. ba = self.boxAnchor
  301. maxWidth = self._calculateMaxBoundaries(colorNamePairs)
  302. nCols = int((n+columnMaximum-1)/(columnMaximum*1.0))
  303. xW = dx+dxTextSpace+self.autoXPadding
  304. variColumn = self.variColumn
  305. if variColumn:
  306. width = sum([m[-1] for m in maxWidth])+xW*nCols
  307. else:
  308. deltax = max(maxWidth[-1]+xW,deltax)
  309. width = nCols*deltax
  310. maxWidth = nCols*[maxWidth]
  311. thisx = self.x
  312. thisy = self.y - self.dy
  313. if ba not in ('ne','n','nw','autoy'):
  314. height = self._calcHeight()
  315. if ba in ('e','c','w'):
  316. thisy += height/2.
  317. else:
  318. thisy += height
  319. if ba not in ('nw','w','sw','autox'):
  320. if ba in ('n','c','s'):
  321. thisx -= width/2
  322. else:
  323. thisx -= width
  324. upperlefty = thisy
  325. g = Group()
  326. ascent=getFont(fontName).face.ascent/1000.
  327. if ascent==0: ascent=0.718 # default (from helvetica)
  328. ascent *= fontSize # normalize
  329. lim = columnMaximum - 1
  330. callout = getattr(self,'callout',None)
  331. scallout = getattr(self,'swatchCallout',None)
  332. dividerLines = self.dividerLines
  333. if dividerLines:
  334. dividerWidth = self.dividerWidth
  335. dividerColor = self.dividerColor
  336. dividerDashArray = self.dividerDashArray
  337. dividerOffsX = self.dividerOffsX
  338. dividerOffsY = self.dividerOffsY
  339. for i in range(n):
  340. if autoCP:
  341. col = autoCP
  342. col.index = i
  343. name = chartTexts[i]
  344. else:
  345. col, name = colorNamePairs[i]
  346. if isAuto(swatchMarker):
  347. col = swatchMarker
  348. col.index = i
  349. if isAuto(name):
  350. name = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None)).getSeriesName(i,'series %d' % i)
  351. T = _getLines(name)
  352. S = []
  353. aS = S.append
  354. j = int(i/(columnMaximum*1.0))
  355. jOffs = maxWidth[j]
  356. # thisy+dy/2 = y+leading/2
  357. y = y0 = thisy+(dy-ascent)*0.5
  358. if callout: callout(self,g,thisx,y,(col,name))
  359. if alignment == "left":
  360. x = thisx
  361. xn = thisx+jOffs[-1]+dxTextSpace
  362. elif alignment == "right":
  363. x = thisx+dx+dxTextSpace
  364. xn = thisx
  365. else:
  366. raise ValueError("bad alignment")
  367. if not isSeq(name):
  368. T = [T]
  369. lineCount = _getLineCount(name)
  370. yd = y
  371. for k,lines in enumerate(T):
  372. y = y0
  373. kk = k*2
  374. x1 = x+jOffs[kk]
  375. x2 = x+jOffs[kk+1]
  376. sc = subCols[k,i]
  377. anchor = sc.align
  378. scdx = sc.dx
  379. scdy = sc.dy
  380. fN = getattr(sc,'fontName',fontName)
  381. fS = getattr(sc,'fontSize',fontSize)
  382. fC = getattr(sc,'fillColor',fillColor)
  383. fL = sc.leading or 1.2*fontSize
  384. if fN==fontName:
  385. fA = (ascent*fS)/fontSize
  386. else:
  387. fA = getFont(fontName).face.ascent/1000.
  388. if fA==0: fA=0.718
  389. fA *= fS
  390. vA = sc.vAlign
  391. if vA=='top':
  392. vAdy = 0
  393. else:
  394. vAdy = -fL * (lineCount - len(lines))
  395. if vA=='middle': vAdy *= 0.5
  396. if anchor=='left':
  397. anchor = 'start'
  398. xoffs = x1
  399. elif anchor=='right':
  400. anchor = 'end'
  401. xoffs = x2
  402. elif anchor=='numeric':
  403. xoffs = x2
  404. else:
  405. anchor = 'middle'
  406. xoffs = 0.5*(x1+x2)
  407. for t in lines:
  408. aS(String(xoffs+scdx,y+scdy+vAdy,t,fontName=fN,fontSize=fS,fillColor=fC, textAnchor = anchor))
  409. y -= fL
  410. yd = min(yd,y)
  411. y += fL
  412. for iy, a in ((y-max(fL-fA,0),'underlines'),(y+fA,'overlines')):
  413. il = getattr(sc,a,None)
  414. if il:
  415. if not isinstance(il,(tuple,list)): il = (il,)
  416. for l in il:
  417. l = copy.copy(l)
  418. l.y1 += iy
  419. l.y2 += iy
  420. l.x1 += x1
  421. l.x2 += x2
  422. aS(l)
  423. x = xn
  424. y = yd
  425. leadingMove = 2*y0-y-thisy
  426. if dividerLines:
  427. xd = thisx+dx+dxTextSpace+jOffs[-1]+dividerOffsX[1]
  428. yd = thisy+dy*0.5+dividerOffsY
  429. if ((dividerLines&1) and i%columnMaximum) or ((dividerLines&2) and not i%columnMaximum):
  430. g.add(Line(thisx+dividerOffsX[0],yd,xd,yd,
  431. strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray))
  432. if (dividerLines&4) and (i%columnMaximum==lim or i==(n-1)):
  433. yd -= max(deltay,leadingMove)+yGap
  434. g.add(Line(thisx+dividerOffsX[0],yd,xd,yd,
  435. strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray))
  436. # Make a 'normal' color swatch...
  437. swatchX = x + getattr(self,'swdx',0)
  438. swatchY = thisy + getattr(self,'swdy',0)
  439. if isAuto(col):
  440. chart = getattr(col,'chart',getattr(col,'obj',None))
  441. c = chart.makeSwatchSample(getattr(col,'index',i),swatchX,swatchY,dx,dy)
  442. elif isinstance(col, colors.Color):
  443. if isSymbol(swatchMarker):
  444. c = uSymbol2Symbol(swatchMarker,swatchX+dx/2.,swatchY+dy/2.,col)
  445. else:
  446. c = self._defaultSwatch(swatchX,swatchY,dx,dy,fillColor=col,strokeWidth=strokeWidth,strokeColor=strokeColor)
  447. elif col is not None:
  448. try:
  449. c = copy.deepcopy(col)
  450. c.x = swatchX
  451. c.y = swatchY
  452. c.width = dx
  453. c.height = dy
  454. except:
  455. c = None
  456. else:
  457. c = None
  458. if c:
  459. g.add(c)
  460. if scallout: scallout(self,g,thisx,y0,i,(col,name),c)
  461. for s in S: g.add(s)
  462. if self.colEndCallout and (i%columnMaximum==lim or i==(n-1)):
  463. if alignment == "left":
  464. xt = thisx
  465. else:
  466. xt = thisx+dx+dxTextSpace
  467. yd = thisy+dy*0.5+dividerOffsY - (max(deltay,leadingMove)+yGap)
  468. self.colEndCallout(self, g, thisx, xt, yd, jOffs[-1], jOffs[-1]+dx+dxTextSpace)
  469. if i%columnMaximum==lim:
  470. if variColumn:
  471. thisx += jOffs[-1]+xW
  472. else:
  473. thisx = thisx+deltax
  474. thisy = upperlefty
  475. else:
  476. thisy = thisy-max(deltay,leadingMove)-yGap
  477. return g
  478. def demo(self):
  479. "Make sample legend."
  480. d = Drawing(200, 100)
  481. legend = Legend()
  482. legend.alignment = 'left'
  483. legend.x = 0
  484. legend.y = 100
  485. legend.dxTextSpace = 5
  486. items = 'red green blue yellow pink black white'.split()
  487. items = [(getattr(colors, i), i) for i in items]
  488. legend.colorNamePairs = items
  489. d.add(legend, 'legend')
  490. return d
  491. class TotalAnnotator(LegendColEndCallout):
  492. def __init__(self, lText='Total', rText='0.0', fontName=_baseGFontName, fontSize=10,
  493. fillColor=colors.black, strokeWidth=0.5, strokeColor=colors.black, strokeDashArray=None,
  494. dx=0, dy=0, dly=0, dlx=(0,0)):
  495. self.lText = lText
  496. self.rText = rText
  497. self.fontName = fontName
  498. self.fontSize = fontSize
  499. self.fillColor = fillColor
  500. self.dy = dy
  501. self.dx = dx
  502. self.dly = dly
  503. self.dlx = dlx
  504. self.strokeWidth = strokeWidth
  505. self.strokeColor = strokeColor
  506. self.strokeDashArray = strokeDashArray
  507. def __call__(self,legend, g, x, xt, y, width, lWidth):
  508. from reportlab.graphics.shapes import String, Line
  509. fontSize = self.fontSize
  510. fontName = self.fontName
  511. fillColor = self.fillColor
  512. strokeColor = self.strokeColor
  513. strokeWidth = self.strokeWidth
  514. ascent=getFont(fontName).face.ascent/1000.
  515. if ascent==0: ascent=0.718 # default (from helvetica)
  516. ascent *= fontSize
  517. leading = fontSize*1.2
  518. yt = y+self.dy-ascent*1.3
  519. if self.lText and fillColor:
  520. g.add(String(xt,yt,self.lText,
  521. fontName=fontName,
  522. fontSize=fontSize,
  523. fillColor=fillColor,
  524. textAnchor = "start"))
  525. if self.rText:
  526. g.add(String(xt+width,yt,self.rText,
  527. fontName=fontName,
  528. fontSize=fontSize,
  529. fillColor=fillColor,
  530. textAnchor = "end"))
  531. if strokeWidth and strokeColor:
  532. yL = y+self.dly-leading
  533. g.add(Line(x+self.dlx[0],yL,x+self.dlx[1]+lWidth,yL,
  534. strokeColor=strokeColor, strokeWidth=strokeWidth,
  535. strokeDashArray=self.strokeDashArray))
  536. class LineSwatch(Widget):
  537. """basically a Line with properties added so it can be used in a LineLegend"""
  538. _attrMap = AttrMap(
  539. x = AttrMapValue(isNumber, desc="x-coordinate for swatch line start point"),
  540. y = AttrMapValue(isNumber, desc="y-coordinate for swatch line start point"),
  541. width = AttrMapValue(isNumber, desc="length of swatch line"),
  542. height = AttrMapValue(isNumber, desc="used for line strokeWidth"),
  543. strokeColor = AttrMapValue(isColorOrNone, desc="color of swatch line"),
  544. strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc="dash array for swatch line"),
  545. )
  546. def __init__(self):
  547. from reportlab.lib.colors import red
  548. from reportlab.graphics.shapes import Line
  549. self.x = 0
  550. self.y = 0
  551. self.width = 20
  552. self.height = 1
  553. self.strokeColor = red
  554. self.strokeDashArray = None
  555. def draw(self):
  556. l = Line(self.x,self.y,self.x+self.width,self.y)
  557. l.strokeColor = self.strokeColor
  558. l.strokeDashArray = self.strokeDashArray
  559. l.strokeWidth = self.height
  560. return l
  561. class LineLegend(Legend):
  562. """A subclass of Legend for drawing legends with lines as the
  563. swatches rather than rectangles. Useful for lineCharts and
  564. linePlots. Should be similar in all other ways the the standard
  565. Legend class.
  566. """
  567. def __init__(self):
  568. Legend.__init__(self)
  569. # Size of swatch rectangle.
  570. self.dx = 10
  571. self.dy = 2
  572. def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor):
  573. l = LineSwatch()
  574. l.x = x
  575. l.y = thisy
  576. l.width = dx
  577. l.height = dy
  578. l.strokeColor = fillColor
  579. return l