grids.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  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/widgets/grids.py
  4. __version__='3.3.0'
  5. from reportlab.lib import colors
  6. from reportlab.lib.validators import isNumber, isColorOrNone, isBoolean, isListOfNumbers, OneOf, isListOfColors, isNumberOrNone
  7. from reportlab.lib.attrmap import AttrMap, AttrMapValue
  8. from reportlab.graphics.shapes import Drawing, Group, Line, Rect, LineShape, definePath, EmptyClipPath
  9. from reportlab.graphics.widgetbase import Widget
  10. def frange(start, end=None, inc=None):
  11. "A range function, that does accept float increments..."
  12. if end == None:
  13. end = start + 0.0
  14. start = 0.0
  15. if inc == None:
  16. inc = 1.0
  17. L = []
  18. end = end - inc*0.0001 #to avoid numrical problems
  19. while 1:
  20. next = start + len(L) * inc
  21. if inc > 0 and next >= end:
  22. break
  23. elif inc < 0 and next <= end:
  24. break
  25. L.append(next)
  26. return L
  27. def makeDistancesList(list):
  28. """Returns a list of distances between adjacent numbers in some input list.
  29. E.g. [1, 1, 2, 3, 5, 7] -> [0, 1, 1, 2, 2]
  30. """
  31. d = []
  32. for i in range(len(list[:-1])):
  33. d.append(list[i+1] - list[i])
  34. return d
  35. class Grid(Widget):
  36. """This makes a rectangular grid of equidistant stripes.
  37. The grid contains an outer border rectangle, and stripes
  38. inside which can be drawn with lines and/or as solid tiles.
  39. The drawing order is: outer rectangle, then lines and tiles.
  40. The stripes' width is indicated as 'delta'. The sequence of
  41. stripes can have an offset named 'delta0'. Both values need
  42. to be positive!
  43. """
  44. _attrMap = AttrMap(
  45. x = AttrMapValue(isNumber, desc="The grid's lower-left x position."),
  46. y = AttrMapValue(isNumber, desc="The grid's lower-left y position."),
  47. width = AttrMapValue(isNumber, desc="The grid's width."),
  48. height = AttrMapValue(isNumber, desc="The grid's height."),
  49. orientation = AttrMapValue(OneOf(('vertical', 'horizontal')),
  50. desc='Determines if stripes are vertical or horizontal.'),
  51. useLines = AttrMapValue(OneOf((0, 1)),
  52. desc='Determines if stripes are drawn with lines.'),
  53. useRects = AttrMapValue(OneOf((0, 1)),
  54. desc='Determines if stripes are drawn with solid rectangles.'),
  55. delta = AttrMapValue(isNumber,
  56. desc='Determines the width/height of the stripes.'),
  57. delta0 = AttrMapValue(isNumber,
  58. desc='Determines the stripes initial width/height offset.'),
  59. deltaSteps = AttrMapValue(isListOfNumbers,
  60. desc='List of deltas to be used cyclically.'),
  61. stripeColors = AttrMapValue(isListOfColors,
  62. desc='Colors applied cyclically in the right or upper direction.'),
  63. fillColor = AttrMapValue(isColorOrNone,
  64. desc='Background color for entire rectangle.'),
  65. strokeColor = AttrMapValue(isColorOrNone,
  66. desc='Color used for lines.'),
  67. strokeWidth = AttrMapValue(isNumber,
  68. desc='Width used for lines.'),
  69. rectStrokeColor = AttrMapValue(isColorOrNone, desc='Color for outer rect stroke.'),
  70. rectStrokeWidth = AttrMapValue(isNumberOrNone, desc='Width for outer rect stroke.'),
  71. )
  72. def __init__(self):
  73. self.x = 0
  74. self.y = 0
  75. self.width = 100
  76. self.height = 100
  77. self.orientation = 'vertical'
  78. self.useLines = 0
  79. self.useRects = 1
  80. self.delta = 20
  81. self.delta0 = 0
  82. self.deltaSteps = []
  83. self.fillColor = colors.white
  84. self.stripeColors = [colors.red, colors.green, colors.blue]
  85. self.strokeColor = colors.black
  86. self.strokeWidth = 2
  87. def demo(self):
  88. D = Drawing(100, 100)
  89. g = Grid()
  90. D.add(g)
  91. return D
  92. def makeOuterRect(self):
  93. strokeColor = getattr(self,'rectStrokeColor',self.strokeColor)
  94. strokeWidth = getattr(self,'rectStrokeWidth',self.strokeWidth)
  95. if self.fillColor or (strokeColor and strokeWidth):
  96. rect = Rect(self.x, self.y, self.width, self.height)
  97. rect.fillColor = self.fillColor
  98. rect.strokeColor = strokeColor
  99. rect.strokeWidth = strokeWidth
  100. return rect
  101. else:
  102. return None
  103. def makeLinePosList(self, start, isX=0):
  104. "Returns a list of positions where to place lines."
  105. w, h = self.width, self.height
  106. if isX:
  107. length = w
  108. else:
  109. length = h
  110. if self.deltaSteps:
  111. r = [start + self.delta0]
  112. i = 0
  113. while 1:
  114. if r[-1] > start + length:
  115. del r[-1]
  116. break
  117. r.append(r[-1] + self.deltaSteps[i % len(self.deltaSteps)])
  118. i = i + 1
  119. else:
  120. r = frange(start + self.delta0, start + length, self.delta)
  121. r.append(start + length)
  122. if self.delta0 != 0:
  123. r.insert(0, start)
  124. #print 'Grid.makeLinePosList() -> %s' % r
  125. return r
  126. def makeInnerLines(self):
  127. # inner grid lines
  128. group = Group()
  129. w, h = self.width, self.height
  130. if self.useLines == 1:
  131. if self.orientation == 'vertical':
  132. r = self.makeLinePosList(self.x, isX=1)
  133. for x in r:
  134. line = Line(x, self.y, x, self.y + h)
  135. line.strokeColor = self.strokeColor
  136. line.strokeWidth = self.strokeWidth
  137. group.add(line)
  138. elif self.orientation == 'horizontal':
  139. r = self.makeLinePosList(self.y, isX=0)
  140. for y in r:
  141. line = Line(self.x, y, self.x + w, y)
  142. line.strokeColor = self.strokeColor
  143. line.strokeWidth = self.strokeWidth
  144. group.add(line)
  145. return group
  146. def makeInnerTiles(self):
  147. # inner grid lines
  148. group = Group()
  149. w, h = self.width, self.height
  150. # inner grid stripes (solid rectangles)
  151. if self.useRects == 1:
  152. cols = self.stripeColors
  153. if self.orientation == 'vertical':
  154. r = self.makeLinePosList(self.x, isX=1)
  155. elif self.orientation == 'horizontal':
  156. r = self.makeLinePosList(self.y, isX=0)
  157. dist = makeDistancesList(r)
  158. i = 0
  159. for j in range(len(dist)):
  160. if self.orientation == 'vertical':
  161. x = r[j]
  162. stripe = Rect(x, self.y, dist[j], h)
  163. elif self.orientation == 'horizontal':
  164. y = r[j]
  165. stripe = Rect(self.x, y, w, dist[j])
  166. stripe.fillColor = cols[i % len(cols)]
  167. stripe.strokeColor = None
  168. group.add(stripe)
  169. i = i + 1
  170. return group
  171. def draw(self):
  172. # general widget bits
  173. group = Group()
  174. group.add(self.makeOuterRect())
  175. group.add(self.makeInnerTiles())
  176. group.add(self.makeInnerLines(),name='_gridLines')
  177. return group
  178. class DoubleGrid(Widget):
  179. """This combines two ordinary Grid objects orthogonal to each other.
  180. """
  181. _attrMap = AttrMap(
  182. x = AttrMapValue(isNumber, desc="The grid's lower-left x position."),
  183. y = AttrMapValue(isNumber, desc="The grid's lower-left y position."),
  184. width = AttrMapValue(isNumber, desc="The grid's width."),
  185. height = AttrMapValue(isNumber, desc="The grid's height."),
  186. grid0 = AttrMapValue(None, desc="The first grid component."),
  187. grid1 = AttrMapValue(None, desc="The second grid component."),
  188. )
  189. def __init__(self):
  190. self.x = 0
  191. self.y = 0
  192. self.width = 100
  193. self.height = 100
  194. g0 = Grid()
  195. g0.x = self.x
  196. g0.y = self.y
  197. g0.width = self.width
  198. g0.height = self.height
  199. g0.orientation = 'vertical'
  200. g0.useLines = 1
  201. g0.useRects = 0
  202. g0.delta = 20
  203. g0.delta0 = 0
  204. g0.deltaSteps = []
  205. g0.fillColor = colors.white
  206. g0.stripeColors = [colors.red, colors.green, colors.blue]
  207. g0.strokeColor = colors.black
  208. g0.strokeWidth = 1
  209. g1 = Grid()
  210. g1.x = self.x
  211. g1.y = self.y
  212. g1.width = self.width
  213. g1.height = self.height
  214. g1.orientation = 'horizontal'
  215. g1.useLines = 1
  216. g1.useRects = 0
  217. g1.delta = 20
  218. g1.delta0 = 0
  219. g1.deltaSteps = []
  220. g1.fillColor = colors.white
  221. g1.stripeColors = [colors.red, colors.green, colors.blue]
  222. g1.strokeColor = colors.black
  223. g1.strokeWidth = 1
  224. self.grid0 = g0
  225. self.grid1 = g1
  226. ## # This gives an AttributeError:
  227. ## # DoubleGrid instance has no attribute 'grid0'
  228. ## def __setattr__(self, name, value):
  229. ## if name in ('x', 'y', 'width', 'height'):
  230. ## setattr(self.grid0, name, value)
  231. ## setattr(self.grid1, name, value)
  232. def demo(self):
  233. D = Drawing(100, 100)
  234. g = DoubleGrid()
  235. D.add(g)
  236. return D
  237. def draw(self):
  238. group = Group()
  239. g0, g1 = self.grid0, self.grid1
  240. # Order groups to make sure both v and h lines
  241. # are visible (works only when there is only
  242. # one kind of stripes, v or h).
  243. G = g0.useRects == 1 and g1.useRects == 0 and (g0,g1) or (g1,g0)
  244. for g in G:
  245. group.add(g.makeOuterRect())
  246. for g in G:
  247. group.add(g.makeInnerTiles())
  248. group.add(g.makeInnerLines(),name='_gridLines')
  249. return group
  250. class ShadedRect(Widget):
  251. """This makes a rectangle with shaded colors between two colors.
  252. Colors are interpolated linearly between 'fillColorStart'
  253. and 'fillColorEnd', both of which appear at the margins.
  254. If 'numShades' is set to one, though, only 'fillColorStart'
  255. is used.
  256. """
  257. _attrMap = AttrMap(
  258. x = AttrMapValue(isNumber, desc="The grid's lower-left x position."),
  259. y = AttrMapValue(isNumber, desc="The grid's lower-left y position."),
  260. width = AttrMapValue(isNumber, desc="The grid's width."),
  261. height = AttrMapValue(isNumber, desc="The grid's height."),
  262. orientation = AttrMapValue(OneOf(('vertical', 'horizontal')), desc='Determines if stripes are vertical or horizontal.'),
  263. numShades = AttrMapValue(isNumber, desc='The number of interpolating colors.'),
  264. fillColorStart = AttrMapValue(isColorOrNone, desc='Start value of the color shade.'),
  265. fillColorEnd = AttrMapValue(isColorOrNone, desc='End value of the color shade.'),
  266. strokeColor = AttrMapValue(isColorOrNone, desc='Color used for border line.'),
  267. strokeWidth = AttrMapValue(isNumber, desc='Width used for lines.'),
  268. cylinderMode = AttrMapValue(isBoolean, desc='True if shading reverses in middle.'),
  269. )
  270. def __init__(self,**kw):
  271. self.x = 0
  272. self.y = 0
  273. self.width = 100
  274. self.height = 100
  275. self.orientation = 'vertical'
  276. self.numShades = 20
  277. self.fillColorStart = colors.pink
  278. self.fillColorEnd = colors.black
  279. self.strokeColor = colors.black
  280. self.strokeWidth = 2
  281. self.cylinderMode = 0
  282. self.setProperties(kw)
  283. def demo(self):
  284. D = Drawing(100, 100)
  285. g = ShadedRect()
  286. D.add(g)
  287. return D
  288. def _flipRectCorners(self):
  289. "Flip rectangle's corners if width or height is negative."
  290. x, y, width, height, fillColorStart, fillColorEnd = self.x, self.y, self.width, self.height, self.fillColorStart, self.fillColorEnd
  291. if width < 0 and height > 0:
  292. x = x + width
  293. width = -width
  294. if self.orientation=='vertical': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart
  295. elif height<0 and width>0:
  296. y = y + height
  297. height = -height
  298. if self.orientation=='horizontal': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart
  299. elif height < 0 and height < 0:
  300. x = x + width
  301. width = -width
  302. y = y + height
  303. height = -height
  304. return x, y, width, height, fillColorStart, fillColorEnd
  305. def draw(self):
  306. # general widget bits
  307. group = Group()
  308. x, y, w, h, c0, c1 = self._flipRectCorners()
  309. numShades = self.numShades
  310. if self.cylinderMode:
  311. if not numShades%2: numShades = numShades+1
  312. halfNumShades = int((numShades-1)/2) + 1
  313. num = float(numShades) # must make it float!
  314. vertical = self.orientation == 'vertical'
  315. if vertical:
  316. if numShades == 1:
  317. V = [x]
  318. else:
  319. V = frange(x, x + w, w/num)
  320. else:
  321. if numShades == 1:
  322. V = [y]
  323. else:
  324. V = frange(y, y + h, h/num)
  325. for v in V:
  326. stripe = vertical and Rect(v, y, w/num, h) or Rect(x, v, w, h/num)
  327. if self.cylinderMode:
  328. if V.index(v)>=halfNumShades:
  329. col = colors.linearlyInterpolatedColor(c1,c0,V[halfNumShades],V[-1], v)
  330. else:
  331. col = colors.linearlyInterpolatedColor(c0,c1,V[0],V[halfNumShades], v)
  332. else:
  333. col = colors.linearlyInterpolatedColor(c0,c1,V[0],V[-1], v)
  334. stripe.fillColor = col
  335. stripe.strokeColor = col
  336. stripe.strokeWidth = 1
  337. group.add(stripe)
  338. if self.strokeColor and self.strokeWidth>=0:
  339. rect = Rect(x, y, w, h)
  340. rect.strokeColor = self.strokeColor
  341. rect.strokeWidth = self.strokeWidth
  342. rect.fillColor = None
  343. group.add(rect)
  344. return group
  345. def colorRange(c0, c1, n):
  346. "Return a range of intermediate colors between c0 and c1"
  347. if n==1: return [c0]
  348. C = []
  349. if n>1:
  350. lim = n-1
  351. for i in range(n):
  352. C.append(colors.linearlyInterpolatedColor(c0,c1,0,lim, i))
  353. return C
  354. def centroid(P):
  355. '''compute average point of a set of points'''
  356. cx = 0
  357. cy = 0
  358. for x,y in P:
  359. cx+=x
  360. cy+=y
  361. n = float(len(P))
  362. return cx/n, cy/n
  363. def rotatedEnclosingRect(P, angle, rect):
  364. '''
  365. given P a sequence P of x,y coordinate pairs and an angle in degrees
  366. find the centroid of P and the axis at angle theta through it
  367. find the extreme points of P wrt axis parallel distance and axis
  368. orthogonal distance. Then compute the least rectangle that will still
  369. enclose P when rotated by angle.
  370. The class R
  371. '''
  372. from math import pi, cos, sin, tan
  373. x0, y0 = centroid(P)
  374. theta = (angle/180.)*pi
  375. s,c=sin(theta),cos(theta)
  376. def parallelAxisDist(xy,s=s,c=c,x0=x0,y0=y0):
  377. x,y = xy
  378. return (s*(y-y0)+c*(x-x0))
  379. def orthogonalAxisDist(xy,s=s,c=c,x0=x0,y0=y0):
  380. x,y = xy
  381. return (c*(y-y0)+s*(x-x0))
  382. L = list(map(parallelAxisDist,P))
  383. L.sort()
  384. a0, a1 = L[0], L[-1]
  385. L = list(map(orthogonalAxisDist,P))
  386. L.sort()
  387. b0, b1 = L[0], L[-1]
  388. rect.x, rect.width = a0, a1-a0
  389. rect.y, rect.height = b0, b1-b0
  390. g = Group(transform=(c,s,-s,c,x0,y0))
  391. g.add(rect)
  392. return g
  393. class ShadedPolygon(Widget,LineShape):
  394. _attrMap = AttrMap(BASE=LineShape,
  395. angle = AttrMapValue(isNumber,desc="Shading angle"),
  396. fillColorStart = AttrMapValue(isColorOrNone),
  397. fillColorEnd = AttrMapValue(isColorOrNone),
  398. numShades = AttrMapValue(isNumber, desc='The number of interpolating colors.'),
  399. cylinderMode = AttrMapValue(isBoolean, desc='True if shading reverses in middle.'),
  400. points = AttrMapValue(isListOfNumbers),
  401. )
  402. def __init__(self,**kw):
  403. self.angle = 90
  404. self.fillColorStart = colors.red
  405. self.fillColorEnd = colors.green
  406. self.cylinderMode = 0
  407. self.numShades = 50
  408. self.points = [-1,-1,2,2,3,-1]
  409. LineShape.__init__(self,kw)
  410. def draw(self):
  411. P = self.points
  412. P = list(map(lambda i, P=P:(P[i],P[i+1]),range(0,len(P),2)))
  413. path = definePath([('moveTo',)+P[0]]+[('lineTo',)+x for x in P[1:]]+['closePath'],
  414. fillColor=None, strokeColor=None)
  415. path.isClipPath = 1
  416. g = Group()
  417. g.add(path)
  418. angle = self.angle
  419. orientation = 'vertical'
  420. if angle==180:
  421. angle = 0
  422. elif angle in (90,270):
  423. orientation ='horizontal'
  424. angle = 0
  425. rect = ShadedRect(strokeWidth=0,strokeColor=None,orientation=orientation)
  426. for k in 'fillColorStart', 'fillColorEnd', 'numShades', 'cylinderMode':
  427. setattr(rect,k,getattr(self,k))
  428. g.add(rotatedEnclosingRect(P, angle, rect))
  429. g.add(EmptyClipPath)
  430. path = path.copy()
  431. path.isClipPath = 0
  432. path.strokeColor = self.strokeColor
  433. path.strokeWidth = self.strokeWidth
  434. g.add(path)
  435. return g
  436. if __name__=='__main__': #noruntests
  437. from reportlab.lib.colors import blue
  438. from reportlab.graphics.shapes import Drawing
  439. angle=45
  440. D = Drawing(120,120)
  441. D.add(ShadedPolygon(points=(10,10,60,60,110,10),strokeColor=None,strokeWidth=1,angle=90,numShades=50,cylinderMode=0))
  442. D.save(formats=['gif'],fnRoot='shobj',outDir='/tmp')