1 # -*- coding: utf-8 -*-
3 # Copyright (C) 2000-2005 by Yasushi Saito (yasushi.saito@gmail.com)
5 # Jockey is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2, or (at your option) any
10 # Jockey is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
25 def _compute_bounding_box(points):
26 """Given the list of coordinates (x,y), this procedure computes
27 the smallest rectangle that covers all the points."""
28 (xmin, ymin, xmax, ymax) = (999999, 999999, -999999, -999999)
30 xmin = min(xmin, p[0])
31 xmax = max(xmax, p[0])
32 ymin = min(ymin, p[1])
33 ymax = max(ymax, p[1])
34 return (xmin, ymin, xmax, ymax)
36 def _intersect_box(b1, b2):
37 xmin = max(b1[0], b2[0])
38 ymin = max(b1[1], b2[1])
39 xmax = min(b1[2], b2[2])
40 ymax = min(b1[3], b2[3])
41 return (xmin, ymin, xmax, ymax)
43 def invisible_p(x, y):
44 """Return true if the point (X, Y) is visible in the canvas."""
45 if x < -499999 or y < -499999:
50 return deg*2*math.pi / 360.0
53 return ( (p1[0]+p2[0])/2.0, (p1[1]+p2[1])/2.0 )
61 global active_canvases
63 self.__xmax = -InvalidCoord
64 self.__xmin = InvalidCoord
65 self.__ymax = -InvalidCoord
66 self.__ymin = InvalidCoord
67 self.__clip_box = (-InvalidCoord, -InvalidCoord, InvalidCoord, InvalidCoord)
68 self.__clip_stack = []
71 self.title = re.sub("(.*)\\.py$", "\\1", sys.argv[0])
72 self.creator = "pychart %s" % (version.version,)
73 self.creation_date = time.strftime("(%m/%d/%y) (%I:%M %p)")
74 self.aux_comments = ""
76 active_canvases.append(self)
78 def set_title(self, s):
79 """Define the string to shown in EPS/PDF "Title" field. The default value is the name of the script that creates the EPS/PDF file."""
82 def set_creator(self, tag):
83 """Define the string to be shown in EPS %%Creator or PDF Producer field. The default value is "pychart"."""
86 def set_creation_date(self, s):
87 """Define the string to shown in EPS/PDF "CreationDate" field. Defalt value of this field is the current time."""
88 self.creation_date = s
90 def set_author(self, s):
91 """Set the author string. Unless this method is called, the Author field is not output in EPS or PDF."""
94 def add_aux_comments(self, s):
95 """Define an auxiliary comments to be output to the file, just after the required headers"""
96 self.aux_comments += s
99 """This method closes the canvas and writes
100 contents to the associated file.
101 Calling this procedure is optional, because
102 Pychart calls this procedure for every open canvas on normal exit."""
103 for i in range(0, len(active_canvases)):
104 if active_canvases[i] == self:
105 del active_canvases[i]
108 def open_output(self, fname):
109 """Open the output file FNAME. Returns tuple (FD, NEED_CLOSE),
110 where FD is a file (or file-like) object, and NEED_CLOSE is a
111 boolean flag that tells whether FD.close() should be called
112 after finishing writing to the file.
114 FNAME can be one of the three things:
115 (1) None, in which case (sys.stdout, False) is returned.
116 (2) A file-like object, in which case (fname, False) is returned.
117 (3) A string, in which case this procedure opens the file and returns
121 return (sys.stdout, False)
122 elif isinstance(fname, str):
123 return (file(fname, "wb"), True)
125 if not hasattr(fname, "write"):
126 raise Exception, "Expecting either a filename or a file-like object, but got %s" % fname
127 return (fname, False)
129 def setbb(self, x, y):
130 """Call this method when point (X,Y) is to be drawn in the
131 canvas. This methods expands the bounding box to include
133 self.__xmin = min(self.__xmin, max(x, self.__clip_box[0]))
134 self.__xmax = max(self.__xmax, min(x, self.__clip_box[2]))
135 self.__ymin = min(self.__ymin, max(y, self.__clip_box[1]))
136 self.__ymax = max(self.__ymax, min(y, self.__clip_box[3]))
138 def fill_with_pattern(self, pat, x1, y1, x2, y2):
139 if invisible_p(x2, y2): return
141 self.comment("FILL pat=%s (%d %d)-(%d %d)\n" % (pat, x1, y1, x2, y2))
142 self.set_fill_color(pat.bgcolor)
143 self._path_polygon([(x1, y1), (x1, y2), (x2, y2), (x2, y1)])
145 pat.draw(self, x1, y1, x2, y2)
146 self.comment("end FILL.\n")
148 def _path_polygon(self, points):
149 "Low-level polygon-drawing routine."
150 (xmin, ymin, xmax, ymax) = _compute_bounding_box(points)
151 if invisible_p(xmax, ymax):
153 self.setbb(xmin, ymin)
154 self.setbb(xmax, ymax)
157 self.moveto(xscale(points[0][0]), yscale(points[0][1]))
158 for point in points[1:]:
159 self.lineto(xscale(point[0]), yscale(point[1]))
162 def polygon(self, edge_style, pat, points, shadow = None):
163 """Draw a polygon with EDGE_STYLE, fill with PAT, and the edges
164 POINTS. POINTS is a sequence of coordinates, e.g., ((10,10), (15,5),
165 (20,8)). SHADOW is either None or a tuple (XDELTA, YDELTA,
166 fillstyle). If non-null, a shadow of FILLSTYLE is drawn beneath
167 the polygon at the offset of (XDELTA, YDELTA)."""
170 self.comment("POLYGON points=[%s] pat=[%s]"
171 % (str(points), str(pat)))
172 (xmin, ymin, xmax, ymax) = _compute_bounding_box(points)
175 xoff, yoff, shadow_pat = shadow
177 self._path_polygon(map(lambda p, xoff=xoff, yoff=yoff: (p[0]+xoff, p[1]+yoff), points))
179 self.fill_with_pattern(shadow_pat, xmin+xoff, ymin+yoff,
180 xmax+xoff, ymax+yoff)
184 self._path_polygon(points)
186 self.fill_with_pattern(pat, xmin, ymin, xmax, ymax)
189 self.comment("POLYGON points=[%s] edge=[%s]"
190 % (str(points), str(edge_style)))
191 self.set_line_style(edge_style)
192 self._path_polygon(points)
195 def set_background(self, pat, x1, y1, x2, y2):
196 xmax, xmin, ymax, ymin = self.__xmax, self.__xmin, self.__ymax, self.__ymin
197 self.rectangle(None, pat, x1, y1, x2, y2)
198 self.__xmax, self.__xmin, self.__ymax, self.__ymin = xmax, xmin, ymax, ymin
200 def rectangle(self, edge_style, pat, x1, y1, x2, y2, shadow = None):
201 """Draw a rectangle with EDGE_STYLE, fill with PAT, and the
202 bounding box (X1, Y1, X2, Y2). SHADOW is either None or a
203 tuple (XDELTA, YDELTA, fillstyle). If non-null, a shadow of
204 FILLSTYLE is drawn beneath the polygon at the offset of
207 self.polygon(edge_style, pat, [(x1,y1), (x1,y2), (x2,y2), (x2, y1)],
210 def _path_ellipsis(self, x, y, radius, ratio, start_angle, end_angle):
211 self.setbb(x - radius, y - radius*ratio)
212 self.setbb(x + radius, y + radius*ratio)
213 oradius = nscale(radius)
214 centerx, centery = xscale(x), yscale(y)
215 startx, starty = centerx+oradius * math.cos(to_radian(start_angle)), \
216 centery+oradius * math.sin(to_radian(start_angle))
217 self.moveto(centerx, centery)
218 if start_angle % 360 != end_angle % 360:
219 self.moveto(centerx, centery)
220 self.lineto(startx, starty)
222 self.moveto(startx, starty)
223 self.path_arc(xscale(x), yscale(y), nscale(radius),
224 ratio, start_angle, end_angle)
227 def ellipsis(self, line_style, pattern, x, y, radius, ratio = 1.0,
228 start_angle=0, end_angle=360, shadow=None):
229 """Draw an ellipsis with line_style and fill PATTERN. The center is \
230 (X, Y), X radius is RADIUS, and Y radius is RADIUS*RATIO, whose \
231 default value is 1.0. SHADOW is either None or a tuple (XDELTA,
232 YDELTA, fillstyle). If non-null, a shadow of FILLSTYLE is drawn
233 beneath the polygon at the offset of (XDELTA, YDELTA)."""
235 if invisible_p(x + radius, y + radius*ratio):
240 x_off, y_off, shadow_pat = shadow
243 self._path_ellipsis(x+x_off, y+y_off, radius, ratio,
244 start_angle, end_angle)
246 self.fill_with_pattern(shadow_pat,
248 y-radius*ratio*2+y_off,
250 y+radius*ratio*2+y_off)
254 self._path_ellipsis(x, y, radius, ratio, start_angle, end_angle)
256 self.fill_with_pattern(pattern,
257 (x-radius*2), (y-radius*ratio*2),
258 (x+radius*2), (y+radius*ratio*2))
261 self.set_line_style(line_style)
263 self._path_ellipsis(x, y, radius, ratio, start_angle, end_angle)
266 def clip_ellipsis(self, x, y, radius, ratio = 1.0):
267 """Create an elliptical clip region. You must call endclip() after
268 you completed drawing. See also the ellipsis method."""
272 self.moveto(xscale(x)+nscale(radius), yscale(y))
273 self.path_arc(xscale(x), yscale(y), nscale(radius), ratio, 0, 360)
275 self.__clip_stack.append(self.__clip_box)
278 def clip_polygon(self, points):
279 """Create a polygonal clip region. You must call endclip() after
280 you completed drawing. See also the polygon method."""
282 self._path_polygon(points)
283 self.__clip_stack.append(self.__clip_box)
284 self.__clip_box = _intersect_box(self.__clip_box, _compute_bounding_box(points))
287 def clip(self, x1, y1, x2, y2):
288 """Activate a rectangular clip region, (X1, Y1) - (X2, Y2).
289 You must call endclip() after you completed drawing.
291 canvas.clip(x,y,x2,y2)
296 self.__clip_stack.append(self.__clip_box)
297 self.__clip_box = _intersect_box(self.__clip_box, (x1, y1, x2, y2))
300 self.moveto(xscale(x1), yscale(y1))
301 self.lineto(xscale(x1), yscale(y2))
302 self.lineto(xscale(x2), yscale(y2))
303 self.lineto(xscale(x2), yscale(y1))
308 """End the current clip region. When clip calls are nested, it
309 ends the most recently created crip region."""
310 self.__clip_box = self.__clip_stack[-1]
311 del self.__clip_stack[-1]
314 def curve(self, style, points):
316 self.setbb(p[0], p[1])
318 self.set_line_style(style)
319 self.moveto(xscale(points[0][0]), xscale(points[0][1]))
322 while i < len(points):
330 x4 = midpoint(x3, points[i])
331 self.curveto(xscale(x2[0]), xscale(x2[1]),
332 xscale(x3[0]), xscale(x3[1]),
333 xscale(x4[0]), xscale(x4[1]))
339 self.lineto(xscale(x2[0]), xscale(x2[1]))
341 self.curveto(xscale(x2[0]), xscale(x2[1]),
342 xscale(x2[0]), xscale(x2[1]),
343 xscale(x3[0]), xscale(x3[1]))
346 def line(self, style, x1, y1, x2, y2):
349 if invisible_p(x2, y2) and invisible_p(x1, y1):
356 self.set_line_style(style)
357 self.moveto(xscale(x1), yscale(y1))
358 self.lineto(xscale(x2), yscale(y2))
361 def lines(self, style, segments):
364 (xmin, ymin, xmax, ymax) = _compute_bounding_box(segments)
365 if invisible_p(xmax, ymax):
368 self.setbb(xmin, ymin)
369 self.setbb(xmax, ymax)
371 self.set_line_style(style)
372 self.moveto(xscale(segments[0][0]), xscale(segments[0][1]))
373 for i in range(1, len(segments)):
374 self.lineto(xscale(segments[i][0]), yscale(segments[i][1]))
377 def _path_round_rectangle(self, x1, y1, x2, y2, radius):
378 self.moveto(xscale(x1 + radius), yscale(y1))
379 self.lineto(xscale(x2 - radius), yscale(y1))
380 self.path_arc(xscale(x2-radius), yscale(y1+radius), nscale(radius), 1, 270, 360)
381 self.lineto(xscale(x2), yscale(y2-radius))
382 self.path_arc(xscale(x2-radius), yscale(y2-radius), nscale(radius), 1, 0, 90)
383 self.lineto(xscale(x1+radius), yscale(y2))
384 self.path_arc(xscale(x1 + radius), yscale(y2 - radius), nscale(radius), 1, 90, 180)
385 self.lineto(xscale(x1), xscale(y1+radius))
386 self.path_arc(xscale(x1 + radius), yscale(y1 + radius), nscale(radius), 1, 180, 270)
388 def round_rectangle(self, style, fill, x1, y1, x2, y2, radius, shadow=None):
389 """Draw a rectangle with rounded four corners. Parameter <radius> specifies the radius of each corner."""
391 if invisible_p(x2, y2):
398 x_off, y_off, shadow_fill = shadow
401 self._path_round_rectangle(x1+x_off, y1+y_off, x2+x_off, y2+y_off,
405 self.fill_with_pattern(shadow_fill, x1+x_off, y1+y_off,
411 self._path_round_rectangle(x1, y1, x2, y2, radius)
414 self.fill_with_pattern(fill, x1, y1, x2, y2)
417 self.set_line_style(style)
419 self._path_round_rectangle(x1, y1, x2, y2, radius)
423 def show(self, x, y, str):
428 if invisible_p(x, y):
431 (xmin, xmax, ymin, ymax) = font.get_dimension(str)
433 # rectangle(line_style.default, None, x+xmin, y+ymin, x+xmax, y+ymax)
434 # ellipsis(line_style.default, None, x, y, 1)
435 self.setbb(x+xmin, y+ymin)
436 self.setbb(x+xmax, y+ymax)
438 (halign, valign, angle) = font.get_align(str)
443 # Handle vertical alignment
445 y = font.unaligned_text_height(str)
449 y = font.unaligned_text_height(str) / 2.0
451 (xmin, xmax, ymin, ymax) = font.get_dimension(org_str)
452 # print org_str, xmin, xmax, ymin, ymax, x, y_org, y
453 self.setbb(x+xmin, y_org+y+ymin)
454 self.setbb(x+xmax, y_org+y+ymax)
455 itr = font.text_iterator(None)
460 for line in str.split('\n'):
473 (font_name, size, line_height, color, _h, _v, _a, str) = elem
474 cur_width += font.line_width(font_name, size, str)
475 max_width = max(cur_width, max_width)
476 cur_height = max(cur_height, line_height)
478 # replace '(' -> '\(', ')' -> '\)' to make
479 # Postscript string parser happy.
480 str = str.replace("(", "\\(")
481 str = str.replace(")", "\\)")
482 strs.append((font_name, size, color, str))
483 lines.append((cur_width, cur_height, strs))
486 cur_width, cur_height, strs = line
487 cur_y = y - cur_height
489 self.comment("cury: %d hei %d str %s\n" % (cur_y, cur_height, strs))
491 cur_x = -cur_width/2.0
497 rel_x, rel_y = pychart_util.rotate(cur_x, cur_y, angle)
499 self.text_moveto(xscale(base_x + rel_x),
500 yscale(base_y + rel_y), angle)
502 font_name, size, color, str = segment
503 self.text_show(font_name, nscale(size), color, str)